Files
GaCy-GO/internal/gameserver/currency_wars_entry_choice.go
2026-10-28 18:54:43 +07:00

230 lines
10 KiB
Go

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.
cwHandleNamed("GridFightHandleBonfireHealHp", "GridFightHandlePendingActionScRsp", func(s *Session, req *goproto.GridFightHandleBonfireHealHpCsReq, rsp *goproto.GridFightHandlePendingActionScRsp) error {
rsp.QueuePosition = req.QueuePosition
rsp.Retcode = uint32(goproto.Retcode_RET_GRID_FIGHT_CONF_MISS)
selection := req.GetMFFCPLBNHCF()
if req.GetIIDBLCFAAPC() != nil {
if _, active := CURRENT_BATTLE.Load(s.Token); active {
return fmt.Errorf("CW Bonfire selection cannot change during Battle")
}
}
if selection == nil && req.GetNNDGEJFLGGF() == nil && req.GetLGNPHHBGKFH() == nil && req.GetIBCHMBNIPLK() == nil && req.GetDKDFDGJGIHM() == nil && req.GetFIKAGBAEOAP() == nil && req.GetBAKAKODKECM() == nil && req.GetIIDBLCFAAPC() == 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
var runtime *common.CurrencyWarsRuntimeState
if selection != nil {
after, notify, err = currencyWarsEntryChoice(catalog, before, req.QueuePosition, selection.JCAPCNLJGFC)
} else {
after = proto.Clone(before).(*goproto.MPIMAEMCEFH)
switch {
case req.GetIIDBLCFAAPC() != nil && before.FMIGOKIDKJL.GetIIDBLCFAAPC() != nil:
notify, runtime, err = cwSelectBonfire(catalog, after, req.GetIIDBLCFAAPC())
case req.GetBAKAKODKECM() != nil && before.FMIGOKIDKJL.GetBAKAKODKECM() != nil:
notify, err = cwSelectEncounter(after, req.GetBAKAKODKECM())
case req.GetFIKAGBAEOAP() != nil && before.FMIGOKIDKJL.GetFIKAGBAEOAP() != nil:
notify, err = cwSelectSupply(catalog, after, req.GetFIKAGBAEOAP())
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)
route, loadErr := catalog.RouteNodes(stage.GetEMOHLJHPOPC())
if loadErr != nil || len(route) == 0 {
err = fmt.Errorf("CW RoundBegin route missing: %v", loadErr)
} else if stage.DCAHJELNJNP == route[0].ChapterID && stage.SectionId == route[0].SectionID {
notify, err = cwInitialRound(catalog, after)
} else {
notify, err = cwNextRound(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
runtime, err = cwApplyBonfireEnemyRules(catalog, after, runtime)
if err != nil {
return err
}
// Enemy stars and their one-time rule marker must commit together.
if stage, _ := cwStage(after); stage != nil && runtime.Rules["PetPalStarDown"] != 0 && stage.GetIMPNAHBCADN().GetMPOBBIPMNNJ() != nil {
notify.GCPNIHHBGGD = append(notify.GCPNIHHBGGD, cwNotify(&goproto.KFNCAIHIHCM{MNLCHEJKDHO: &goproto.KFNCAIHIHCM_OOICHLAMNIG{OOICHLAMNIG: &goproto.AINFNFIDGFF{DCAHJELNJNP: stage.DCAHJELNJNP, SectionId: stage.SectionId, IMPNAHBCADN: stage.IMPNAHBCADN}}}).GCPNIHHBGGD...)
}
if err := s.saveCurrencyWarsDataWithRuntime(data, catalog, runtime); err != nil {
return err
}
if req.GetIIDBLCFAAPC().GetECDACDNDHGO() != nil {
player, _, _ := cwComponents(after)
stage, _ := cwStage(after)
PacketLog(fmt.Sprintf("CW_BONFIRE_TAKE saved old_queue=%d gold=%d level=%d exp=%d next=%d/%d revision=%d", req.QueuePosition, player.Gold, player.EHNJDIDABHD, player.GridFightCurLevelExp, stage.DCAHJELNJNP, stage.SectionId, s.currencyWarsRevision))
}
// Save before synchronizing, then finish the request after the ordered
// notification, matching the reference's pending-action callback flow.
if err := cwSend(s, 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
})
}