package common import ( "encoding/json" "fmt" "os" "sync" "time" ) // LogInfo prints a timestamped info line (kept dependency-free). func LogInfo(format string, args ...any) { fmt.Printf("%s %s\n", time.Now().Format("2006-01-02 15:04:05"), fmt.Sprintf(format, args...)) } // --------------------------------------------------------------------------- // res.json (camelCase, produced by the AstralOS dump pipeline) // --------------------------------------------------------------------------- type Vector struct { X int32 `json:"x"` Y int32 `json:"y"` Z int32 `json:"z"` } type SceneMonsterInfo struct { Pos Vector `json:"pos"` Rot Vector `json:"rot"` GroupID uint32 `json:"groupId"` InstID uint32 `json:"instId"` MonsterID uint32 `json:"monsterId"` EventID uint32 `json:"eventId"` } type SceneNpcInfo struct { Pos Vector `json:"pos"` Rot Vector `json:"rot"` GroupID uint32 `json:"groupId"` InstID uint32 `json:"instId"` NpcID uint32 `json:"npcId"` } type ScenePropInfo struct { Pos Vector `json:"pos"` Rot Vector `json:"rot"` GroupID uint32 `json:"groupId"` InstID uint32 `json:"instId"` PropState uint32 `json:"propState"` PropID uint32 `json:"propId"` } type TeleportInfo struct { Pos Vector `json:"pos"` Rot Vector `json:"rot"` } type SceneData struct { Npcs []SceneNpcInfo `json:"npcs"` Props []ScenePropInfo `json:"props"` Monsters []SceneMonsterInfo `json:"monsters"` Teleports map[uint32]TeleportInfo `json:"teleports"` FinishedSubMissions []uint32 `json:"finishedSubMissions"` FinishedMainMissions []uint32 `json:"finishedMainMissions"` Chests []uint32 `json:"chests"` } type LevelOutputConfig struct { IsEnteredSceneInfo bool `json:"isEnteredSceneInfo"` Scenes map[uint32]SceneData `json:"scenes"` PlaneType uint32 `json:"planeType"` WorldID uint32 `json:"worldId"` Sections []uint32 `json:"sections"` SavedValues map[string]int32 `json:"savedValues"` } type AvatarConfig struct { WeaknessBuffID uint32 `json:"weaknessBuffId"` } type JsonConfig struct { // entryId -> "P_F" -> config LevelOutputConfigs map[uint32]map[string]*LevelOutputConfig `json:"levelOutputConfigs"` AvatarConfigs map[uint32]AvatarConfig `json:"avatarConfigs"` MapDefaultEntranceMap map[uint32]uint32 `json:"mapDefaultEntranceMap"` RelicAvatarRecommend map[uint32][]uint32 `json:"relicAvatarRecommend"` SkinIDs []uint32 `json:"skinIds"` } var ( gameResOnce sync.Once gameRes *JsonConfig gameResErr error teleportOnce sync.Once teleportDB map[uint32]map[uint32]TeleportInfo ) type MapEntranceEntry struct { ID uint32 `json:"ID"` PlaneID uint32 `json:"PlaneID"` FloorID uint32 `json:"FloorID"` StartGroupID uint32 `json:"StartGroupID"` StartAnchorID uint32 `json:"StartAnchorID"` } type MazePlaneEntry struct { PlaneID uint32 `json:"PlaneID"` WorldID uint32 `json:"WorldID"` StartFloorID uint32 `json:"StartFloorID"` FloorIDList []uint32 `json:"FloorIDList"` } type ResourceTeleport struct { TeleportID uint32 `json:"teleportId"` AnchorID uint32 `json:"anchorId"` GroupID uint32 `json:"groupId"` InstID uint32 `json:"instId"` Pos Vector `json:"pos"` Rot Vector `json:"rot"` } type ResourceSceneEntry struct { PlaneID uint32 `json:"planeID"` EntryID uint32 `json:"entryID"` Props []ScenePropInfo `json:"props"` Monsters []SceneMonsterInfo `json:"monsters"` Teleports []ResourceTeleport `json:"teleports"` } type ResourceResConfig struct { SceneConfig []ResourceSceneEntry `json:"scene_config"` } type AnchorItem struct { ID uint32 `json:"ID"` Pos Vector `json:"pos"` Rot Vector `json:"rot"` } type AnchorConfigEntry struct { EntryID uint32 `json:"entryID"` Anchor []AnchorItem `json:"anchor"` } type AnchorConfigFile struct { AnchorConfig []AnchorConfigEntry `json:"anchor_config"` } func readResourceFile(paths ...string) ([]byte, error) { for _, p := range paths { if raw, err := os.ReadFile(p); err == nil { return raw, nil } } return nil, os.ErrNotExist } // GameRes lazily parses res.json and merges supplementary configs from resources/ func GameRes() (*JsonConfig, error) { gameResOnce.Do(func() { gameRes = &JsonConfig{} raw, err := readResourceFile("res.json", "resources/res.json") if err != nil { gameResErr = err return } if err := json.Unmarshal(raw, gameRes); err != nil { gameResErr = fmt.Errorf("res.json is malformed: %w", err) return } if gameRes.LevelOutputConfigs == nil { gameRes.LevelOutputConfigs = map[uint32]map[string]*LevelOutputConfig{} } if gameRes.AvatarConfigs == nil { gameRes.AvatarConfigs = map[uint32]AvatarConfig{} } if gameRes.MapDefaultEntranceMap == nil { gameRes.MapDefaultEntranceMap = map[uint32]uint32{} } _ = Teleports() // ensure teleportDB is initialized loadSupplementaryResources(gameRes) }) return gameRes, gameResErr } func loadSupplementaryResources(res *JsonConfig) { entrancesByEntry := map[uint32]MapEntranceEntry{} if raw, err := readResourceFile("resources/MapEntrance.json", "MapEntrance.json"); err == nil { var list []MapEntranceEntry if json.Unmarshal(raw, &list) != nil { var m map[string][]MapEntranceEntry if json.Unmarshal(raw, &m) == nil { for _, entries := range m { list = append(list, entries...) } } } for _, e := range list { if e.FloorID != 0 && e.ID != 0 { if _, ok := res.MapDefaultEntranceMap[e.FloorID]; !ok { res.MapDefaultEntranceMap[e.FloorID] = e.ID } entrancesByEntry[e.ID] = e } } } planesByID := map[uint32]MazePlaneEntry{} if raw, err := readResourceFile("resources/MazePlane.json", "MazePlane.json"); err == nil { var list []MazePlaneEntry if json.Unmarshal(raw, &list) != nil { var m map[string][]MazePlaneEntry if json.Unmarshal(raw, &m) == nil { for _, entries := range m { list = append(list, entries...) } } } for _, p := range list { planesByID[p.PlaneID] = p if p.StartFloorID != 0 { if _, ok := res.MapDefaultEntranceMap[p.StartFloorID]; !ok { res.MapDefaultEntranceMap[p.StartFloorID] = p.PlaneID*100 + 1 } } } } if raw, err := readResourceFile("resources/res.json"); err == nil { var res2 ResourceResConfig if json.Unmarshal(raw, &res2) == nil && len(res2.SceneConfig) > 0 { for _, sc := range res2.SceneConfig { floorID := uint32(0) if ent, ok := entrancesByEntry[sc.EntryID]; ok && ent.FloorID != 0 { floorID = ent.FloorID } else if p, ok := planesByID[sc.PlaneID]; ok && p.StartFloorID != 0 { floorID = p.StartFloorID } else { floorID = sc.PlaneID*1000 + 1 } if _, ok := res.MapDefaultEntranceMap[floorID]; !ok { res.MapDefaultEntranceMap[floorID] = sc.EntryID } if teleportDB[floorID] == nil { teleportDB[floorID] = map[uint32]TeleportInfo{} } for _, t := range sc.Teleports { tid := t.TeleportID if tid == 0 { tid = t.AnchorID } if tid != 0 { teleportDB[floorID][tid] = TeleportInfo{Pos: t.Pos, Rot: t.Rot} } } if _, exists := res.LevelOutputConfigs[sc.EntryID]; !exists { groups := map[uint32]SceneData{} for _, p := range sc.Props { gid := p.GroupID if gid == 0 { gid = 1 } sd := groups[gid] if sd.Teleports == nil { sd.Teleports = map[uint32]TeleportInfo{} } sd.Props = append(sd.Props, p) groups[gid] = sd } for _, m := range sc.Monsters { gid := m.GroupID if gid == 0 { gid = 1 } sd := groups[gid] if sd.Teleports == nil { sd.Teleports = map[uint32]TeleportInfo{} } sd.Monsters = append(sd.Monsters, m) groups[gid] = sd } for _, t := range sc.Teleports { gid := t.GroupID if gid == 0 { gid = 1 } sd := groups[gid] if sd.Teleports == nil { sd.Teleports = map[uint32]TeleportInfo{} } tid := t.TeleportID if tid == 0 { tid = t.AnchorID } if tid != 0 { sd.Teleports[tid] = TeleportInfo{Pos: t.Pos, Rot: t.Rot} } groups[gid] = sd } name := fmt.Sprintf("P%d_F%d", sc.PlaneID, floorID) res.LevelOutputConfigs[sc.EntryID] = map[string]*LevelOutputConfig{ name: { IsEnteredSceneInfo: true, Scenes: groups, }, } } } } } if raw, err := readResourceFile("resources/Anchor.json", "Anchor.json"); err == nil { var aFile AnchorConfigFile if json.Unmarshal(raw, &aFile) == nil && len(aFile.AnchorConfig) > 0 { for _, item := range aFile.AnchorConfig { floorID := uint32(0) if ent, ok := entrancesByEntry[item.EntryID]; ok && ent.FloorID != 0 { floorID = ent.FloorID } else { floorID = item.EntryID*10 + 1 } if teleportDB[floorID] == nil { teleportDB[floorID] = map[uint32]TeleportInfo{} } for _, a := range item.Anchor { if a.ID != 0 { teleportDB[floorID][a.ID] = TeleportInfo{Pos: a.Pos, Rot: a.Rot} teleportDB[floorID][item.EntryID*100+a.ID] = TeleportInfo{Pos: a.Pos, Rot: a.Rot} } } if _, exists := res.LevelOutputConfigs[item.EntryID]; !exists { tps := map[uint32]TeleportInfo{} for _, a := range item.Anchor { if a.ID != 0 { tps[a.ID] = TeleportInfo{Pos: a.Pos, Rot: a.Rot} tps[item.EntryID*100+a.ID] = TeleportInfo{Pos: a.Pos, Rot: a.Rot} } } name := fmt.Sprintf("P%d_F%d", item.EntryID/100, floorID) res.LevelOutputConfigs[item.EntryID] = map[string]*LevelOutputConfig{ name: { IsEnteredSceneInfo: true, Scenes: map[uint32]SceneData{ 1: {Teleports: tps}, }, }, } } } } } } // Teleports lazily parses teleports.json ({floor_id: {teleport_id: info}}). func Teleports() map[uint32]map[uint32]TeleportInfo { teleportOnce.Do(func() { teleportDB = map[uint32]map[uint32]TeleportInfo{} raw, err := readResourceFile("teleports.json", "resources/teleports.json") if err == nil { _ = json.Unmarshal(raw, &teleportDB) } }) return teleportDB }