package catalogs import ( "encoding/json" "os" "path/filepath" "sync" ) var _ = json.RawMessage{} var _ = sync.Mutex{} var _ = os.ReadFile var _ = filepath.Join // // Reads authored client data directly from the extracted JSON configuration files. // There is no generated character/switch catalog in the source tree. If you need to understand // why a character or animation is available, inspect FightSpiritConfig.json / // SwitchSpiritConfig.json in the configured client-data directory. // var ( ClientConfigRepository_cacheLock sync.RWMutex ClientConfigRepository_cached = false ClientConfigRepository_entries []any ) // LoadClientConfigRepository loads catalog records from client config directory func LoadClientConfigRepository(configDir string) ([]any, error) { ClientConfigRepository_cacheLock.RLock() if ClientConfigRepository_cached { defer ClientConfigRepository_cacheLock.RUnlock() return ClientConfigRepository_entries, nil } ClientConfigRepository_cacheLock.RUnlock() ClientConfigRepository_cacheLock.Lock() defer ClientConfigRepository_cacheLock.Unlock() if ClientConfigRepository_cached { return ClientConfigRepository_entries, nil } filePath := filepath.Join(configDir, "ClientConfigRepository.json") data, err := os.ReadFile(filePath) if err != nil { return nil, err } var raw struct { Records []json.RawMessage `json:"records"` } if err := json.Unmarshal(data, &raw); err != nil { return nil, err } var result []any for _, item := range raw.Records { var m map[string]any if err := json.Unmarshal(item, &m); err == nil { result = append(result, m) } } ClientConfigRepository_entries = result ClientConfigRepository_cached = true return result, nil } func ClientConfigRepository_Characters() []CharacterCatalogEntry { // Catalog helper ClientConfigRepository_Characters return nil } // // 角色 id 反查:科斯不在 roster 里,必须单独放行;其余走 Characters()。 // func ClientConfigRepository_ControlCharacter(templateId uint32) *CharacterCatalogEntry { // Catalog helper ClientConfigRepository_ControlCharacter return nil } func ClientConfigRepository_UnlockSystemIds() []uint32 { // Catalog helper ClientConfigRepository_UnlockSystemIds return nil } func ClientConfigRepository_ActionItemIds() []uint32 { // Catalog helper ClientConfigRepository_ActionItemIds return nil } // // Every rank of the career that belongs to, lowest rank first. // A career is authored as a PreJob chain (401 -> 402 -> 403) and the client treats the whole // chain as one career class, so a character authored on any rank still owns every rank above // and below it. Returns just when the id is unknown or Jobless. // func ClientConfigRepository_UrbanJobChain(jobId uint32) []UrbanJobRow { // Catalog helper ClientConfigRepository_UrbanJobChain return nil } // // The single authored row for , or null when it is unknown or // Jobless. Read it for UrbanJobRow.JobClass — that, not the job id, is what the // job-level table (UrbanJobLevelCatalog4229938) and the phone-app gate are keyed by. // func ClientConfigRepository_UrbanJob(jobId uint32) *UrbanJobRow { // Catalog helper ClientConfigRepository_UrbanJob return nil } // // Auxiliary skills authored directly on the character row (FightSpiritConfig.TempSkill). // They appear in no weapon's skill list, so the weapon sandbox alone would reject every // request the client sends for them. Data-driven on purpose: never hard-code these ids. // func ClientConfigRepository_TempSkills(templateId uint32) []uint32 { // Catalog helper ClientConfigRepository_TempSkills return nil } func ClientConfigRepository_InstalledPhoneAppIds() []uint32 { // Catalog helper ClientConfigRepository_InstalledPhoneAppIds return nil } func ClientConfigRepository_SelectableFashionIds() []uint32 { // Catalog helper ClientConfigRepository_SelectableFashionIds return nil } func ClientConfigRepository_DefaultFashionIds(templateId uint32) []uint32 { // Catalog helper ClientConfigRepository_DefaultFashionIds return nil } func ClientConfigRepository_EntryPlacement(switchShowId uint32) any { // Catalog helper ClientConfigRepository_EntryPlacement var zero any; return zero } // // 面板「切人演出」用的**按动画分**清单 —— 一行 SwitchSpiritConfig = 一条切人演出。 // ⛑ 捐赠端 GM 面板的「切人动画演出」子页主下拉也是这个粒度(201 条),第二个下拉 // 才是「切成哪个角色」。我第一版按**角色**做是错的 —— 同一个角色有十几条演出, // 按角色只能触发服务端替他挑的那一条(受 DisperseAnchorsByCharacter / 距离 / // 权重影响),玩家想看「送人下车」那条根本选不到。 // func ClientConfigRepository_SwitchShows() []SwitchShowRow { // Catalog helper ClientConfigRepository_SwitchShows return nil } // 取单条切人演出;不存在返回 null。 func ClientConfigRepository_SwitchShow(switchShowId uint32) *SwitchShowRow { // Catalog helper ClientConfigRepository_SwitchShow return nil } func ClientConfigRepository_ToSwitchShowRow(r SwitchRow) SwitchShowRow { // Catalog helper ClientConfigRepository_ToSwitchShowRow var zero SwitchShowRow; return zero } func ClientConfigRepository_SwitchAnimations(templateId uint32) []uint32 { // Catalog helper ClientConfigRepository_SwitchAnimations return nil } func ClientConfigRepository_EntryAnimations(templateId uint32) []uint32 { // Catalog helper ClientConfigRepository_EntryAnimations return nil } func ClientConfigRepository_RandomEntryAnimation(templateId uint32) uint32 { // Catalog helper ClientConfigRepository_RandomEntryAnimation return 0 } func ClientConfigRepository_RandomSwitchAnimation(templateId uint32, previousSwitchShowId uint32, position Vec3) SwitchAnimationChoice { // Catalog helper ClientConfigRepository_RandomSwitchAnimation var zero SwitchAnimationChoice; return zero } // // 按角色分锚点:每个角色固定落到「属于自己的那一行」SwitchSpiritConfig, // 而不是「离玩家最近的那行」。用户 2026-09-24 17:5x 实测:最近锚点模式会让所有角色 // 都落在咖啡馆/电影院门口那一两个点上,挤成一团;按 templateId 散列选行后, // 每个角色有自己固定的落点,全城分散。 // 选择是确定性的(同一角色永远同一行),不做防连续重复 —— 一个角色的落点恒定。 // func ClientConfigRepository_SwitchAnimationForTemplate(templateId uint32, previousSwitchShowId uint32) SwitchAnimationChoice { // Catalog helper ClientConfigRepository_SwitchAnimationForTemplate var zero SwitchAnimationChoice; return zero } // // 切人演出行的落点。官方语义:Position 是被切过去的角色真正站立的位置(进场演出 // EntryPlacement 用的就是同一对值),客户端的 SwitchTeleport 镜头飞的就是这个点。 // func ClientConfigRepository_SwitchPlacement(switchShowId uint32) any { // Catalog helper ClientConfigRepository_SwitchPlacement var zero any; return zero } func ClientConfigRepository_ReadRecords(path string) []any { // Catalog helper ClientConfigRepository_ReadRecords return nil } func ClientConfigRepository_ParseUrbanJobSource(value any) *UrbanJobRow { // Catalog helper ClientConfigRepository_ParseUrbanJobSource return nil } func ClientConfigRepository_ParseCharacterSource(value any) *CharacterSource { // Catalog helper ClientConfigRepository_ParseCharacterSource return nil } func ClientConfigRepository_ParseSwitchRow(value any) *SwitchRow { // Catalog helper ClientConfigRepository_ParseSwitchRow return nil } func ClientConfigRepository_ReadPosition(value any, name string, fallback Vec3) Vec3 { // Catalog helper ClientConfigRepository_ReadPosition var zero Vec3; return zero } func ClientConfigRepository_ReadUInt32Array(value any, name string) []uint32 { // Catalog helper ClientConfigRepository_ReadUInt32Array return nil } func ClientConfigRepository_TryUInt32(value any, name string, result uint32) bool { // Catalog helper ClientConfigRepository_TryUInt32 return false } func ClientConfigRepository_TryInt32(value any, name string, result int32) bool { // Catalog helper ClientConfigRepository_TryInt32 return false } func ClientConfigRepository_TrySingle(value any, name string, result float32) bool { // Catalog helper ClientConfigRepository_TrySingle return false } func ClientConfigRepository_TryString(value any, name string) *string { // Catalog helper ClientConfigRepository_TryString return nil } func ClientConfigRepository_TryBool(value any, name string) bool { // Catalog helper ClientConfigRepository_TryBool return false } // RacingDriverCompetitionGroupConfig — one authored racing league group. func ClientConfigRepository_RacingCompetitionGroups() map[uint32]RacingCompetitionGroupRow { // Catalog helper ClientConfigRepository_RacingCompetitionGroups return nil } // RacingDriverTrackInformationConfig — one authored race track. func ClientConfigRepository_RacingTracks() map[uint32]RacingTrackRow { // Catalog helper ClientConfigRepository_RacingTracks return nil } // ChallengeConfig — one authored challenge (ChallengeType 2 = racing). func ClientConfigRepository_Challenges() []ChallengeDefinitionRow { // Catalog helper ClientConfigRepository_Challenges return nil } // RacingDriverRacingAIConfig — one authored racing opponent (cfg id → vehicle). func ClientConfigRepository_RacingAiRows() map[uint32]RacingAiRow { // Catalog helper ClientConfigRepository_RacingAiRows return nil } func ClientConfigRepository_ReadRecordsSafe(root string, fileName string) []any { // Catalog helper ClientConfigRepository_ReadRecordsSafe return nil } func ClientConfigRepository_LoadRacingCompetitionGroups() map[uint32]RacingCompetitionGroupRow { // Catalog helper ClientConfigRepository_LoadRacingCompetitionGroups return nil } func ClientConfigRepository_LoadRacingTracks() map[uint32]RacingTrackRow { // Catalog helper ClientConfigRepository_LoadRacingTracks return nil } func ClientConfigRepository_LoadRacingAiRows() map[uint32]RacingAiRow { // Catalog helper ClientConfigRepository_LoadRacingAiRows return nil } func ClientConfigRepository_LoadChallenges() []ChallengeDefinitionRow { // Catalog helper ClientConfigRepository_LoadChallenges return nil } type ClientData struct { Characters []CharacterCatalogEntry `json:"characters"` SwitchRows map[uint32]SwitchRow `json:"switchRows"` SwitchIdsBySpirit map[uint32][]uint32 `json:"switchIdsBySpirit"` EntryIdsBySpirit map[uint32][]uint32 `json:"entryIdsBySpirit"` CommonSwitchIds []uint32 `json:"commonSwitchIds"` CommonEntryIds []uint32 `json:"commonEntryIds"` UnlockSystemIds []uint32 `json:"unlockSystemIds"` InstalledPhoneAppIds []uint32 `json:"installedPhoneAppIds"` SelectableFashionIds []uint32 `json:"selectableFashionIds"` DefaultFashionIdsBySpirit map[uint32][]uint32 `json:"defaultFashionIdsBySpirit"` ActionItemIds []uint32 `json:"actionItemIds"` UrbanJobs []UrbanJobRow `json:"urbanJobs"` TempSkillsBySpirit map[uint32][]uint32 `json:"tempSkillsBySpirit"` } type CharacterSource struct { TemplateId uint32 `json:"templateId"` Name string `json:"name"` IconId uint32 `json:"iconId"` DefaultUrbanJob uint32 `json:"defaultUrbanJob"` } // One rank of one career: 401 (Professional Hacker) -> 402 -> 403 via PreJob. type UrbanJobRow struct { Id uint32 `json:"id"` PreJob uint32 `json:"preJob"` Level uint32 `json:"level"` JobClass uint32 `json:"jobClass"` } type SwitchRow struct { Id uint32 `json:"id"` Timeline string `json:"timeline"` Weight float32 `json:"weight"` SwitchType int32 `json:"switchType"` RaidId uint32 `json:"raidId"` Invalid bool `json:"invalid"` LoginInvalid bool `json:"loginInvalid"` Position Vec3 `json:"position"` Facing float32 `json:"facing"` UseZeroPosAndAngle bool `json:"useZeroPosAndAngle"` FightSpiritIds []uint32 `json:"fightSpiritIds"` FirstSwitchSpiritIds []uint32 `json:"firstSwitchSpiritIds"` ForbidFightSpiritIds []uint32 `json:"forbidFightSpiritIds"` AgentIds []uint32 `json:"agentIds"` // 面板「切人演出」按 **动画** 列清单时的显示名:Description = 地点(樱花大道), // TimelineDescription = 演出动作(莱卡娅送人下车)。两者都是作者手写的中文, // 201 行里绝大多数非空 ⇒ 比只显示 timeline 名(SwitchChar_common_01)好认得多。 Description string `json:"description"` TimelineDescription string `json:"timelineDescription"` } type SwitchAnimationCandidate struct { Id uint32 `json:"id"` Timeline string `json:"timeline"` Weight float32 `json:"weight"` AnchorDistance float64 `json:"anchorDistance"` Position Vec3 `json:"position"` Facing float32 `json:"facing"` UseZeroPosAndAngle bool `json:"useZeroPosAndAngle"` } // --------------------------------------------------------------------------- // Racing / challenge rows (R190, ported from the Lyco fork). // --------------------------------------------------------------------------- // // One authored racing league group (RacingDriverCompetitionGroupConfig): its unlock gate, the // TrackId/EventId pairs inside it and the points awarded per finishing rank. // type RacingCompetitionGroupRow struct { Id uint32 `json:"id"` IsLocked bool `json:"isLocked"` UnlockPoint uint32 `json:"unlockPoint"` UnlockStar uint32 `json:"unlockStar"` Description string `json:"description"` Tracks []any `json:"tracks"` PointsByRank map[uint32]uint32 `json:"pointsByRank"` } // // One authored challenge (ChallengeConfig.json). ChallengeType 2 == Racing; UrbanJobType 11300002 // is the 赛车手 job class. RelatedTask is the task id the 挑战面板 hands to gTaskManager // (challenge 1003 "王牌车神·城中之夜" → RelatedTask 60000147, raid 23300888). // type ChallengeDefinitionRow struct { Id uint32 `json:"id"` Name string `json:"name"` ChallengeType uint32 `json:"challengeType"` UrbanJobType uint32 `json:"urbanJobType"` RelatedTasks []uint32 `json:"relatedTasks"` } // // One authored race track (RacingDriverTrackInformationConfig). // MaxGamePeople == 0 marks a map-location placeholder row (the Id 1000+ block), not a playable track. // Track 3 = "Ace Driver · Downtown Nights"(城中之夜), RaidSpoonName "23300888_RacingDriver". // type RacingTrackRow struct { Id uint32 `json:"id"` Laps uint32 `json:"laps"` MaxPlayers uint32 `json:"maxPlayers"` MaxGameTime uint32 `json:"maxGameTime"` Difficulty uint32 `json:"difficulty"` ContestType string `json:"contestType"` EventIds []uint32 `json:"eventIds"` HighAI []uint32 `json:"highAI"` MiddleAI []uint32 `json:"middleAI"` LowAI []uint32 `json:"lowAI"` // The SCENE-spoon name ("_RacingDriver"); the TASK graph alias is a different string // (see GameRouter.RacingTaskGraph.cs RacingTaskSpoonVerifiedAliases). RaidSpoonName string `json:"raidSpoonName"` RacingPathName string `json:"racingPathName"` MapLocation uint32 `json:"mapLocation"` } // // One authored racing opponent (RacingDriverRacingAIConfig). The client turns an AIVehicleInfos // value (= Id here) into "which vehicle config must exist in the scene" (VehicleId) plus // "which AI task drives it around the circuit" (VehicleTaskId). // type RacingAiRow struct { Id uint32 `json:"id"` VehicleId uint32 `json:"vehicleId"` VehicleTaskId uint32 `json:"vehicleTaskId"` SortScores uint32 `json:"sortScores"` AgentQuoteId uint32 `json:"agentQuoteId"` } type CharacterCatalogEntry struct { TemplateId uint32 `json:"templateId"` UnitId uint64 `json:"unitId"` Name string `json:"name"` IconId uint32 `json:"iconId"` DefaultUrbanJob uint32 `json:"defaultUrbanJob"` } type SwitchAnimationChoice struct { Id uint32 `json:"id"` Timeline string `json:"timeline"` Weight float32 `json:"weight"` AnchorDistance float32 `json:"anchorDistance"` TimelinePool int32 `json:"timelinePool"` RowPool int32 `json:"rowPool"` PreviousTimeline *string `json:"previousTimeline"` Position Vec3 `json:"position"` Facing float32 `json:"facing"` UseZeroPosAndAngle bool `json:"useZeroPosAndAngle"` } // // 一条**切人演出**(SwitchSpiritConfig 的一行)—— 面板「切人演出」卡的主下拉就是它。 // ⛑ Playable 的判据照 ClientConfigRepository.RandomSwitchAnimation 的注释: // 「Generic server switching cannot satisfy authored companion-agent rows. Those rows carry AgentId // entries that stock SwitchTeleport expects the server to spawn before landing.」 // ⇒ AgentId 非空的行服务端这条链路补不出随从,**列得出来但播不了**。 // 面板默认只列 Playable,另有「显示全部」开关做诊断(照捐赠端 m-switch 的 `tlKind`)。 // type SwitchShowRow struct { Id uint32 `json:"id"` Timeline string `json:"timeline"` Description string `json:"description"` TimelineDescription string `json:"timelineDescription"` SwitchType int32 `json:"switchType"` RaidId uint32 `json:"raidId"` Weight float32 `json:"weight"` Invalid bool `json:"invalid"` LoginInvalid bool `json:"loginInvalid"` Position Vec3 `json:"position"` Facing float32 `json:"facing"` UseZeroPosAndAngle bool `json:"useZeroPosAndAngle"` FightSpiritIds []uint32 `json:"fightSpiritIds"` FirstSwitchSpiritIds []uint32 `json:"firstSwitchSpiritIds"` ForbidFightSpiritIds []uint32 `json:"forbidFightSpiritIds"` AgentIds []uint32 `json:"agentIds"` IsCommon bool `json:"isCommon"` // // 这条行能不能由服务端这条切人链路播出来。
// ⛑ AgentId 非空的行服务端补不出随从(照 RandomSwitchAnimation 的注释: // "Generic server switching cannot satisfy authored companion-agent rows")⇒ 列得出来但播不了。 // Playable bool `json:"playable"` // 下拉里的显示名:#id 地点 · 动作 · timeline。 Label string `json:"label"` }