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
Ananta-ps/internal/catalogs/client_config_repository.go

494 lines
17 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"`
}