494 lines
17 KiB
Go
494 lines
17 KiB
Go
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"`
|
||
}
|
||
|