chore: checkpoint stable battle and eidolon progression
This commit is contained in:
1 parent
365e66ffe0
commit
a681affdb3
23 files changed
+34511
-11310
No files matched your search
+28
-29
@@ -8,7 +8,6 @@ package common
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
@@ -149,6 +148,23 @@ type SubAffix struct {
|
||||
Step uint32 `json:"step"`
|
||||
}
|
||||
|
||||
func (s *SubAffix) UnmarshalJSON(data []byte) error {
|
||||
type Alias SubAffix
|
||||
aux := &struct {
|
||||
Cnt *uint32 `json:"cnt"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(s),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
if aux.Cnt != nil && s.Count == 0 {
|
||||
s.Count = *aux.Cnt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Relic struct {
|
||||
Level uint32 `json:"level"`
|
||||
RelicID uint32 `json:"relic_id"`
|
||||
@@ -352,6 +368,8 @@ func DefaultPersistent() Persistent {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type FreesrData struct {
|
||||
persistentBaseline map[string]any
|
||||
|
||||
Lightcones []Lightcone `json:"lightcones"`
|
||||
Relics []Relic `json:"relics"`
|
||||
Avatars map[uint32]*AvatarJson `json:"avatars"`
|
||||
@@ -372,7 +390,7 @@ type FreesrData struct {
|
||||
// LoadFreesrData reads freesr-data.json + persistent and merges them,
|
||||
// mirroring FreesrData::load() from the Rust crate.
|
||||
func LoadFreesrData() (*FreesrData, error) {
|
||||
raw, err := os.ReadFile("freesr-data.json")
|
||||
raw, err := readResourceFile("freesr-data.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read freesr-data.json: %w", err)
|
||||
}
|
||||
@@ -390,7 +408,7 @@ func LoadFreesrData() (*FreesrData, error) {
|
||||
}
|
||||
|
||||
// persistent is optional
|
||||
pers, err := os.ReadFile("persistent")
|
||||
pers, err := readResourceFile("persistent")
|
||||
if err == nil {
|
||||
var p Persistent
|
||||
if err := json.Unmarshal(pers, &p); err == nil {
|
||||
@@ -440,6 +458,10 @@ func LoadFreesrData() (*FreesrData, error) {
|
||||
data.MarchType = MultiPathMarchHunt
|
||||
}
|
||||
|
||||
data.persistentBaseline, err = jsonObject(data.persistentSnapshot())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
@@ -453,7 +475,7 @@ func (f *FreesrData) SortedLineupKeys() []uint32 {
|
||||
return keys
|
||||
}
|
||||
|
||||
// VerifyLineup pads the lineup to 4 slots, defaulting to {0:8001}, then saves.
|
||||
// VerifyLineup normalizes slots in memory. Reading a save must not rewrite it.
|
||||
func (f *FreesrData) VerifyLineup() {
|
||||
if len(f.Lineups) == 0 {
|
||||
f.Lineups = map[uint32]uint32{0: 8001, 1: 0, 2: 0, 3: 0}
|
||||
@@ -464,35 +486,12 @@ func (f *FreesrData) VerifyLineup() {
|
||||
}
|
||||
}
|
||||
}
|
||||
f.SavePersistent()
|
||||
}
|
||||
|
||||
// SavePersistent writes the persistent subset back to the "persistent" file.
|
||||
func (f *FreesrData) SavePersistent() {
|
||||
exUnlocked := make(map[uint32]bool)
|
||||
for aid, av := range f.Avatars {
|
||||
if av.ExRankUnlocked {
|
||||
exUnlocked[aid] = true
|
||||
}
|
||||
}
|
||||
p := Persistent{
|
||||
Lineups: f.Lineups,
|
||||
Position: f.Position,
|
||||
Scene: f.Scene,
|
||||
MainCharacter: f.MainCharacter,
|
||||
MarchType: f.MarchType,
|
||||
EnableSWGlobal: f.EnableSWGlobal,
|
||||
EnableCastoriceGlobal: f.EnableCastoriceGlobal,
|
||||
EnableAhaGlobal: f.EnableAhaGlobal,
|
||||
DressedSkins: f.DressedSkins,
|
||||
ExRanksUnlocked: exUnlocked,
|
||||
}
|
||||
out, err := json.MarshalIndent(p, "", " ")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := os.WriteFile("persistent", out, 0o644); err == nil {
|
||||
LogInfo("persistent saved")
|
||||
if err := f.SavePersistentChecked(); err != nil {
|
||||
LogInfo("[SAVE ERROR] %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
//go:build linux || darwin || freebsd || openbsd || netbsd || dragonfly
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func lockPersistentFile() (func(), error) {
|
||||
file, err := os.OpenFile("persistent.lock", os.O_CREATE|os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX); err != nil {
|
||||
file.Close()
|
||||
return nil, err
|
||||
}
|
||||
return func() { syscall.Flock(int(file.Fd()), syscall.LOCK_UN); file.Close() }, nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//go:build windows
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// LockFileEx's exclusive byte-range lock is released by Windows if either
|
||||
// process exits. The shared lock file coordinates SDK and game server saves.
|
||||
func lockPersistentFile() (func(), error) {
|
||||
file, err := os.OpenFile("persistent.lock", os.O_CREATE|os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dll := syscall.NewLazyDLL("kernel32.dll")
|
||||
lock := dll.NewProc("LockFileEx")
|
||||
unlock := dll.NewProc("UnlockFileEx")
|
||||
overlapped := new(syscall.Overlapped)
|
||||
const lockfileExclusiveLock = 2 // Win32 LOCKFILE_EXCLUSIVE_LOCK.
|
||||
ok, _, callErr := lock.Call(file.Fd(), lockfileExclusiveLock, 0, 1, 0, uintptr(unsafe.Pointer(overlapped)))
|
||||
if ok == 0 {
|
||||
file.Close()
|
||||
return nil, fmt.Errorf("lock persistent: %w", callErr)
|
||||
}
|
||||
return func() { unlock.Call(file.Fd(), 0, 1, 0, uintptr(unsafe.Pointer(overlapped))); file.Close() }, nil
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var persistentWriteMu sync.Mutex
|
||||
|
||||
func ReadPersistent() (*Persistent, error) {
|
||||
raw, err := os.ReadFile("persistent")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var p Persistent
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
return nil, fmt.Errorf("parse persistent: %w", err)
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func jsonObject(value any) (map[string]any, error) {
|
||||
raw, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var object map[string]any
|
||||
err = json.Unmarshal(raw, &object)
|
||||
return object, err
|
||||
}
|
||||
|
||||
func (f *FreesrData) persistentSnapshot() Persistent {
|
||||
ex := make(map[uint32]bool)
|
||||
for id := range AvatarExRankMap {
|
||||
if avatar := f.Avatars[id]; avatar != nil {
|
||||
ex[id] = avatar.ExRankUnlocked
|
||||
}
|
||||
}
|
||||
return Persistent{Lineups: f.Lineups, Position: f.Position, Scene: f.Scene,
|
||||
MainCharacter: f.MainCharacter, MarchType: f.MarchType,
|
||||
EnableSWGlobal: f.EnableSWGlobal, EnableCastoriceGlobal: f.EnableCastoriceGlobal,
|
||||
EnableAhaGlobal: f.EnableAhaGlobal, DressedSkins: f.DressedSkins, ExRanksUnlocked: ex}
|
||||
}
|
||||
|
||||
// Apply only locally changed fields. A stale game session saving its position
|
||||
// must not undo an EX switch written by the SDK process. Nested maps are merged
|
||||
// per key so different avatars' switches do not overwrite one another.
|
||||
func mergePersistentChanges(existing, baseline, next map[string]any) {
|
||||
for key, value := range next {
|
||||
old, known := baseline[key]
|
||||
if known && reflect.DeepEqual(old, value) {
|
||||
continue
|
||||
}
|
||||
newMap, newOK := value.(map[string]any)
|
||||
oldMap, oldOK := old.(map[string]any)
|
||||
if newOK && oldOK {
|
||||
dest, ok := existing[key].(map[string]any)
|
||||
if !ok {
|
||||
dest = make(map[string]any)
|
||||
}
|
||||
mergePersistentChanges(dest, oldMap, newMap)
|
||||
for removed := range oldMap {
|
||||
if _, present := newMap[removed]; !present {
|
||||
delete(dest, removed)
|
||||
}
|
||||
}
|
||||
existing[key] = dest
|
||||
} else {
|
||||
existing[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSONAtomic(path string, value any) error {
|
||||
raw, err := json.MarshalIndent(value, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.CreateTemp(".", ".gac-save-*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := file.Name()
|
||||
defer os.Remove(name)
|
||||
if _, err := file.Write(raw); err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(name, path)
|
||||
}
|
||||
|
||||
func (f *FreesrData) SavePersistentChecked() error {
|
||||
persistentWriteMu.Lock()
|
||||
defer persistentWriteMu.Unlock()
|
||||
unlock, err := lockPersistentFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
next, err := jsonObject(f.persistentSnapshot())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
existing := make(map[string]any)
|
||||
if raw, err := os.ReadFile("persistent"); err == nil {
|
||||
if err := json.Unmarshal(raw, &existing); err != nil {
|
||||
return fmt.Errorf("refuse to overwrite invalid persistent: %w", err)
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
mergePersistentChanges(existing, f.persistentBaseline, next)
|
||||
if err := writeJSONAtomic("persistent", existing); err != nil {
|
||||
return err
|
||||
}
|
||||
// Track our own observed snapshot, not external changes we have not loaded.
|
||||
f.persistentBaseline = next
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshExRanks imports persisted switches without replacing inventory,
|
||||
// lineup, scene or the configured battle. Call under the session state lock.
|
||||
func (f *FreesrData) RefreshExRanks(p *Persistent) []uint32 {
|
||||
var changed []uint32
|
||||
for id := range AvatarExRankMap {
|
||||
avatar := f.Avatars[id]
|
||||
if avatar == nil {
|
||||
continue
|
||||
}
|
||||
unlocked := p.ExRanksUnlocked[id]
|
||||
if avatar.ExRankUnlocked != unlocked {
|
||||
avatar.ExRankUnlocked = unlocked
|
||||
changed = append(changed, id)
|
||||
}
|
||||
}
|
||||
if current, err := jsonObject(f.persistentSnapshot()); err == nil {
|
||||
if f.persistentBaseline == nil {
|
||||
f.persistentBaseline = make(map[string]any)
|
||||
}
|
||||
f.persistentBaseline["ex_ranks_unlocked"] = current["ex_ranks_unlocked"]
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
// Patch only the selected avatar's rank in the latest import. Unknown FreeSR
|
||||
// fields and all inventory IDs/affixes retain their original JSON values.
|
||||
func SaveAvatarRank(id, rank uint32) error {
|
||||
persistentWriteMu.Lock()
|
||||
defer persistentWriteMu.Unlock()
|
||||
unlock, err := lockPersistentFile()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer unlock()
|
||||
raw, err := os.ReadFile("freesr-data.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var root map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &root); err != nil {
|
||||
return err
|
||||
}
|
||||
var avatars map[uint32]json.RawMessage
|
||||
if err := json.Unmarshal(root["avatars"], &avatars); err != nil {
|
||||
return err
|
||||
}
|
||||
var avatar map[string]json.RawMessage
|
||||
if err := json.Unmarshal(avatars[id], &avatar); err != nil {
|
||||
return err
|
||||
}
|
||||
var data map[string]json.RawMessage
|
||||
if err := json.Unmarshal(avatar["data"], &data); err != nil {
|
||||
return err
|
||||
}
|
||||
data["rank"], err = json.Marshal(rank)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
avatar["data"], err = json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
avatars[id], err = json.Marshal(avatar)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
root["avatars"], err = json.Marshal(avatars)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeJSONAtomic("freesr-data.json", root)
|
||||
}
|
||||
@@ -162,11 +162,36 @@ func (a *AvatarJson) ToAvatarPathDataProto(lightcone *Lightcone, relics []*Relic
|
||||
out.EquipRelicList = append(out.EquipRelicList, r.ToEquipRelic())
|
||||
}
|
||||
|
||||
for anchorType, level := range a.Data.SkillsByAnchorType {
|
||||
out.AvatarPathSkillTree = append(out.AvatarPathSkillTree, &goproto.AvatarPathSkillTree{
|
||||
PointId: anchorType,
|
||||
Level: level,
|
||||
})
|
||||
if len(a.Data.Skills) > 0 {
|
||||
for pointID, level := range a.Data.Skills {
|
||||
out.AvatarPathSkillTree = append(out.AvatarPathSkillTree, &goproto.AvatarPathSkillTree{
|
||||
PointId: pointID,
|
||||
Level: level,
|
||||
})
|
||||
}
|
||||
} else if len(a.Data.SkillsByAnchorType) > 0 {
|
||||
res, _ := GameRes()
|
||||
for anchorType, level := range a.Data.SkillsByAnchorType {
|
||||
pointID := anchorType
|
||||
if res != nil && res.AvatarSkillTreePoints != nil && res.AvatarSkillTreePoints[a.AvatarID] != nil {
|
||||
if pid, ok := res.AvatarSkillTreePoints[a.AvatarID][anchorType]; ok && pid != 0 {
|
||||
pointID = pid
|
||||
}
|
||||
}
|
||||
out.AvatarPathSkillTree = append(out.AvatarPathSkillTree, &goproto.AvatarPathSkillTree{
|
||||
PointId: pointID,
|
||||
Level: level,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if res, err := GameRes(); err == nil && res.AvatarSkillTreePoints != nil && res.AvatarSkillTreePoints[a.AvatarID] != nil {
|
||||
for _, pid := range res.AvatarSkillTreePoints[a.AvatarID] {
|
||||
out.AvatarPathSkillTree = append(out.AvatarPathSkillTree, &goproto.AvatarPathSkillTree{
|
||||
PointId: pid,
|
||||
Level: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// deterministic order (Rust iterated the HashMap; order is irrelevant to the
|
||||
// client, but stable output keeps packets reproducible)
|
||||
@@ -211,8 +236,27 @@ func (a *AvatarJson) ToBattleAvatarProto(index uint32, lightcone *Lightcone, rel
|
||||
},
|
||||
}
|
||||
|
||||
for pointID, level := range a.Data.Skills {
|
||||
ba.SkilltreeList = append(ba.SkilltreeList, &goproto.AvatarSkillTree{PointId: pointID, Level: level})
|
||||
if len(a.Data.Skills) > 0 {
|
||||
for pointID, level := range a.Data.Skills {
|
||||
ba.SkilltreeList = append(ba.SkilltreeList, &goproto.AvatarSkillTree{PointId: pointID, Level: level})
|
||||
}
|
||||
} else if len(a.Data.SkillsByAnchorType) > 0 {
|
||||
res, _ := GameRes()
|
||||
for anchorType, level := range a.Data.SkillsByAnchorType {
|
||||
pointID := anchorType
|
||||
if res != nil && res.AvatarSkillTreePoints != nil && res.AvatarSkillTreePoints[a.AvatarID] != nil {
|
||||
if pid, ok := res.AvatarSkillTreePoints[a.AvatarID][anchorType]; ok && pid != 0 {
|
||||
pointID = pid
|
||||
}
|
||||
}
|
||||
ba.SkilltreeList = append(ba.SkilltreeList, &goproto.AvatarSkillTree{PointId: pointID, Level: level})
|
||||
}
|
||||
} else {
|
||||
if res, err := GameRes(); err == nil && res.AvatarSkillTreePoints != nil && res.AvatarSkillTreePoints[a.AvatarID] != nil {
|
||||
for _, pid := range res.AvatarSkillTreePoints[a.AvatarID] {
|
||||
ba.SkilltreeList = append(ba.SkilltreeList, &goproto.AvatarSkillTree{PointId: pid, Level: 1})
|
||||
}
|
||||
}
|
||||
}
|
||||
sortAvatarSkillTree(ba.SkilltreeList)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
goproto "gac-hkrpg-go/protocol/proto"
|
||||
@@ -33,3 +34,64 @@ func TestStandaloneBattleRelicWire(t *testing.T) {
|
||||
t.Fatal("inventory and equipped relic references must retain their IDs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAvatarPathSkillTreePointIds(t *testing.T) {
|
||||
// 1. avatar with explicit Skills map uses real PointIDs directly
|
||||
av := AvatarJson{
|
||||
AvatarID: 1213,
|
||||
Data: AvatarData{
|
||||
Rank: 0,
|
||||
Skills: map[uint32]uint32{
|
||||
1213001: 6,
|
||||
1213002: 10,
|
||||
},
|
||||
SkillsByAnchorType: map[uint32]uint32{
|
||||
1: 6,
|
||||
2: 10,
|
||||
},
|
||||
},
|
||||
}
|
||||
proto := av.ToAvatarPathDataProto(nil, nil, 0)
|
||||
if len(proto.AvatarPathSkillTree) != 2 {
|
||||
t.Fatalf("expected 2 skill tree nodes, got %d", len(proto.AvatarPathSkillTree))
|
||||
}
|
||||
if proto.AvatarPathSkillTree[0].PointId != 1213001 || proto.AvatarPathSkillTree[1].PointId != 1213002 {
|
||||
t.Fatalf("point ids must be real config point IDs, got %v", proto.AvatarPathSkillTree)
|
||||
}
|
||||
|
||||
// 2. avatar with only SkillsByAnchorType resolves from AvatarSkillTreeConfig.json
|
||||
avAnchorOnly := AvatarJson{
|
||||
AvatarID: 1001,
|
||||
Data: AvatarData{
|
||||
Rank: 0,
|
||||
SkillsByAnchorType: map[uint32]uint32{
|
||||
1: 6,
|
||||
},
|
||||
},
|
||||
}
|
||||
protoAnchor := avAnchorOnly.ToAvatarPathDataProto(nil, nil, 0)
|
||||
if len(protoAnchor.AvatarPathSkillTree) != 1 {
|
||||
t.Fatalf("expected 1 skill tree node, got %d", len(protoAnchor.AvatarPathSkillTree))
|
||||
}
|
||||
if protoAnchor.AvatarPathSkillTree[0].PointId != 1001001 {
|
||||
t.Fatalf("expected resolved point ID 1001001 for March Point01, got %d", protoAnchor.AvatarPathSkillTree[0].PointId)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubAffixUnmarshal(t *testing.T) {
|
||||
var sub1 SubAffix
|
||||
if err := json.Unmarshal([]byte(`{"sub_affix_id": 7, "cnt": 4, "step": 2}`), &sub1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sub1.Count != 4 || sub1.SubAffixID != 7 || sub1.Step != 2 {
|
||||
t.Fatalf("cnt alias failed: %+v", sub1)
|
||||
}
|
||||
|
||||
var sub2 SubAffix
|
||||
if err := json.Unmarshal([]byte(`{"sub_affix_id": 8, "count": 3, "step": 1}`), &sub2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if sub2.Count != 3 || sub2.SubAffixID != 8 || sub2.Step != 1 {
|
||||
t.Fatalf("count parsing failed: %+v", sub2)
|
||||
}
|
||||
}
|
||||
+44
-3
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -76,6 +77,7 @@ type LevelOutputConfig struct {
|
||||
type AvatarConfig struct {
|
||||
WeaknessBuffID uint32 `json:"weaknessBuffId"`
|
||||
DamageType string `json:"damageType"`
|
||||
MaxRank uint32 `json:"maxRank"`
|
||||
}
|
||||
|
||||
type JsonConfig struct {
|
||||
@@ -85,6 +87,7 @@ type JsonConfig struct {
|
||||
MapDefaultEntranceMap map[uint32]uint32 `json:"mapDefaultEntranceMap"`
|
||||
RelicAvatarRecommend map[uint32][]uint32 `json:"relicAvatarRecommend"`
|
||||
SkinIDs []uint32 `json:"skinIds"`
|
||||
AvatarSkillTreePoints map[uint32]map[uint32]uint32 `json:"-"`
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -146,15 +149,23 @@ type AnchorConfigFile struct {
|
||||
AnchorConfig []AnchorConfigEntry `json:"anchor_config"`
|
||||
}
|
||||
|
||||
func readResourceFile(paths ...string) ([]byte, error) {
|
||||
// ReadResourceFile reads a resource file checking current directory and parent directories.
|
||||
func ReadResourceFile(paths ...string) ([]byte, error) {
|
||||
for _, p := range paths {
|
||||
if raw, err := os.ReadFile(p); err == nil {
|
||||
return raw, nil
|
||||
for _, prefix := range []string{"", "..", "../..", "../../.."} {
|
||||
target := filepath.Join(prefix, p)
|
||||
if raw, err := os.ReadFile(target); err == nil {
|
||||
return raw, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func readResourceFile(paths ...string) ([]byte, error) {
|
||||
return ReadResourceFile(paths...)
|
||||
}
|
||||
|
||||
// GameRes lazily parses res.json and merges supplementary configs from resources/
|
||||
func GameRes() (*JsonConfig, error) {
|
||||
gameResOnce.Do(func() {
|
||||
@@ -186,6 +197,34 @@ func GameRes() (*JsonConfig, error) {
|
||||
}
|
||||
|
||||
func loadSupplementaryResources(res *JsonConfig) {
|
||||
// AvatarSkillTreeConfig: maps AvatarID and AnchorType (e.g. Point01 -> 1) to real PointID
|
||||
if raw, err := readResourceFile("resources/AvatarSkillTreeConfig.json", "AvatarSkillTreeConfig.json"); err == nil {
|
||||
var tree struct {
|
||||
Entries []struct {
|
||||
AvatarID uint32 `json:"AvatarID"`
|
||||
PointID uint32 `json:"PointID"`
|
||||
AnchorType string `json:"AnchorType"`
|
||||
} `json:"avatar_skill_tree"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &tree); err == nil {
|
||||
if res.AvatarSkillTreePoints == nil {
|
||||
res.AvatarSkillTreePoints = make(map[uint32]map[uint32]uint32)
|
||||
}
|
||||
for _, entry := range tree.Entries {
|
||||
if entry.AvatarID == 0 || entry.PointID == 0 {
|
||||
continue
|
||||
}
|
||||
var anchorNum uint32
|
||||
if n, _ := fmt.Sscanf(entry.AnchorType, "Point%d", &anchorNum); n == 1 {
|
||||
if res.AvatarSkillTreePoints[entry.AvatarID] == nil {
|
||||
res.AvatarSkillTreePoints[entry.AvatarID] = make(map[uint32]uint32)
|
||||
}
|
||||
res.AvatarSkillTreePoints[entry.AvatarID][anchorNum] = entry.PointID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PearlSR's resource export keeps avatar data in AvatarConfig.json;
|
||||
// res.json may legitimately contain an empty avatarConfigs object.
|
||||
if raw, err := readResourceFile("resources/AvatarConfig.json"); err == nil {
|
||||
@@ -193,12 +232,14 @@ func loadSupplementaryResources(res *JsonConfig) {
|
||||
Entries []struct {
|
||||
ID uint32 `json:"AvatarID"`
|
||||
DamageType string `json:"DamageType"`
|
||||
MaxRank uint32 `json:"MaxRank"`
|
||||
} `json:"avatar_config"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &avatars); err == nil {
|
||||
for _, avatar := range avatars.Entries {
|
||||
cfg := res.AvatarConfigs[avatar.ID]
|
||||
cfg.DamageType = avatar.DamageType
|
||||
cfg.MaxRank = avatar.MaxRank
|
||||
res.AvatarConfigs[avatar.ID] = cfg
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user