230 lines
10 KiB
Go
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
|
|
})
|
|
}
|