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" ) // This advances the existing captured-entry prototype, not the final // procedural portal/effect processor. Each starter outcome is a complete, // internally consistent reference capture. No adventure inventory is touched. func currencyWarsEntryChoice(catalog *common.CurrencyWarsCatalog, before *goproto.MPIMAEMCEFH, queue, portal uint32) (*goproto.MPIMAEMCEFH, *goproto.GridFightSyncUpdateResultScNotify, error) { if catalog == nil || before == nil || before.FMIGOKIDKJL == nil || before.FMIGOKIDKJL.QueuePosition != queue { return nil, nil, fmt.Errorf("CW pending queue differs from the active run") } offer := before.FMIGOKIDKJL.GetMFFCPLBNHCF() if offer == nil { return nil, nil, fmt.Errorf("CW active pending action is not an initial portal selection") } found := false for _, id := range offer.NPHOIMEEDHM { found = found || id == portal } if portal == 0 || !found { return nil, nil, fmt.Errorf("CW portal is not in the active offer") } for _, component := range before.NAGDJGMMCBE { if team := component.GetEJIPINIKNCK(); team != nil && len(team.GridGameRoleList) != 0 { return nil, nil, fmt.Errorf("CW entry prototype cannot replace an existing team") } } raw, err := common.ReadResourceFile("resources/CurrencyWarsEntryTransitions.json") if err != nil { return nil, nil, err } var resource struct { Format uint32 `json:"format_version"` Client string `json:"target_client_version"` Assembly string `json:"target_assembly_sha256"` Source string `json:"source_binary_sha256"` Choices []struct { Division uint32 `json:"division_id"` Season uint32 `json:"season_id"` Overclock bool `json:"is_overclock"` Portal uint32 `json:"portal_id"` BeforeQueue uint32 `json:"before_queue"` AfterWire []byte `json:"after_wire"` NotifyWire []byte `json:"notify_wire"` AfterSHA string `json:"after_sha256"` NotifySHA string `json:"notify_sha256"` } `json:"choices"` } if err := json.Unmarshal(raw, &resource); err != nil { return nil, nil, err } if resource.Format != 1 || resource.Client != catalog.ClientVersion || resource.Assembly != catalog.Provenance.GameAssemblySHA256 { return nil, nil, fmt.Errorf("CW entry transition revision differs from the active catalog") } if h, err := hex.DecodeString(resource.Source); err != nil || len(h) != sha256.Size { return nil, nil, fmt.Errorf("CW entry transition source identity is missing") } var after *goproto.MPIMAEMCEFH var notify *goproto.GridFightSyncUpdateResultScNotify for _, choice := range resource.Choices { if choice.Division != before.DivisionId || choice.Season != before.LJNGPCKPJLF || choice.Overclock != before.IsOverlock || choice.Portal != portal || choice.BeforeQueue != queue { continue } if after != nil { return nil, nil, fmt.Errorf("CW entry transition key is duplicated") } if !currencyWarsWireDigest(choice.AfterWire, choice.AfterSHA) || !currencyWarsWireDigest(choice.NotifyWire, choice.NotifySHA) { return nil, nil, fmt.Errorf("CW entry transition wire digest differs") } after = new(goproto.MPIMAEMCEFH) notify = new(goproto.GridFightSyncUpdateResultScNotify) if err := proto.Unmarshal(choice.AfterWire, after); err != nil { return nil, nil, err } if err := proto.Unmarshal(choice.NotifyWire, notify); err != nil { return nil, nil, err } if after.DivisionId != before.DivisionId || after.LJNGPCKPJLF != before.LJNGPCKPJLF || after.IsOverlock != before.IsOverlock || after.FMIGOKIDKJL == nil || after.FMIGOKIDKJL.QueuePosition <= queue { return nil, nil, fmt.Errorf("CW captured transition identity or next queue is invalid") } selected := false finished := false for _, result := range notify.GCPNIHHBGGD { for _, update := range result.JKPJFKCBIEM { if value := update.GetHPIIGHOELBH(); value != nil && value.PortalBuffId == portal { selected = true } finished = finished || update.GetPMFBIGLPJJA() == queue } } if !selected || !finished { return nil, nil, fmt.Errorf("CW captured transition lacks portal or queue acknowledgement") } if err := validateCurrencyWarsEntryComponents(after, catalog); err != nil { return nil, nil, err } after.UniqueId = before.UniqueId } if after == nil { return nil, nil, fmt.Errorf("CW initial portal outcome is not captured for this division/mode") } // The native reference's GetData projection omits selected portals even // though the live sync includes them. Preserve this in our resume snapshot. for _, component := range after.NAGDJGMMCBE { if stage := component.GetKLCPAAHDPJF(); stage != nil { stage.GridFightPortalBuffList = append(stage.GridFightPortalBuffList, &goproto.GIDEDMOPFKF{PortalBuffId: portal}) } } cwEnsureSpotlights(after) return after, notify, nil } func currencyWarsWireDigest(wire []byte, expected string) bool { hash := sha256.Sum256(wire) return len(wire) > 0 && hex.EncodeToString(hash[:]) == expected } func init() { // The revision's request name is misleading. Controlled reference replay // establishes this request's MFFCPLBNHCF arm as portal selection and its // response as GridFightHandlePendingActionScRsp. No numeric alias is used. handleNamed("GridFightHandleBonfireHealHp", "GridFightHandlePendingActionScRsp", func(s *Session, req *goproto.GridFightHandleBonfireHealHpCsReq, rsp *goproto.GridFightHandlePendingActionScRsp) (resultErr error) { rsp.QueuePosition = req.QueuePosition rsp.Retcode = uint32(goproto.Retcode_RET_GRID_FIGHT_CONF_MISS) defer func() { if resultErr != nil { PacketLog(fmt.Sprintf("CW_PENDING_ACTION failed queue=%d error=%v", req.QueuePosition, resultErr)) rsp.Retcode = uint32(goproto.Retcode_RET_GRID_FIGHT_CONF_MISS) resultErr = nil // Send the typed error response so the wait block is released. } }() selection := req.GetMFFCPLBNHCF() if selection == nil && req.GetNNDGEJFLGGF() == nil && req.GetLGNPHHBGKFH() == nil && req.GetIBCHMBNIPLK() == nil && req.GetDKDFDGJGIHM() == nil { PacketLog(fmt.Sprintf("CW_PENDING_ACTION unsupported queue=%d action=%T", req.QueuePosition, req.FGLIODAJFML)) return nil } catalog, err := common.LoadCurrencyWarsCatalog() if err != nil { return err } data, revision, err := readCurrencyWarsData(catalog) if err != nil { return err } if data == nil || data.DHFOFLFCLAH == nil { rsp.Retcode = uint32(goproto.Retcode_RET_GRID_FIGHT_NOT_IN_GAMEPLAY) return nil } if revision != s.currencyWarsRevision { rsp.Retcode = uint32(goproto.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH) return nil } before := data.DHFOFLFCLAH if before.FMIGOKIDKJL == nil || before.FMIGOKIDKJL.QueuePosition != req.QueuePosition { rsp.Retcode = uint32(goproto.Retcode_RET_GRID_FIGHT_PARAM_NOT_MATCH) return nil } var after *goproto.MPIMAEMCEFH var notify *goproto.GridFightSyncUpdateResultScNotify if selection != nil { after, notify, err = currencyWarsEntryChoice(catalog, before, req.QueuePosition, selection.JCAPCNLJGFC) } else { after = proto.Clone(before).(*goproto.MPIMAEMCEFH) switch { case req.GetDKDFDGJGIHM() != nil && before.FMIGOKIDKJL.GetEEKMIAMGHFL() != nil: notify, err = cwSelectSpotlight(catalog, after, req.GetDKDFDGJGIHM()) case req.GetNNDGEJFLGGF() != nil && before.FMIGOKIDKJL.GetNNDGEJFLGGF() != nil: stage, _ := cwStage(after) entries, loadErr := cwNodeTransitions(catalog) isNextRound := false if loadErr == nil && stage != nil { for _, entry := range entries { level := new(goproto.AINFNFIDGFF) if proto.Unmarshal(entry.LevelWire, level) == nil && level.DCAHJELNJNP == stage.DCAHJELNJNP && level.SectionId == stage.SectionId { isNextRound = true } } } if isNextRound { notify, err = cwNextRound(catalog, after) } else { notify, err = cwInitialRound(catalog, after) } case req.GetLGNPHHBGKFH() != nil && before.FMIGOKIDKJL.GetLGNPHHBGKFH() != nil: notify, err = cwReturnPreparation(after) case req.GetIBCHMBNIPLK() != nil && before.FMIGOKIDKJL.GetIBCHMBNIPLK() != nil: notify, err = cwSelectInvestment(catalog, after, req.GetIBCHMBNIPLK().AugmentId) default: err = fmt.Errorf("pending request kind differs from active queue") } } if err != nil { PacketLog(fmt.Sprintf("CW_PENDING_ACTION rejected queue=%d action=%T error=%v", req.QueuePosition, req.FGLIODAJFML, err)) return nil } data.DHFOFLFCLAH = after if err := s.saveCurrencyWarsData(data, catalog); err != nil { return err } // Save before synchronizing, then finish the request after the ordered // notification, matching the reference's pending-action callback flow. if err := s.Send(notify); err != nil { return err } rsp.Retcode = 0 PacketLog(fmt.Sprintf("CW_PENDING_ACTION advanced action=%T queue=%d next_queue=%d preparing=%t revision=%d", req.FGLIODAJFML, req.QueuePosition, after.GetFMIGOKIDKJL().GetQueuePosition(), after.FMIGOKIDKJL == nil, s.currencyWarsRevision)) return nil }) }