package common import ( "encoding/json" "fmt" "sync" ) type TechniqueFood struct { RecoverMP uint32 `json:"recover_mp"` MaxUse uint32 `json:"max_use"` PileLimit uint32 `json:"pile_limit"` } type MazeSkillRule struct { ID uint32 `json:"MazeSkillId"` MPCost uint32 `json:"MPCost"` Trigger string `json:"SkillTriggerKey"` } type TechniqueRules struct { ClientVersion string `json:"client_version"` BaseMaxMP uint32 `json:"base_max_mp"` InitialMP uint32 `json:"initial_mp"` MaxMPByAvatar map[uint32]uint32 `json:"max_mp_by_avatar"` NonAttackTechniques []uint32 `json:"non_attack_techniques"` Avatars map[uint32][]MazeSkillRule `json:"avatars"` Foods map[uint32]TechniqueFood `json:"foods"` InitialFoodCount uint32 `json:"initial_food_count"` AutoFoodDefault bool `json:"auto_food_default"` } var techniqueOnce sync.Once var techniqueRules TechniqueRules var techniqueErr error func LoadTechniqueRules() (*TechniqueRules, error) { techniqueOnce.Do(func() { raw, err := ReadResourceFile("resources/TechniqueRules.json") if err != nil { techniqueErr = err return } if err = json.Unmarshal(raw, &techniqueRules); err != nil { techniqueErr = err return } cfg, err := LoadRuntimeConfig() if err != nil { techniqueErr = err return } if techniqueRules.ClientVersion != cfg.ClientVersion || techniqueRules.BaseMaxMP == 0 || techniqueRules.InitialMP > techniqueRules.BaseMaxMP { techniqueErr = fmt.Errorf("invalid or mismatched TechniqueRules client resource") return } for id, f := range techniqueRules.Foods { if id == 0 || f.RecoverMP == 0 || f.MaxUse == 0 || techniqueRules.InitialFoodCount > f.PileLimit { techniqueErr = fmt.Errorf("invalid technique food %d", id) return } } }) return &techniqueRules, techniqueErr } func (r *TechniqueRules) MaxMP(avatars map[uint32]uint32) uint32 { maximum := r.BaseMaxMP // PearlSR buildLineup selects the largest team cap; it does not add both caps. for _, id := range avatars { if value := r.MaxMPByAvatar[id]; value > maximum { maximum = value } } return maximum } // Called only by an explicit inventory/cast operation, never by save readers. func (f *FreesrData) EnsureTechniqueFoods() error { if f.TechniqueFoodStock != nil && f.AutoTechniqueFood != nil { return nil } r, err := LoadTechniqueRules() if err != nil { return err } if f.TechniqueFoodStock == nil { f.TechniqueFoodStock = map[uint32]uint32{} for id := range r.Foods { f.TechniqueFoodStock[id] = r.InitialFoodCount } } if f.AutoTechniqueFood == nil { value := r.AutoFoodDefault f.AutoTechniqueFood = &value } return f.SavePersistentChecked() } // Refresh just the setting, and advance its merge baseline so a subsequent // session position save cannot undo an external API toggle. func (f *FreesrData) RefreshTechniqueSettings(p *Persistent) { f.AutoTechniqueFood = p.AutoTechniqueFood if snapshot, err := jsonObject(f.persistentSnapshot()); err == nil { if f.persistentBaseline == nil { f.persistentBaseline = map[string]any{} } f.persistentBaseline["auto_technique_food"] = snapshot["auto_technique_food"] } }