365 lines
13 KiB
Go
365 lines
13 KiB
Go
package gameserver
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand/v2"
|
|
|
|
"gac-hkrpg-go/internal/common"
|
|
pb "gac-hkrpg-go/protocol/proto"
|
|
"google.golang.org/protobuf/proto"
|
|
)
|
|
|
|
type cwReferenceEnemies struct {
|
|
Format uint32 `json:"format_version"`
|
|
Client string `json:"target_client_version"`
|
|
Assembly string `json:"target_assembly_sha256"`
|
|
Reference string `json:"reference_executable_sha256"`
|
|
Excel string `json:"excel_dump_sha256"`
|
|
Policy struct {
|
|
FirstChapter uint32 `json:"first_chapter"`
|
|
FirstStar uint32 `json:"first_chapter_boss_star"`
|
|
LaterStar uint32 `json:"later_chapter_boss_star"`
|
|
BaseElite uint32 `json:"base_elite_group"`
|
|
AlternateThreshold uint32 `json:"alternate_route_threshold"`
|
|
AlternateElite uint32 `json:"alternate_base_elite_group"`
|
|
CampMaxTier uint32 `json:"camp_monster_max_tier"`
|
|
MonsterMaxTier uint32 `json:"monster_max_tier"`
|
|
FirstMonsterCount uint32 `json:"first_monster_count"`
|
|
LaterMonsterCount uint32 `json:"later_monster_count"`
|
|
CampWaveCounts [][]uint32 `json:"camp_wave_counts"`
|
|
ChapterStarWeights [][]uint32 `json:"chapter_star_weights"`
|
|
} `json:"policy"`
|
|
MonsterTiers map[uint32]uint32 `json:"monster_tiers"`
|
|
EliteGroups []uint32 `json:"elite_groups"`
|
|
}
|
|
|
|
func cwLoadReferenceEnemies(c *common.CurrencyWarsCatalog) (*cwReferenceEnemies, error) {
|
|
b, e := common.ReadResourceFile("resources/CurrencyWarsReferenceEnemies.json")
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
v := new(cwReferenceEnemies)
|
|
if e = json.Unmarshal(b, v); e != nil {
|
|
return nil, e
|
|
}
|
|
if c == nil || v.Format != 1 || v.Client != c.ClientVersion || v.Assembly != c.Provenance.GameAssemblySHA256 {
|
|
return nil, fmt.Errorf("CW reference enemy revision differs")
|
|
}
|
|
for _, s := range []string{v.Reference, v.Excel} {
|
|
h, e := hex.DecodeString(s)
|
|
if e != nil || len(h) != sha256.Size {
|
|
return nil, fmt.Errorf("CW reference enemy provenance missing")
|
|
}
|
|
}
|
|
if v.Policy.FirstChapter == 0 || v.Policy.FirstStar == 0 || v.Policy.LaterStar == 0 || v.Policy.BaseElite == 0 || v.Policy.AlternateElite == 0 || v.Policy.AlternateThreshold == 0 || len(v.MonsterTiers) == 0 || len(v.EliteGroups) == 0 {
|
|
return nil, fmt.Errorf("CW reference enemy policy incomplete")
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
func cwReferenceCurrentTemplate(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH) (*pb.BEEFLEJAOCN, common.CurrencyWarsReferenceNodeTemplate, error) {
|
|
s, _ := cwStage(run)
|
|
if s == nil {
|
|
return nil, common.CurrencyWarsReferenceNodeTemplate{}, fmt.Errorf("CW reference enemy stage missing")
|
|
}
|
|
rows, e := c.RouteNodes(s.EMOHLJHPOPC)
|
|
if e != nil {
|
|
return nil, common.CurrencyWarsReferenceNodeTemplate{}, e
|
|
}
|
|
t, e := common.LoadCurrencyWarsReferenceNodes(c)
|
|
if e != nil {
|
|
return nil, common.CurrencyWarsReferenceNodeTemplate{}, e
|
|
}
|
|
for _, r := range rows {
|
|
if r.ChapterID == s.DCAHJELNJNP && r.SectionID == s.SectionId {
|
|
v, e := t.Template(r.NodeTemplateID)
|
|
return s, v, e
|
|
}
|
|
}
|
|
return nil, common.CurrencyWarsReferenceNodeTemplate{}, fmt.Errorf("CW reference enemy coordinate not in route")
|
|
}
|
|
|
|
// The reference factory uses the chapter's already selected boss and camp.
|
|
// It never selects a new boss on entering the node or copies another run.
|
|
func cwGenerateReferenceEnemyLayer(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, display *pb.OHMDHKGAEMM) (*pb.NCKOKDHNJDC, error) {
|
|
s, t, e := cwReferenceCurrentTemplate(c, run)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
if display == nil || (t.NodeType != "Boss" && t.NodeType != "CampMonster") || display.StageId != t.StageID {
|
|
return nil, fmt.Errorf("CW reference enemy factory does not support node type %s", t.NodeType)
|
|
}
|
|
r, e := cwLoadReferenceEnemies(c)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
if t.NodeType == "CampMonster" {
|
|
return cwGenerateReferenceCampLayer(c, run, s, t, r)
|
|
}
|
|
if s.DCAHJELNJNP == 0 || s.BossInfo == nil || len(s.BossInfo.NCGHKPEMPDP) == 0 || len(s.HEKMEJLMGFF) == 0 {
|
|
return nil, fmt.Errorf("CW saved chapter boss/camp selection missing")
|
|
}
|
|
i := int(s.DCAHJELNJNP) - 1
|
|
if i >= len(s.BossInfo.NCGHKPEMPDP) {
|
|
i = len(s.BossInfo.NCGHKPEMPDP) - 1
|
|
}
|
|
boss := s.BossInfo.NCGHKPEMPDP[i]
|
|
if boss == nil || r.MonsterTiers[boss.MonsterId] == 0 {
|
|
return nil, fmt.Errorf("CW saved boss has no native monster row")
|
|
}
|
|
j := int(s.DCAHJELNJNP) - 1
|
|
if j >= len(s.HEKMEJLMGFF) {
|
|
j = len(s.HEKMEJLMGFF) - 1
|
|
}
|
|
campID := s.HEKMEJLMGFF[j].GetLOGHLGKIFEN()
|
|
camp, e := c.MonsterCamp(run.LJNGPCKPJLF, campID)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
allowed := false
|
|
for _, id := range camp.Season.MonsterList {
|
|
if id == boss.MonsterId {
|
|
allowed = true
|
|
break
|
|
}
|
|
}
|
|
if !allowed {
|
|
return nil, fmt.Errorf("CW saved boss %d is outside chapter camp %d", boss.MonsterId, campID)
|
|
}
|
|
star := r.Policy.LaterStar
|
|
if s.DCAHJELNJNP == r.Policy.FirstChapter {
|
|
star = r.Policy.FirstStar
|
|
}
|
|
monster := proto.Clone(boss).(*pb.AHKDPCIIELA)
|
|
monster.RoleStar = star
|
|
branch := &pb.KKBGIPCAGGH{IFIIOIIKJHO: t.PenaltyBonusRuleID, MonsterWaveList: []*pb.PNAKHANCNGK{{OCPEPEMNNEK: 1, PBLBBDBDLEK: []*pb.AHKDPCIIELA{monster}}}}
|
|
layer := &pb.MDJBBILMHLC{LNDKCPBJDKH: campID, MOADBALHMFD: 1, BPDABHGFBFO: []*pb.KKBGIPCAGGH{branch}}
|
|
return &pb.NCKOKDHNJDC{EFECLPJFOCM: &pb.NCKOKDHNJDC_MPOBBIPMNNJ{MPOBBIPMNNJ: layer}}, nil
|
|
}
|
|
|
|
func cwReferenceEarlierCount(c *common.CurrencyWarsCatalog, s *pb.BEEFLEJAOCN, kind string, sameChapter bool) (uint32, error) {
|
|
rows, e := c.RouteNodes(s.EMOHLJHPOPC)
|
|
if e != nil {
|
|
return 0, e
|
|
}
|
|
templates, e := common.LoadCurrencyWarsReferenceNodes(c)
|
|
if e != nil {
|
|
return 0, e
|
|
}
|
|
var count uint32
|
|
for _, row := range rows {
|
|
if sameChapter && row.ChapterID != s.DCAHJELNJNP {
|
|
continue
|
|
}
|
|
if row.ChapterID > s.DCAHJELNJNP || (row.ChapterID == s.DCAHJELNJNP && row.SectionID >= s.SectionId) {
|
|
continue
|
|
}
|
|
t, e := templates.Template(row.NodeTemplateID)
|
|
if e != nil {
|
|
return 0, e
|
|
}
|
|
if t.NodeType == kind {
|
|
count++
|
|
}
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
func cwGenerateReferenceCampLayer(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, s *pb.BEEFLEJAOCN, t common.CurrencyWarsReferenceNodeTemplate, r *cwReferenceEnemies) (*pb.NCKOKDHNJDC, error) {
|
|
if len(s.HEKMEJLMGFF) == 0 || len(r.Policy.CampWaveCounts) == 0 || len(r.Policy.ChapterStarWeights) == 0 {
|
|
return nil, fmt.Errorf("CW native camp/wave policy missing")
|
|
}
|
|
campID := s.HEKMEJLMGFF[rand.IntN(len(s.HEKMEJLMGFF))].GetLOGHLGKIFEN()
|
|
camp, e := c.MonsterCamp(run.LJNGPCKPJLF, campID)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
var minions []uint32
|
|
for _, id := range camp.Season.MonsterList {
|
|
tier, ok := r.MonsterTiers[id]
|
|
if ok && tier > 0 && tier <= r.Policy.CampMaxTier {
|
|
minions = append(minions, id)
|
|
}
|
|
}
|
|
if len(minions) == 0 {
|
|
return nil, fmt.Errorf("CW selected camp %d has no native minions", campID)
|
|
}
|
|
previous, e := cwReferenceEarlierCount(c, s, "CampMonster", true)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
i := int(previous)
|
|
if i >= len(r.Policy.CampWaveCounts) {
|
|
i = len(r.Policy.CampWaveCounts) - 1
|
|
}
|
|
chapter := int(s.DCAHJELNJNP) - 1
|
|
if chapter < 0 {
|
|
return nil, fmt.Errorf("CW chapter zero")
|
|
}
|
|
if chapter >= len(r.Policy.ChapterStarWeights) {
|
|
chapter = len(r.Policy.ChapterStarWeights) - 1
|
|
}
|
|
weights := r.Policy.ChapterStarWeights[chapter]
|
|
var total uint32
|
|
for _, w := range weights {
|
|
total += w
|
|
}
|
|
if total == 0 {
|
|
return nil, fmt.Errorf("CW native star weight sum zero")
|
|
}
|
|
branch := &pb.KKBGIPCAGGH{IFIIOIIKJHO: t.PenaltyBonusRuleID}
|
|
for wi, count := range r.Policy.CampWaveCounts[i] {
|
|
wave := &pb.PNAKHANCNGK{OCPEPEMNNEK: uint32(wi + 1)}
|
|
for n := uint32(0); n < count; n++ {
|
|
roll := rand.Uint32N(total)
|
|
star := uint32(1)
|
|
for j, w := range weights {
|
|
if roll < w {
|
|
star = uint32(j + 1)
|
|
break
|
|
}
|
|
roll -= w
|
|
}
|
|
wave.PBLBBDBDLEK = append(wave.PBLBBDBDLEK, &pb.AHKDPCIIELA{MonsterId: minions[rand.IntN(len(minions))], RoleStar: star})
|
|
}
|
|
branch.MonsterWaveList = append(branch.MonsterWaveList, wave)
|
|
}
|
|
layer := &pb.MDJBBILMHLC{LNDKCPBJDKH: campID, MOADBALHMFD: 1, BPDABHGFBFO: []*pb.KKBGIPCAGGH{branch}}
|
|
return &pb.NCKOKDHNJDC{EFECLPJFOCM: &pb.NCKOKDHNJDC_MPOBBIPMNNJ{MPOBBIPMNNJ: layer}}, nil
|
|
}
|
|
|
|
// gfRouteEliteGroup counts preceding CampMonster/EliteBranch/Boss nodes.
|
|
// Parameters are normalized from the extracted reference policy, and the
|
|
// result must occur in the extracted elite-group table.
|
|
func cwReferenceEliteGroup(c *common.CurrencyWarsCatalog, s *pb.BEEFLEJAOCN, r *cwReferenceEnemies) (uint32, error) {
|
|
rows, e := c.RouteNodes(s.EMOHLJHPOPC)
|
|
if e != nil {
|
|
return 0, e
|
|
}
|
|
templates, e := common.LoadCurrencyWarsReferenceNodes(c)
|
|
if e != nil {
|
|
return 0, e
|
|
}
|
|
group := r.Policy.BaseElite
|
|
if s.EMOHLJHPOPC >= r.Policy.AlternateThreshold {
|
|
group = r.Policy.AlternateElite
|
|
}
|
|
// FF uses route ID at this point; current captured routes retain that ID.
|
|
for _, row := range rows {
|
|
if row.ChapterID > s.DCAHJELNJNP || (row.ChapterID == s.DCAHJELNJNP && row.SectionID >= s.SectionId) {
|
|
continue
|
|
}
|
|
t, e := templates.Template(row.NodeTemplateID)
|
|
if e != nil {
|
|
return 0, e
|
|
}
|
|
if t.NodeType == "CampMonster" || t.NodeType == "EliteBranch" || t.NodeType == "Boss" {
|
|
group++
|
|
}
|
|
}
|
|
for group >= r.Policy.BaseElite {
|
|
for _, id := range r.EliteGroups {
|
|
if id == group {
|
|
return group, nil
|
|
}
|
|
}
|
|
group--
|
|
}
|
|
return 0, fmt.Errorf("CW reference elite group is absent from resource")
|
|
}
|
|
|
|
// A newly generated Boss has a native recipe rather than a captured run
|
|
// snapshot. Reuse scalar wave defaults from this mode/division's validated
|
|
// Client envelope, then replace identities, stars, difficulty and coordinates.
|
|
func cwBuildReferenceBossEnemy(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, s *pb.BEEFLEJAOCN, node *pb.OHMDHKGAEMM, info *pb.SceneBattleInfo) error {
|
|
_, t, e := cwReferenceCurrentTemplate(c, run)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if (t.NodeType != "Boss" && t.NodeType != "CampMonster" && t.NodeType != "Monster" && t.NodeType != "EliteBranch") || t.StageID != node.StageId {
|
|
return fmt.Errorf("CW native recipe unavailable for node %s", t.NodeType)
|
|
}
|
|
r, e := cwLoadReferenceEnemies(c)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
elite, e := cwReferenceEliteGroup(c, s, r)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
layer := s.IMPNAHBCADN.GetMPOBBIPMNNJ()
|
|
branch, e := cwSelectedEnemyBranch(layer)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if len(branch.MonsterWaveList) == 0 || branch.IFIIOIIKJHO == 0 {
|
|
return fmt.Errorf("CW saved branch differs from native recipe")
|
|
}
|
|
if t.NodeType != "EliteBranch" && branch.IFIIOIIKJHO != t.PenaltyBonusRuleID {
|
|
return fmt.Errorf("CW saved branch penalty differs from native recipe")
|
|
}
|
|
if t.NodeType == "EliteBranch" {
|
|
// EliteBranch offers can have distinct penalties. They must come from
|
|
// the revision-gated reference table, not from the base node's value.
|
|
penalties, err := common.LoadCurrencyWarsReferencePenalties(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, ok := penalties[branch.IFIIOIIKJHO]; !ok {
|
|
return fmt.Errorf("CW elite branch penalty is not in the reference table")
|
|
}
|
|
}
|
|
if len(info.MonsterWaveList) == 0 || info.MonsterWaveList[0].WaveParam == nil || info.EIGMKOJOEEI == nil {
|
|
return fmt.Errorf("CW validated battle wave envelope missing")
|
|
}
|
|
base := info.MonsterWaveList[0].WaveParam
|
|
// The proven first-node envelope omits Level/HardLevelGroup (both zero).
|
|
// Updated FFGO gfBattleMonsterWave likewise writes only formation and elite
|
|
// into this param. Zero is the captured wire value, not missing evidence;
|
|
// forcing the older explicit 90/1 recipe here would change the protocol.
|
|
if base.CPHIINKIDEB == 0 {
|
|
return fmt.Errorf("CW validated battle formation missing")
|
|
}
|
|
camp, e := c.MonsterCamp(run.LJNGPCKPJLF, layer.LNDKCPBJDKH)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
allowed := map[uint32]bool{}
|
|
for _, id := range camp.Season.MonsterList {
|
|
allowed[id] = true
|
|
}
|
|
waves := make([]*pb.SceneMonsterWave, 0, len(branch.MonsterWaveList))
|
|
for i, current := range branch.MonsterWaveList {
|
|
if current == nil || current.OCPEPEMNNEK != uint32(i+1) {
|
|
return fmt.Errorf("CW native enemy wave ordinal differs")
|
|
}
|
|
wave := &pb.SceneMonsterWave{BattleStageId: node.StageId, WaveId: current.OCPEPEMNNEK, WaveParam: proto.Clone(base).(*pb.SceneMonsterWaveParam)}
|
|
wave.WaveParam.EliteGroup = elite
|
|
for _, m := range current.PBLBBDBDLEK {
|
|
if m == nil || !allowed[m.MonsterId] || m.RoleStar == 0 {
|
|
return fmt.Errorf("CW saved native monster identity/star invalid")
|
|
}
|
|
wave.MonsterList = append(wave.MonsterList, &pb.SceneMonster{MonsterId: m.MonsterId, ExtraInfo: &pb.MNDMNDKLKIE{TeleportNice: &pb.MNDMNDKLKIE_EIGMKOJOEEI{EIGMKOJOEEI: &pb.HNEBODEBELG{RoleStar: m.RoleStar}}}})
|
|
}
|
|
if len(wave.MonsterList) == 0 {
|
|
return fmt.Errorf("CW native wave empty")
|
|
}
|
|
waves = append(waves, wave)
|
|
}
|
|
division, e := c.CapturedStage(run.DivisionId)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
info.StageId = node.StageId
|
|
info.MonsterWaveList = waves
|
|
grid := info.EIGMKOJOEEI
|
|
grid.IOCKDJCDMAH = &pb.MIPDJDJLGPL{EMOHLJHPOPC: s.EMOHLJHPOPC, DCAHJELNJNP: s.DCAHJELNJNP, SectionId: s.SectionId, CJCCMAACJDB: node.CJCCMAACJDB}
|
|
grid.IFIIOIIKJHO = branch.IFIIOIIKJHO
|
|
grid.LODNFLMBIEH = division.EnemyHardLevel
|
|
grid.BattleWaveId = waves[0].WaveId
|
|
return nil
|
|
}
|