package gameserver import ( "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "gac-hkrpg-go/internal/common" goproto "gac-hkrpg-go/protocol/proto" "google.golang.org/protobuf/proto" ) // Initial entry presets are captured reference states, not a replacement for // the procedural run/action processor. IDs, route displays, pending choices // and modifiers stay in the revision-bound resource rather than handler code. func buildCurrencyWarsInitialRun(catalog *common.CurrencyWarsCatalog, req *goproto.GridFightStartGamePlayCsReq, uniqueID uint32) (*goproto.MPIMAEMCEFH, error) { if catalog == nil || req == nil || uniqueID == 0 { return nil, fmt.Errorf("CW start inputs are incomplete") } if req.LJNGPCKPJLF != catalog.ActiveSeasonID { return nil, fmt.Errorf("CW requested season is not active") } raw, err := common.ReadResourceFile("resources/CurrencyWarsInitialRuns.json") if err != nil { return nil, err } var resource struct { Format uint32 `json:"format_version"` Client string `json:"target_client_version"` Assembly string `json:"target_assembly_sha256"` SourceSHA string `json:"source_binary_sha256"` Entries []struct { Division uint32 `json:"division_id"` Overclock bool `json:"is_overclock"` Wire []byte `json:"run_wire"` CaptureSHA string `json:"capture_sha256"` } `json:"entries"` } if err := json.Unmarshal(raw, &resource); err != nil { return nil, err } if resource.Format != 1 || resource.Client != catalog.ClientVersion || resource.Assembly != catalog.Provenance.GameAssemblySHA256 { return nil, fmt.Errorf("CW initial preset revision differs from the current catalog") } if h, err := hex.DecodeString(resource.SourceSHA); err != nil || len(h) != sha256.Size { return nil, fmt.Errorf("CW initial preset source identity is missing") } var selected *goproto.MPIMAEMCEFH for _, entry := range resource.Entries { if entry.Division != req.DivisionId || entry.Overclock != req.IsOverlock { continue } if selected != nil { return nil, fmt.Errorf("CW initial preset key is duplicated") } if h, err := hex.DecodeString(entry.CaptureSHA); err != nil || len(h) != sha256.Size { return nil, fmt.Errorf("CW initial preset capture identity is missing") } var run goproto.MPIMAEMCEFH if err := proto.Unmarshal(entry.Wire, &run); err != nil { return nil, fmt.Errorf("decode CW initial preset: %w", err) } if run.DivisionId != req.DivisionId || run.IsOverlock != req.IsOverlock || run.LJNGPCKPJLF != req.LJNGPCKPJLF { return nil, fmt.Errorf("CW initial preset identity differs from its resource key") } if err := validateCurrencyWarsEntryComponents(&run, catalog); err != nil { return nil, err } run.UniqueId = uniqueID selected = &run } if selected == nil { return nil, fmt.Errorf("CW requested division/mode has no captured initial preset") } cwEnsureSpotlights(selected) return selected, nil } func validateCurrencyWarsEntryComponents(run *goproto.MPIMAEMCEFH, catalog *common.CurrencyWarsCatalog) error { if run == nil || run.FMIGOKIDKJL == nil || run.JLDDDLDNFEF == nil || run.CAAGABEPPPE == nil { return fmt.Errorf("CW initial pending/context/modifier containers are missing") } var player *goproto.NIHNGPKHEPC var stage *goproto.BEEFLEJAOCN var shop *goproto.POCJDMMANBM for _, info := range run.NAGDJGMMCBE { if info == nil { return fmt.Errorf("CW initial component is nil") } if p := info.GetFAOCFBOFCJP(); p != nil { if player != nil { return fmt.Errorf("CW player component is duplicated") } player = p } if p := info.GetKLCPAAHDPJF(); p != nil { if stage != nil { return fmt.Errorf("CW stage component is duplicated") } stage = p } if p := info.GetEBHDIOGALIE(); p != nil { if shop != nil { return fmt.Errorf("CW shop component is duplicated") } shop = p } } if player == nil || stage == nil || shop == nil || shop.ABBNGCPECAI == nil { return fmt.Errorf("CW initial player/stage/shop is incomplete") } if player.PPKKHHNEOHB == 0 || player.GridFightLineupHp > player.PPKKHHNEOHB || player.EHNJDIDABHD == 0 || player.EHNJDIDABHD > player.NDAINFLOLPJ { return fmt.Errorf("CW initial health/level is invalid") } if _, err := catalog.PlayerLevel(player.EHNJDIDABHD); err != nil { return err } displays := make(map[currencyWarsNodeCoordinate]*goproto.OHMDHKGAEMM) for _, node := range stage.OOMMIIKPHBC { if node == nil || node.DisplayValue == nil { return fmt.Errorf("CW initial route node is incomplete") } coord := currencyWarsNodeCoordinate{Chapter: node.DCAHJELNJNP, Section: node.SectionId} if _, exists := displays[coord]; exists { return fmt.Errorf("CW initial route coordinate is duplicated") } if _, err := catalog.NodeTypeName(node.DisplayValue.CJCCMAACJDB); err != nil { return err } displays[coord] = node.DisplayValue } copyStage := proto.Clone(stage).(*goproto.BEEFLEJAOCN) return applyCurrencyWarsRoute(copyStage, catalog, stage.EMOHLJHPOPC, currencyWarsNodeCoordinate{Chapter: stage.DCAHJELNJNP, Section: stage.SectionId}, displays) } func newCurrencyWarsData(catalog *common.CurrencyWarsCatalog) (*goproto.GridFightGetDataScRsp, error) { // March7's freeplay profile marks the tutorial as unlocked. Without this // metadata the current Client sends a tutorial request (division omitted), // not the division selected by the normal entry page. This policy does not // implement or award completion of the scripted tutorial itself. raw, err := common.ReadResourceFile("resources/CurrencyWarsInitialRuns.json") if err != nil { return nil, err } var entry struct { Client string `json:"target_client_version"` Assembly string `json:"target_assembly_sha256"` Profile struct { Policy string `json:"tutorial_policy"` Completion uint32 `json:"tutorial_completion"` Season uint32 `json:"last_tutorial_season"` History map[uint32]*goproto.GLHCHCJMFGA `json:"tutorial_history"` } `json:"entry_profile"` } if err := json.Unmarshal(raw, &entry); err != nil { return nil, err } if entry.Client != catalog.ClientVersion || entry.Assembly != catalog.Provenance.GameAssemblySHA256 || entry.Profile.Policy != "freeplay_unlocked" || entry.Profile.Completion == 0 || entry.Profile.Season != catalog.ActiveSeasonID { return nil, fmt.Errorf("CW entry profile revision or freeplay tutorial policy is invalid") } if len(entry.Profile.History) == 0 { return nil, fmt.Errorf("CW freeplay tutorial history is missing") } for season, history := range entry.Profile.History { if season == 0 || history == nil || history.DivisionId == 0 || history.GDAKCOICIDA == 0 { return nil, fmt.Errorf("CW freeplay tutorial history is incomplete") } } var division uint32 for _, d := range catalog.SeasonDivisions(catalog.ActiveSeasonID) { if !d.IsPromotion { division = d.ID break } } if division == 0 { return nil, fmt.Errorf("CW has no initial division in active season") } return &goproto.GridFightGetDataScRsp{LFLCDNGALME: &goproto.CLEDCMIOIPF{ PKELAIPIHMF: &goproto.CKPOBLIFMGN{FBJMBJKMAJA: entry.Profile.History, ENECKKGPKHI: entry.Profile.Season, MHFHLBHFIBC: &goproto.FMNKKAMOHEE{}}, HFPEJIDDDJG: &goproto.AMEEHIIPMFP{GDAKCOICIDA: entry.Profile.Completion, ADFEKPNIFOJ: catalog.ActiveSeasonID, GEGMHOMNBDG: catalog.ActiveSubSeasonID, DivisionId: division, Exp: &goproto.GLIFOLCEMIN{}, MHFHLBHFIBC: &goproto.FMNKKAMOHEE{}, NLENPEPIHAC: &goproto.NMECHIFGKAD{ DNEGDNEBJMB: &goproto.NCHKFJPOHAH{}, NIOAIKLPACM: &goproto.AHPHDNBNLPJ{}, EECJFCNKKMB: &goproto.LLBMKOILIDN{}, KJIJEGAHLGB: &goproto.GNIIDINFPDO{}, NNIJIFGJJJA: &goproto.ODKODPBCDKD{}, JLFANEOAHOC: &goproto.GKNIJKHENBF{}, EJHDGLHCNLH: &goproto.NEDHMLJKIOA{}, PJBIPMKACOF: &goproto.FCEILNCDMAH{}, }}, }}, nil }