package gameserver import ( "fmt" "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" ) // Shared read path for entry and GetData. It never adopts a revision into a // session or writes persistent; callers choose when a validated view becomes // their current session revision. func readCurrencyWarsData(catalog *common.CurrencyWarsCatalog) (*goproto.GridFightGetDataScRsp, uint64, error) { if catalog == nil { return nil, 0, fmt.Errorf("CW catalog is missing") } cfg, err := common.LoadRuntimeConfig() if err != nil { return nil, 0, err } snapshot, err := common.ReadCurrencyWarsSnapshot(cfg.PlayerUID, catalog.ClientVersion, catalog.Provenance.GameAssemblySHA256) if err != nil { return nil, 0, err } if snapshot == nil { return nil, 0, nil } var saved goproto.GridFightGetDataScRsp if err := proto.Unmarshal(snapshot.WireData, &saved); err != nil { return nil, 0, fmt.Errorf("decode saved CW protocol: %w", err) } if err := validateCurrencyWarsSavedData(&saved, catalog); err != nil { return nil, 0, err } return &saved, snapshot.Revision, nil } // Validate known metadata prerequisites before restoring a saved response. // Full run validation belongs to the gameplay state machine, which is still // being implemented. Entry and captured initial-choice handlers commit only // their validated phases; this is not full gameplay/resume validation. func validateCurrencyWarsSavedData(data *goproto.GridFightGetDataScRsp, catalog *common.CurrencyWarsCatalog) error { if data == nil || catalog == nil || data.Retcode != 0 || data.LFLCDNGALME == nil { return fmt.Errorf("saved CW response is missing metadata or contains an error") } meta := data.LFLCDNGALME if meta.PKELAIPIHMF == nil || meta.PKELAIPIHMF.MHFHLBHFIBC == nil || meta.HFPEJIDDDJG == nil { return fmt.Errorf("saved CW permanent/season state is incomplete") } season := meta.HFPEJIDDDJG if season.ADFEKPNIFOJ != catalog.ActiveSeasonID || season.GEGMHOMNBDG != catalog.ActiveSubSeasonID || season.Exp == nil || season.MHFHLBHFIBC == nil || season.NLENPEPIHAC == nil { return fmt.Errorf("saved CW season state differs from captured active season") } division, ok := catalog.Division(season.DivisionId) if !ok || division.SeasonID != catalog.ActiveSeasonID { return fmt.Errorf("saved CW division is not in active season") } book := season.NLENPEPIHAC if book.DNEGDNEBJMB == nil || book.NIOAIKLPACM == nil || book.EECJFCNKKMB == nil || book.KJIJEGAHLGB == nil || book.NNIJIFGJJJA == nil || book.JLFANEOAHOC == nil || book.EJHDGLHCNLH == nil || book.PJBIPMKACOF == nil { return fmt.Errorf("saved CW handbook containers are incomplete") } if run := data.DHFOFLFCLAH; run != nil { // Native Start/GetData captures establish LJNGPCKPJLF as Season. // MPJPLKKJNGE is not a required season identity; it can be zero. if run.UniqueId == 0 || run.LJNGPCKPJLF != catalog.ActiveSeasonID || run.DivisionId == 0 { return fmt.Errorf("saved CW active run identity is incomplete") } if d, ok := catalog.Division(run.DivisionId); !ok || d.SeasonID != run.LJNGPCKPJLF { return fmt.Errorf("saved CW run division differs from its season") } } return nil } // saveCurrencyWarsData must be called only after a validated CW state-machine // transition, under the session state lock. It does not alter adventure data. func (s *Session) saveCurrencyWarsData(data *goproto.GridFightGetDataScRsp, catalog *common.CurrencyWarsCatalog) error { if err := validateCurrencyWarsSavedData(data, catalog); err != nil { return err } cfg, err := common.LoadRuntimeConfig() if err != nil { return err } wire, err := proto.Marshal(data) if err != nil { return err } next := common.NewCurrencyWarsSnapshot(cfg.PlayerUID, catalog.ClientVersion, catalog.Provenance.GameAssemblySHA256, wire) revision, err := common.CommitCurrencyWarsSnapshot(s.currencyWarsRevision, next) if err != nil { return err } s.currencyWarsRevision = revision return nil }