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 }