Update GaCy-GO
This commit is contained in:
1 parent
da8a32f60d
commit
0cbe460e49
345 files changed
+98202
-77559
No files matched your search
@@ -0,0 +1,364 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user