package modules import ( "context" "database/sql" "encoding/json" "github.com/heroiclabs/nakama-common/runtime" ) // RegisterCharacter 注册角色相关 RPC。 func RegisterCharacter(initializer runtime.Initializer) error { if err := initializer.RegisterRpc("CharacterService/CreateCharacter", createCharacter); err != nil { return err } if err := initializer.RegisterRpc("CharacterService/GetCharacter", getCharacter); err != nil { return err } return nil } type createCharacterReq struct { Name string `json:"name"` RaceID string `json:"race_id"` BirthWorldTier int32 `json:"birth_world_tier"` } type getCharacterReq struct { CharacterID string `json:"character_id"` WithSnapshot bool `json:"with_snapshot"` } type characterData struct { CharacterID string `json:"character_id"` PlayerID string `json:"player_id"` Name string `json:"name"` RaceID string `json:"race_id"` WorldTier int32 `json:"world_tier"` RealmTier int32 `json:"realm_tier"` MinorRealm int32 `json:"minor_realm"` RealmStatus string `json:"realm_status"` Level int32 `json:"level"` Exp int64 `json:"exp"` Status string `json:"status"` BaseStats string `json:"base_stats"` BattleStats string `json:"battle_stats"` SanCurrent int32 `json:"san_current"` SanMax int32 `json:"san_max"` CrimeScore int32 `json:"crime_score"` HeavenlyValue int32 `json:"heavenly_value"` KarmaValue int32 `json:"karma_value"` ReputationScore int32 `json:"reputation_score"` LastOnlineAt string `json:"last_online_at"` CreatedAt string `json:"created_at"` } func createCharacter(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 createCharacterReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2001, "invalid payload", traceID) } // 校验参数 if req.Name == "" || len(req.Name) > 32 { return errResp(2002, "invalid name length", traceID) } if req.RaceID == "" { return errResp(2003, "race_id required", traceID) } if req.BirthWorldTier < 1 || req.BirthWorldTier > 5 { req.BirthWorldTier = 1 // 默认第1层 } // 检查种族是否可创建 var canCreate bool err := db.QueryRowContext(ctx, ` SELECT can_create FROM race_templates WHERE id = $1 `, req.RaceID).Scan(&canCreate) if err != nil { return errResp(2004, "invalid race", traceID) } if !canCreate { return errResp(2005, "race not creatable", traceID) } // 检查角色名是否已存在 var nameCount int32 err = db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM characters WHERE name = $1 `, req.Name).Scan(&nameCount) if err == nil && nameCount > 0 { return errResp(2006, "name already exists", traceID) } // 获取玩家ID var playerID string err = db.QueryRowContext(ctx, ` SELECT id FROM players WHERE nakama_user_id = $1 `, uid).Scan(&playerID) if err != nil { return errResp(4002, "player not found", traceID) } // 获取种族初始属性 var baseStatsJSON string err = db.QueryRowContext(ctx, ` SELECT COALESCE(description, '{}') FROM race_templates WHERE id = $1 `, req.RaceID).Scan(&baseStatsJSON) if err != nil { baseStatsJSON = `{"str":10,"vit":10,"wis":10,"agi":10,"spi":10,"luk":10}` } // 创建角色 var charID string err = db.QueryRowContext(ctx, ` INSERT INTO characters ( player_id, name, race_id, birth_race_id, birth_world_tier, world_tier, realm_tier, minor_realm, realm_status, level, exp, status, base_stats, battle_stats, san_current, san_max, crime_score, heavenly_value, karma_value, reputation_score, last_online_at, created_at, updated_at ) VALUES ( $1, $2, $3, $3, $4, $4, 1, 1, 'normal', 1, 0, 'active', $5, '{}', 100, 100, 0, 0, 0, 0, NOW(), NOW(), NOW() ) RETURNING id::text `, playerID, req.Name, req.RaceID, req.BirthWorldTier, baseStatsJSON).Scan(&charID) if err != nil { logger.Error("create character failed: %v", err) return errResp(9002, "internal error", traceID) } // 创建种族状态记录 _, err = db.ExecContext(ctx, ` INSERT INTO character_race_states (character_id, main_race_id, bloodline_data, race_talents, hidden_talents) VALUES ($1, $2, '{}', '{}', '{}') `, charID, req.RaceID) if err != nil { logger.Error("create race state failed: %v", err) } // 创建货币余额记录 _, err = db.ExecContext(ctx, ` INSERT INTO currency_balances (character_id, currency_code, amount, total_earned, total_spent) VALUES ($1, 'copper', 100, 100, 0) `, charID) if err != nil { logger.Error("create currency balance failed: %v", err) } logger.Info("CharacterService/CreateCharacter success: char_id=%s name=%s race=%s", charID, req.Name, req.RaceID) return okResp(characterData{ CharacterID: charID, PlayerID: playerID, Name: req.Name, RaceID: req.RaceID, WorldTier: req.BirthWorldTier, RealmTier: 1, MinorRealm: 1, Level: 1, Exp: 0, Status: "active", }, traceID) } func getCharacter(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 getCharacterReq if err := json.Unmarshal([]byte(payload), &req); err != nil { return errResp(2005, "invalid payload", traceID) } // 查询玩家ID var playerID string err := db.QueryRowContext(ctx, ` SELECT id FROM players WHERE nakama_user_id = $1 `, uid).Scan(&playerID) if err != nil { return errResp(4002, "player not found", traceID) } // 查询角色 var char characterData err = db.QueryRowContext(ctx, ` SELECT id, player_id, name, race_id, world_tier, realm_tier, minor_realm, realm_status, level, exp, status, base_stats, battle_stats, san_current, san_max, crime_score, heavenly_value, karma_value, reputation_score, last_online_at, created_at FROM characters WHERE id = $1 AND player_id = $2 `, req.CharacterID, playerID).Scan( &char.CharacterID, &char.PlayerID, &char.Name, &char.RaceID, &char.WorldTier, &char.RealmTier, &char.MinorRealm, &char.RealmStatus, &char.Level, &char.Exp, &char.Status, &char.BaseStats, &char.BattleStats, &char.SanCurrent, &char.SanMax, &char.CrimeScore, &char.HeavenlyValue, &char.KarmaValue, &char.ReputationScore, &char.LastOnlineAt, &char.CreatedAt, ) if err != nil { return errResp(2007, "character not found", traceID) } return okResp(char, traceID) }