Files

129 lines
5.1 KiB
Go

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)
}
cwProjectRoleIDs(&saved, catalog)
if err := cwProjectLegacyBonfirePhase(catalog, &saved, snapshot.Runtime); err != nil {
return nil, 0, 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, commandID ...string) error {
return s.saveCurrencyWarsDataWithRuntime(data, catalog, nil, commandID...)
}
func (s *Session) saveCurrencyWarsDataWithRuntime(data *goproto.GridFightGetDataScRsp, catalog *common.CurrencyWarsCatalog, runtime *common.CurrencyWarsRuntimeState, commandID ...string) error {
cwProjectRoleIDs(data, catalog)
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)
if run := data.DHFOFLFCLAH; run != nil {
if runtime != nil && runtime.RunID != run.UniqueId {
return fmt.Errorf("CW runtime rules belong to a different run")
}
if runtime == nil {
existing, err := cwBonfireRuntime(catalog, run)
if err == nil && existing != nil && existing.RunID == run.UniqueId {
runtime = existing
} else {
runtime = &common.CurrencyWarsRuntimeState{RunID: run.UniqueId}
}
}
next.Runtime = runtime
}
var revision uint64
if len(commandID) > 1 {
return fmt.Errorf("only one CW command may be committed per transaction")
}
if len(commandID) == 1 {
revision, err = common.CommitCurrencyWarsCommand(s.currencyWarsRevision, next, commandID[0])
} else {
revision, err = common.CommitCurrencyWarsSnapshot(s.currencyWarsRevision, next)
}
if err != nil {
return err
}
s.currencyWarsRevision = revision
return nil
}