Files
GaCy-GO/internal/gameserver/currency_wars_start.go
T

186 lines
7.7 KiB
Go
Raw Normal View History

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
}