This repository has been archived on 2026-10-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files

493 lines
17 KiB
Go
Raw Permalink Normal View History

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 ("<raidId>_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"`
//
// 这条行能不能由服务端这条切人链路播出来。<br/>
// ⛑ AgentId 非空的行服务端补不出随从(照 RandomSwitchAnimation 的注释:
// "Generic server switching cannot satisfy authored companion-agent rows")⇒ 列得出来但播不了。
//
Playable bool `json:"playable"`
// 下拉里的显示名:#id 地点 · 动作 · timeline。
Label string `json:"label"`
}