186 lines
7.7 KiB
Go
186 lines
7.7 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"
|
||
|
|
)
|
||
|
|
|
||
|
|
// 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
|
||
|
|
}
|