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

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
}