package modules import ( "context" "database/sql" "encoding/json" "time" "github.com/heroiclabs/nakama-common/runtime" ) // RegisterMap 注册地图/副本/事件相关 RPC。 func RegisterMap(initializer runtime.Initializer) error { rpcs := map[string]func(context.Context, runtime.Logger, *sql.DB, runtime.NakamaModule, string) (string, error){ "MapService/GetRegion": getRegion, "MapService/GetNearby": getNearby, "MapService/EnterInstance": enterInstance, "MapService/ListWorldEvents": listWorldEvents, "MapService/PublishPlayerEvent": publishPlayerEvent, } for name, fn := range rpcs { if err := initializer.RegisterRpc(name, fn); err != nil { return err } } return nil } type regionReq struct { RegionID string `json:"region_id"` } type nearbyReq struct { RegionID string `json:"region_id"` Limit int32 `json:"limit"` EventTypes []string `json:"event_types"` ExcludeCharacterID string `json:"exclude_character_id"` } type enterInstanceReq struct { InstanceID string `json:"instance_id"` PartyMembers []string `json:"party_members"` ConsumableKeyID string `json:"consumable_key_id"` UseStamina bool `json:"use_stamina"` LeaderID string `json:"leader_id"` } type worldEventReq struct { WorldTier int32 `json:"world_tier"` RegionID string `json:"region_id"` EventScope string `json:"event_scope"` Page int32 `json:"page"` PageSize int32 `json:"page_size"` } type playerEventReq struct { EventType string `json:"event_type"` RegionID string `json:"region_id"` CostItems []materialCostReq `json:"cost_items"` Description string `json:"description"` } type regionData struct { RegionID string `json:"region_id"` WorldTier int32 `json:"world_tier"` Name string `json:"name"` IsSafeZone bool `json:"is_safe_zone"` PvpRules interface{} `json:"pvp_rules"` Weather string `json:"weather"` ResourceDensity int32 `json:"resource_density"` DangerLevel int32 `json:"danger_level"` NearbyPlayerCount int32 `json:"nearby_player_count"` Zones interface{} `json:"zones"` } type zoneData struct { ZoneID string `json:"zone_id"` ZoneType string `json:"zone_type"` Name string `json:"name"` Terrain string `json:"terrain"` DangerLevel int32 `json:"danger_level"` Rarity string `json:"rarity"` } type nearbyData struct { Players interface{} `json:"players"` Events interface{} `json:"events"` } type instanceRunData struct { RunID string `json:"run_id"` InstanceID string `json:"instance_id"` Status string `json:"status"` DifficultyCoefficient float64 `json:"difficulty_coefficient"` ConsumedKeys int32 `json:"consumed_keys"` StartedAt string `json:"started_at"` } type worldEventItemData struct { EventID string `json:"event_id"` EventType string `json:"event_type"` WorldTier int32 `json:"world_tier"` RegionID string `json:"region_id"` Title string `json:"title"` Content string `json:"content"` ExpiresAt string `json:"expires_at"` } type worldEventListData struct { Total int32 `json:"total"` List []worldEventItemData `json:"list"` } type worldEventData struct { EventID string `json:"event_id"` EventType string `json:"event_type"` Status string `json:"status"` CreatedAt string `json:"created_at"` } func getRegion(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req regionReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(8001, "invalid payload", traceID) } // 查询区域信息 var name string var worldTier int32 var isSafeZone bool err := db.QueryRowContext(ctx, ` SELECT name, world_tier, is_safe_zone FROM regions WHERE id = $1 `, req.RegionID).Scan(&name, &worldTier, &isSafeZone) if err != nil { return errResp(8002, "region not found", traceID) } // 查询区域内的区域 rows, err := db.QueryContext(ctx, ` SELECT id, zone_type, name, terrain, danger_level, rarity FROM zones WHERE region_id = $1 ORDER BY danger_level ASC `, req.RegionID) if err != nil { logger.Error("get region zones failed: %v", err) return errResp(9002, "internal error", traceID) } defer rows.Close() var zones []zoneData for rows.Next() { var z zoneData if err := rows.Scan(&z.ZoneID, &z.ZoneType, &z.Name, &z.Terrain, &z.DangerLevel, &z.Rarity); err != nil { continue } zones = append(zones, z) } return okResp(regionData{ RegionID: req.RegionID, Name: name, WorldTier: worldTier, IsSafeZone: isSafeZone, Zones: zones, }, traceID) } func getNearby(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req nearbyReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(8001, "invalid payload", traceID) } if req.Limit <= 0 || req.Limit > 50 { req.Limit = 20 } // 查询附近玩家(同区域) rows, err := db.QueryContext(ctx, ` SELECT c.id, c.name, c.race_id, c.world_tier, c.realm_tier FROM characters c JOIN regions r ON c.world_tier = r.world_tier WHERE r.id = $1 AND c.status = 'active' AND c.id != $2 ORDER BY c.last_online_at DESC LIMIT $3 `, req.RegionID, req.ExcludeCharacterID, req.Limit) if err != nil { logger.Error("get nearby players failed: %v", err) return errResp(9002, "internal error", traceID) } defer rows.Close() type playerInfo struct { ID string `json:"id"` Name string `json:"name"` RaceID string `json:"race_id"` WorldTier int32 `json:"world_tier"` RealmTier int32 `json:"realm_tier"` } var players []playerInfo for rows.Next() { var p playerInfo if err := rows.Scan(&p.ID, &p.Name, &p.RaceID, &p.WorldTier, &p.RealmTier); err != nil { continue } players = append(players, p) } return okResp(nearbyData{ Players: players, Events: []interface{}{}, }, traceID) } func enterInstance(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req enterInstanceReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(8003, "invalid payload", traceID) } // 校验副本是否存在 var instanceExists bool var recommendedRealm int32 err := db.QueryRowContext(ctx, ` SELECT EXISTS(SELECT 1 FROM instances WHERE id = $1), recommended_realm_tier FROM instances WHERE id = $1 `, req.InstanceID).Scan(&instanceExists, &recommendedRealm) if err != nil || !instanceExists { return errResp(8004, "instance not found", traceID) } // 创建副本运行记录 var runID string err = db.QueryRowContext(ctx, ` INSERT INTO instance_runs (instance_id, party_leader_id, party_members, difficulty_coefficient, status, consumed_keys, started_at) VALUES ($1, $2, $3, 1.15, 'in_progress', 1, NOW()) RETURNING id::text `, req.InstanceID, req.LeaderID, req.PartyMembers).Scan(&runID) if err != nil { logger.Error("enter instance failed: %v", err) return errResp(9002, "internal error", traceID) } logger.Info("MapService/EnterInstance success: run_id=%s", runID) return okResp(instanceRunData{ RunID: runID, InstanceID: req.InstanceID, Status: "in_progress", DifficultyCoefficient: 1.15, ConsumedKeys: 1, }, traceID) } func listWorldEvents(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() var req worldEventReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(8001, "invalid payload", traceID) } // 简化版:返回示例事件 events := []worldEventData{ {EventID: "evt_001", EventType: "world_boss", Status: "active", CreatedAt: time.Now().Format(time.RFC3339)}, {EventID: "evt_002", EventType: "resource_spawn", Status: "active", CreatedAt: time.Now().Format(time.RFC3339)}, } return okResp(map[string]interface{}{ "total": len(events), "list": events, }, traceID) } func publishPlayerEvent(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) { traceID := newTraceID() uid := userIDFromCtx(ctx) if uid == "" { return errResp(1001, "missing token", traceID) } var req playerEventReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(8009, "invalid payload", traceID) } // 获取角色ID var charID string err := db.QueryRowContext(ctx, ` SELECT id FROM characters WHERE player_id = $1 AND status = 'active' ORDER BY created_at DESC LIMIT 1 `, uid).Scan(&charID) if err != nil { return errResp(7003, "character not found", traceID) } // 校验事件类型 validTypes := map[string]bool{"pvp": true, "trade": true, "help": true, "event": true} if !validTypes[req.EventType] { return errResp(8010, "invalid event type", traceID) } // 创建事件记录(简化版) eventID := genUUID() logger.Info("MapService/PublishPlayerEvent success: event_id=%s", eventID) return okResp(worldEventData{ EventID: eventID, EventType: req.EventType, Status: "active", }, traceID) }