Update AA synchronization, CW progression and diagnostics
This commit is contained in:
1 parent
67a819a4d3
commit
30bee81fe9
30 files changed
+1143
-55
No files matched your search
@@ -1,5 +1,7 @@
|
||||
-- Current Client metadata: ChallengePeakModule exposes CurrentPeakGroupID and
|
||||
-- SendGetChallengePeakDataRequest(). Request real server data; never set a
|
||||
-- SendModuleInitRequest() requests the round/group catalog. The similarly named
|
||||
-- SendGetChallengePeakDataRequest() requests the current run, not the catalog.
|
||||
-- Request real server data; never set a
|
||||
-- private group field, fake a response, remap a room or close a running mode.
|
||||
do local function synchronize()
|
||||
local data=_G.__GAC_AA_ROUND_REFRESH
|
||||
@@ -12,8 +14,8 @@ do local function synchronize()
|
||||
local module=modules and modules.ChallengePeakModule
|
||||
if not module then return end
|
||||
local state=_G.__GAC_AA_ROUND_REFRESH_STATE
|
||||
if not state or state.module~=module then
|
||||
state={module=module};_G.__GAC_AA_ROUND_REFRESH_STATE=state
|
||||
if not state or state.module~=module or state.adapter~='module-init-v2' then
|
||||
state={module=module,adapter='module-init-v2'};_G.__GAC_AA_ROUND_REFRESH_STATE=state
|
||||
end
|
||||
local current=module.CurrentPeakGroupID
|
||||
-- Wait for the native login initialization request; do not duplicate it.
|
||||
@@ -31,13 +33,17 @@ do local function synchronize()
|
||||
state.pending=nil
|
||||
end
|
||||
if state.requested==data.revision then return end
|
||||
local sender=module.SendGetChallengePeakDataRequest
|
||||
if type(sender)~='function' then error('observed AA refresh API is unavailable') end
|
||||
-- Native methods are not consistently exposed as Lua functions by xLua.
|
||||
-- Resolve this exact observed zero-argument method on the live module type.
|
||||
local sender=module:GetType():GetMethod('SendModuleInitRequest',flags)
|
||||
if not sender or sender:GetParameters().Length~=0 then error('observed AA refresh API is unavailable') end
|
||||
-- No known in-flight query is exposed by this module. Send once and wait
|
||||
-- for its actual CurrentPeakGroupID change, instead of flooding retries.
|
||||
local args=CS.System.Array.CreateInstance(typeof(CS.System.Object),0)
|
||||
local invokeFlags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),0)
|
||||
sender:Invoke(module,invokeFlags,nil,args,nil)
|
||||
state.requested=data.revision
|
||||
state.pending={from=current,desired=data.desired_group}
|
||||
module:SendGetChallengePeakDataRequest()
|
||||
CS.UnityEngine.Debug.Log('[GAC_AA_ROUND_SYNC] requested group='..data.desired_group..' previous='..current)
|
||||
end
|
||||
local ok,err=pcall(synchronize)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
pb "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
|
||||
// cwEncounterRewards grants the selected server-side offer, never a list
|
||||
// supplied by the battle upload. In the reference, gfRouteBattleStageInfoProto
|
||||
// projects option reward type/id/count into the branch's drop list; the battle
|
||||
// result applies that option reward on passing the threshold or a perfect clear.
|
||||
// Perfect clears additionally earn monster Orbs, which are not handled here.
|
||||
// run must be a detached snapshot which the caller discards on any error.
|
||||
func cwEncounterRewards(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, branch *pb.KKBGIPCAGGH, progress, threshold uint32) ([]*pb.KFNCAIHIHCM, error) {
|
||||
if branch == nil {
|
||||
return nil, fmt.Errorf("CW Encounter selected branch missing")
|
||||
}
|
||||
if progress < threshold {
|
||||
return nil, nil
|
||||
}
|
||||
if branch.BOAMLLHPNEC == nil {
|
||||
return nil, fmt.Errorf("CW Encounter selected rewards missing")
|
||||
}
|
||||
recipes, err := common.LoadCurrencyWarsOrbRecipes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var updates []*pb.KFNCAIHIHCM
|
||||
for _, drop := range branch.BOAMLLHPNEC.NMGIABHIJJB {
|
||||
if drop == nil || drop.Num == 0 {
|
||||
return nil, fmt.Errorf("CW Encounter reward is invalid")
|
||||
}
|
||||
var reward common.CurrencyWarsOrbReward
|
||||
switch drop.FKKGLFOHGGA {
|
||||
case pb.MJJIFGINJAJ_MJJIFGINJAJ_item_value:
|
||||
if drop.FNLMKJLKLFH != 0 {
|
||||
return nil, fmt.Errorf("CW Encounter gold identity is invalid")
|
||||
}
|
||||
reward = common.CurrencyWarsOrbReward{Reward: "Gold", Amount: drop.Num}
|
||||
case pb.MJJIFGINJAJ_MJJIFGINJAJ_exp:
|
||||
if drop.FNLMKJLKLFH != 0 {
|
||||
return nil, fmt.Errorf("CW Encounter EXP identity is invalid")
|
||||
}
|
||||
reward = common.CurrencyWarsOrbReward{Reward: "Exp", Amount: drop.Num}
|
||||
case pb.MJJIFGINJAJ_MJJIFGINJAJ_NLFCABFPJHD:
|
||||
reward = common.CurrencyWarsOrbReward{Reward: "Item", ItemID: drop.FNLMKJLKLFH, Count: drop.Num}
|
||||
default:
|
||||
return nil, fmt.Errorf("CW Encounter reward type %v is not mapped", drop.FKKGLFOHGGA)
|
||||
}
|
||||
grants, _, err := cwApplyOrbReward(c, run, recipes, reward)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
updates = append(updates, grants...)
|
||||
}
|
||||
return updates, nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
pb "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
|
||||
func TestCurrencyWarsEncounterGoldRequiresThreshold(t *testing.T) {
|
||||
c, err := common.LoadCurrencyWarsCatalog()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
player := &pb.NIHNGPKHEPC{Gold: 10}
|
||||
run := &pb.MPIMAEMCEFH{NAGDJGMMCBE: []*pb.GridFightGameInfo{
|
||||
{LEOIAOFBBFA: &pb.GridFightGameInfo_FAOCFBOFCJP{FAOCFBOFCJP: player}},
|
||||
{LEOIAOFBBFA: &pb.GridFightGameInfo_EJIPINIKNCK{EJIPINIKNCK: &pb.HKFECLCHBFJ{}}},
|
||||
{LEOIAOFBBFA: &pb.GridFightGameInfo_EBHDIOGALIE{EBHDIOGALIE: &pb.POCJDMMANBM{}}},
|
||||
}}
|
||||
branch := &pb.KKBGIPCAGGH{BOAMLLHPNEC: &pb.GridFightDropInfo{NMGIABHIJJB: []*pb.BCCONKJHDJL{{FKKGLFOHGGA: pb.MJJIFGINJAJ_MJJIFGINJAJ_item_value, Num: 6}}}}
|
||||
updates, err := cwEncounterRewards(c, run, branch, 100, 70)
|
||||
if err != nil || len(updates) == 0 || player.Gold != 16 {
|
||||
t.Fatalf("perfect reward: gold=%d updates=%d err=%v", player.Gold, len(updates), err)
|
||||
}
|
||||
_, err = cwEncounterRewards(c, run, branch, 80, 70)
|
||||
if err != nil || player.Gold != 22 {
|
||||
t.Fatal("passing the threshold must award the selected option")
|
||||
}
|
||||
_, err = cwEncounterRewards(c, run, branch, 60, 70)
|
||||
if err != nil || player.Gold != 22 {
|
||||
t.Fatal("failing the threshold must not award the selected option")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrencyWarsEncounterExperienceUpdatesLevel(t *testing.T) {
|
||||
c, err := common.LoadCurrencyWarsCatalog()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(c.PlayerLevels) < 2 {
|
||||
t.Fatal("captured player levels missing")
|
||||
}
|
||||
first := c.PlayerLevels[0]
|
||||
player := &pb.NIHNGPKHEPC{EHNJDIDABHD: first.Level, AFFCFLFIFPH: first.AvatarMaxNumber, NDAINFLOLPJ: c.PlayerLevels[len(c.PlayerLevels)-1].Level}
|
||||
run := &pb.MPIMAEMCEFH{NAGDJGMMCBE: []*pb.GridFightGameInfo{
|
||||
{LEOIAOFBBFA: &pb.GridFightGameInfo_FAOCFBOFCJP{FAOCFBOFCJP: player}},
|
||||
{LEOIAOFBBFA: &pb.GridFightGameInfo_EBHDIOGALIE{EBHDIOGALIE: &pb.POCJDMMANBM{}}},
|
||||
}}
|
||||
branch := &pb.KKBGIPCAGGH{BOAMLLHPNEC: &pb.GridFightDropInfo{NMGIABHIJJB: []*pb.BCCONKJHDJL{{FKKGLFOHGGA: pb.MJJIFGINJAJ_MJJIFGINJAJ_exp, Num: first.LevelUpExp}}}}
|
||||
updates, err := cwEncounterRewards(c, run, branch, cwPercentScale, 0)
|
||||
if err != nil || player.EHNJDIDABHD <= first.Level || len(updates) == 0 {
|
||||
t.Fatalf("EXP reward: player=%v updates=%d err=%v", player, len(updates), err)
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"sort"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
pb "gac-hkrpg-go/protocol/proto"
|
||||
@@ -19,18 +20,19 @@ type cwReferenceEnemies struct {
|
||||
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"`
|
||||
NeutralCampCompatibility string `json:"neutral_camp_compatibility"`
|
||||
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"`
|
||||
@@ -89,7 +91,7 @@ func cwGenerateReferenceEnemyLayer(c *common.CurrencyWarsCatalog, run *pb.MPIMAE
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if display == nil || (t.NodeType != "Boss" && t.NodeType != "CampMonster") || display.StageId != t.StageID {
|
||||
if display == nil || (t.NodeType != "Boss" && t.NodeType != "CampMonster" && t.NodeType != "Monster") || display.StageId != t.StageID {
|
||||
return nil, fmt.Errorf("CW reference enemy factory does not support node type %s", t.NodeType)
|
||||
}
|
||||
r, e := cwLoadReferenceEnemies(c)
|
||||
@@ -99,6 +101,9 @@ func cwGenerateReferenceEnemyLayer(c *common.CurrencyWarsCatalog, run *pb.MPIMAE
|
||||
if t.NodeType == "CampMonster" {
|
||||
return cwGenerateReferenceCampLayer(c, run, s, t, r)
|
||||
}
|
||||
if t.NodeType == "Monster" {
|
||||
return cwGenerateReferenceMonsterLayer(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")
|
||||
}
|
||||
@@ -140,6 +145,84 @@ func cwGenerateReferenceEnemyLayer(c *common.CurrencyWarsCatalog, run *pb.MPIMAE
|
||||
return &pb.NCKOKDHNJDC{EFECLPJFOCM: &pb.NCKOKDHNJDC_MPOBBIPMNNJ{MPOBBIPMNNJ: layer}}, nil
|
||||
}
|
||||
|
||||
// The original server's neutral camp is a run-owned field absent from our
|
||||
// captured wire. Until that identity can be restored from the Client, select
|
||||
// one eligible captured season camp deterministically per run. This is the
|
||||
// declared compatibility policy, not a claim about the original camp roll.
|
||||
func cwGenerateReferenceMonsterLayer(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, s *pb.BEEFLEJAOCN, t common.CurrencyWarsReferenceNodeTemplate, r *cwReferenceEnemies) (*pb.NCKOKDHNJDC, error) {
|
||||
if run == nil || run.UniqueId == 0 || s == nil || s.DCAHJELNJNP == 0 || r.Policy.NeutralCampCompatibility != "stable_random_enabled_season_camp_by_run_id" {
|
||||
return nil, fmt.Errorf("CW neutral camp compatibility policy or run identity missing")
|
||||
}
|
||||
type eligible struct {
|
||||
id uint32
|
||||
monsters []uint32
|
||||
}
|
||||
var camps []eligible
|
||||
for _, camp := range c.MonsterCamps {
|
||||
if camp.SeasonID != run.LJNGPCKPJLF || camp.Common.IfRandomEnabled == 0 {
|
||||
continue
|
||||
}
|
||||
var monsters []uint32
|
||||
for _, id := range camp.Season.MonsterList {
|
||||
if tier, ok := r.MonsterTiers[id]; ok && tier > 0 && tier <= r.Policy.MonsterMaxTier {
|
||||
monsters = append(monsters, id)
|
||||
}
|
||||
}
|
||||
if len(monsters) != 0 {
|
||||
camps = append(camps, eligible{camp.ID, monsters})
|
||||
}
|
||||
}
|
||||
if len(camps) == 0 {
|
||||
return nil, fmt.Errorf("CW no captured neutral camp with eligible monsters")
|
||||
}
|
||||
sort.Slice(camps, func(i, j int) bool { return camps[i].id < camps[j].id })
|
||||
chosen := camps[uint64(run.UniqueId)%uint64(len(camps))]
|
||||
previous, err := cwReferenceEarlierCount(c, s, "Monster", false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
count := r.Policy.LaterMonsterCount
|
||||
if previous == 0 {
|
||||
count = r.Policy.FirstMonsterCount
|
||||
}
|
||||
if count == 0 || len(r.Policy.ChapterStarWeights) == 0 {
|
||||
return nil, fmt.Errorf("CW neutral monster policy incomplete")
|
||||
}
|
||||
chapter := int(s.DCAHJELNJNP) - 1
|
||||
if chapter >= len(r.Policy.ChapterStarWeights) {
|
||||
chapter = len(r.Policy.ChapterStarWeights) - 1
|
||||
}
|
||||
weights := r.Policy.ChapterStarWeights[chapter]
|
||||
var total uint32
|
||||
for _, weight := range weights {
|
||||
if ^uint32(0)-total < weight {
|
||||
return nil, fmt.Errorf("CW monster star weights overflow")
|
||||
}
|
||||
total += weight
|
||||
}
|
||||
if total == 0 {
|
||||
return nil, fmt.Errorf("CW monster star weights empty")
|
||||
}
|
||||
wave := &pb.PNAKHANCNGK{OCPEPEMNNEK: 1}
|
||||
for i := uint32(0); i < count; i++ {
|
||||
roll := rand.Uint32N(total)
|
||||
var star uint32
|
||||
for j, weight := range weights {
|
||||
if roll < weight {
|
||||
star = uint32(j + 1)
|
||||
break
|
||||
}
|
||||
roll -= weight
|
||||
}
|
||||
if star == 0 {
|
||||
return nil, fmt.Errorf("CW monster star roll unresolved")
|
||||
}
|
||||
wave.PBLBBDBDLEK = append(wave.PBLBBDBDLEK, &pb.AHKDPCIIELA{MonsterId: chosen.monsters[rand.IntN(len(chosen.monsters))], RoleStar: star})
|
||||
}
|
||||
branch := &pb.KKBGIPCAGGH{IFIIOIIKJHO: t.PenaltyBonusRuleID, MonsterWaveList: []*pb.PNAKHANCNGK{wave}}
|
||||
return &pb.NCKOKDHNJDC{EFECLPJFOCM: &pb.NCKOKDHNJDC_MPOBBIPMNNJ{MPOBBIPMNNJ: &pb.MDJBBILMHLC{LNDKCPBJDKH: chosen.id, MOADBALHMFD: 1, BPDABHGFBFO: []*pb.KKBGIPCAGGH{branch}}}}, nil
|
||||
}
|
||||
|
||||
func cwReferenceEarlierCount(c *common.CurrencyWarsCatalog, s *pb.BEEFLEJAOCN, kind string, sameChapter bool) (uint32, error) {
|
||||
rows, e := c.RouteNodes(s.EMOHLJHPOPC)
|
||||
if e != nil {
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
pb "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
|
||||
func TestCurrencyWarsGeneratedMonsterUsesOneStableCapturedCamp(t *testing.T) {
|
||||
c, err := common.LoadCurrencyWarsCatalog()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
refs, err := common.LoadCurrencyWarsReferenceNodes(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
policy, err := cwLoadReferenceEnemies(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var row common.CurrencyWarsRouteNode
|
||||
var tmpl common.CurrencyWarsReferenceNodeTemplate
|
||||
for _, candidate := range c.StageRoutes {
|
||||
node, e := refs.Template(candidate.NodeTemplateID)
|
||||
if e == nil && node.NodeType == "Monster" {
|
||||
row, tmpl = candidate, node
|
||||
break
|
||||
}
|
||||
}
|
||||
if row.RouteID == 0 {
|
||||
t.Fatal("captured route has no Monster node")
|
||||
}
|
||||
run := &pb.MPIMAEMCEFH{UniqueId: 42, LJNGPCKPJLF: c.ActiveSeasonID}
|
||||
stage := &pb.BEEFLEJAOCN{EMOHLJHPOPC: row.RouteID, DCAHJELNJNP: row.ChapterID, SectionId: row.SectionID}
|
||||
first, err := cwGenerateReferenceMonsterLayer(c, run, stage, tmpl, policy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
again, err := cwGenerateReferenceMonsterLayer(c, run, stage, tmpl, policy)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
layer, other := first.GetMPOBBIPMNNJ(), again.GetMPOBBIPMNNJ()
|
||||
if layer == nil || other == nil || layer.LNDKCPBJDKH != other.LNDKCPBJDKH || layer.MOADBALHMFD != 1 || len(layer.BPDABHGFBFO) != 1 {
|
||||
t.Fatal("monster camp or branch state differs")
|
||||
}
|
||||
camp, err := c.MonsterCamp(run.LJNGPCKPJLF, layer.LNDKCPBJDKH)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
allowed := map[uint32]bool{}
|
||||
for _, id := range camp.Season.MonsterList {
|
||||
allowed[id] = true
|
||||
}
|
||||
branch := layer.BPDABHGFBFO[0]
|
||||
if branch.IFIIOIIKJHO != tmpl.PenaltyBonusRuleID || len(branch.MonsterWaveList) != 1 {
|
||||
t.Fatal("monster wave or penalty differs")
|
||||
}
|
||||
previous, err := cwReferenceEarlierCount(c, stage, "Monster", false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := policy.Policy.LaterMonsterCount
|
||||
if previous == 0 {
|
||||
want = policy.Policy.FirstMonsterCount
|
||||
}
|
||||
if len(branch.MonsterWaveList[0].PBLBBDBDLEK) != int(want) {
|
||||
t.Fatal("monster count differs from policy")
|
||||
}
|
||||
for _, m := range branch.MonsterWaveList[0].PBLBBDBDLEK {
|
||||
if !allowed[m.MonsterId] || policy.MonsterTiers[m.MonsterId] == 0 || policy.MonsterTiers[m.MonsterId] > policy.Policy.MonsterMaxTier || m.RoleStar == 0 {
|
||||
t.Fatal("monster is outside captured eligible camp")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,12 @@ func cwAdvanceRouteNode(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH
|
||||
shadowStage.DCAHJELNJNP = next.ChapterID
|
||||
shadowStage.SectionId = next.SectionID
|
||||
shadowStage.IMPNAHBCADN = nil
|
||||
layer, err := cwGenerateReferenceEnemyLayer(catalog, shadow, display)
|
||||
var layer *pb.NCKOKDHNJDC
|
||||
if template.NodeType == "Supply" {
|
||||
layer, err = cwCapturedSupplyLayer(catalog)
|
||||
} else {
|
||||
layer, err = cwGenerateReferenceEnemyLayer(catalog, shadow, display)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -101,6 +106,17 @@ func cwGeneratedRoundPreparation(catalog *common.CurrencyWarsCatalog, run *pb.MP
|
||||
if queue == math.MaxUint32 {
|
||||
return nil, fmt.Errorf("CW pending queue exhausted")
|
||||
}
|
||||
name, err := catalog.NodeTypeName(display.CJCCMAACJDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if name == "Supply" {
|
||||
offer, err := cwGenerateSupplyOffer(catalog, run)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.CHCHAJIANIM{QueuePosition: queue + 1, FGLIODAJFML: &pb.CHCHAJIANIM_FIKAGBAEOAP{FIKAGBAEOAP: offer}}, nil
|
||||
}
|
||||
if display.HPNNLMBHOPF {
|
||||
offer, err := cwSpotlightChoices(catalog, run)
|
||||
if err != nil {
|
||||
@@ -108,10 +124,6 @@ func cwGeneratedRoundPreparation(catalog *common.CurrencyWarsCatalog, run *pb.MP
|
||||
}
|
||||
return &pb.CHCHAJIANIM{QueuePosition: queue + 1, FGLIODAJFML: &pb.CHCHAJIANIM_EEKMIAMGHFL{EEKMIAMGHFL: offer}}, nil
|
||||
}
|
||||
name, err := catalog.NodeTypeName(display.CJCCMAACJDB)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if name == "EliteBranch" && stage.IMPNAHBCADN.GetMPOBBIPMNNJ().MOADBALHMFD == 0 {
|
||||
return &pb.CHCHAJIANIM{QueuePosition: queue + 1, FGLIODAJFML: &pb.CHCHAJIANIM_BAKAKODKECM{BAKAKODKECM: &pb.HNPHLAHJPOB{}}}, nil
|
||||
}
|
||||
|
||||
@@ -95,12 +95,14 @@ func finishCurrencyWarsBattle(s *Session, req *pb.PVEBattleResultCsReq, current
|
||||
// base display rule. Validate against the battle we actually issued before
|
||||
// using that rule for HP/rewards or selecting the next transition.
|
||||
penaltyID := node.IFIIOIIKJHO
|
||||
var selectedBranch *pb.KKBGIPCAGGH
|
||||
if layer := stage.GetIMPNAHBCADN().GetMPOBBIPMNNJ(); layer != nil && len(layer.BPDABHGFBFO) > 0 {
|
||||
selected, e := cwSelectedEnemyBranch(layer)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
penaltyID = selected.IFIIOIIKJHO
|
||||
selectedBranch = selected
|
||||
if current.GetEIGMKOJOEEI().GetIFIIOIIKJHO() != penaltyID {
|
||||
return fmt.Errorf("CW selected Encounter penalty differs from issued battle")
|
||||
}
|
||||
@@ -277,6 +279,13 @@ func finishCurrencyWarsBattle(s *Session, req *pb.PVEBattleResultCsReq, current
|
||||
if err = cwApplyBaseExperience(catalog, player, shop, uint32(gain)); err != nil {
|
||||
return err
|
||||
}
|
||||
var encounterUpdates []*pb.KFNCAIHIHCM
|
||||
if kind == "EliteBranch" {
|
||||
encounterUpdates, err = cwEncounterRewards(catalog, run, selectedBranch, progress, penalty.ThresholdPosition)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
damage := &pb.BHBAILHMCOI{}
|
||||
frontMax, err := catalog.IntegerRule("GridFight_Front_AvatarMaxNum")
|
||||
if err != nil {
|
||||
@@ -338,6 +347,7 @@ func finishCurrencyWarsBattle(s *Session, req *pb.PVEBattleResultCsReq, current
|
||||
}
|
||||
CURRENT_BATTLE.Delete(s.Token) // Duplicate uploads cannot award again.
|
||||
update := cwNotify(cwUnlock(), cwGoldSync(player.Gold), cwCapacitySync(player), &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_GridFightLineupHp{GridFightLineupHp: &pb.FMJDDIHBELC{PPKKHHNEOHB: player.PPKKHHNEOHB, GridFightLineupHp: player.GridFightLineupHp}}}, &pb.KFNCAIHIHCM{MNLCHEJKDHO: &pb.KFNCAIHIHCM_PLPHEIBLPDL{PLPHEIBLPDL: &pb.PIHKLPJGEPM{Level: plan.Level, Exp: plan.Exp, HFMKLAPEKFA: player.NDAINFLOLPJ}}})
|
||||
update.GCPNIHHBGGD[0].JKPJFKCBIEM = append(update.GCPNIHHBGGD[0].JKPJFKCBIEM, encounterUpdates...)
|
||||
if err = s.Send(update); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -158,7 +158,21 @@ func cwSelectSupply(catalog *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, re
|
||||
}
|
||||
}
|
||||
if next == nil {
|
||||
return nil, fmt.Errorf("CW Supply next node not captured")
|
||||
// Generated routes do not necessarily have a captured next-node wire.
|
||||
// Keep the selected rewards on this detached run and let the common
|
||||
// route factory validate and construct the actual next node. If that
|
||||
// node family is not supported yet, the caller discards the snapshot.
|
||||
advance, err := cwAdvanceRouteNode(catalog, run)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CW Supply next node: %w", err)
|
||||
}
|
||||
if len(advance.GCPNIHHBGGD) != 1 || len(advance.GCPNIHHBGGD[0].JKPJFKCBIEM) < 1 {
|
||||
return nil, fmt.Errorf("CW Supply advance notification incomplete")
|
||||
}
|
||||
updates := advance.GCPNIHHBGGD[0].JKPJFKCBIEM
|
||||
advance.GCPNIHHBGGD[0].JKPJFKCBIEM = append([]*pb.KFNCAIHIHCM{updates[0], cwUnlock()}, updates[1:]...)
|
||||
notify.GCPNIHHBGGD = append(notify.GCPNIHHBGGD, advance.GCPNIHHBGGD...)
|
||||
return notify, nil
|
||||
}
|
||||
level := new(pb.AINFNFIDGFF)
|
||||
nextPending := new(pb.CHCHAJIANIM)
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"sort"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
pb "gac-hkrpg-go/protocol/proto"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// cwGenerateSupplyOffer follows the Supply option factory in the reference
|
||||
// executable identified in docs/cw-route-continuation-audit.md. The protocol
|
||||
// envelope comes from the native capture; role and equipment IDs must also be
|
||||
// present in this client's captured data.
|
||||
func cwGenerateSupplyOffer(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH) (*pb.IAKEDFLIOFD, error) {
|
||||
stage, _ := cwStage(run)
|
||||
if stage == nil || stage.DCAHJELNJNP == 0 {
|
||||
return nil, fmt.Errorf("CW Supply chapter missing")
|
||||
}
|
||||
runtime, err := cwBonfireRuntime(c, run)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cwGenerateSupplyOfferWithRules(c, run, runtime.Rules)
|
||||
}
|
||||
|
||||
func cwGenerateSupplyOfferWithRules(c *common.CurrencyWarsCatalog, run *pb.MPIMAEMCEFH, rules map[string]uint32) (*pb.IAKEDFLIOFD, error) {
|
||||
stage, _ := cwStage(run)
|
||||
if stage == nil || stage.DCAHJELNJNP == 0 {
|
||||
return nil, fmt.Errorf("CW Supply chapter missing")
|
||||
}
|
||||
// Box modifies a different part of the selection flow; do not silently
|
||||
// grant an ordinary selection if the corresponding action is not mapped.
|
||||
if rules["SupplyBox"] != 0 {
|
||||
return nil, fmt.Errorf("CW SupplyBox action is not mapped")
|
||||
}
|
||||
count, err := c.IntegerRule("GridFight_BaseSupplyRoleNum")
|
||||
// gfFxHProjectRunState projects ExtraSupplyOption into the field added by
|
||||
// gfPendingFillSupplyOptions to the Client's captured base count.
|
||||
if err != nil || count <= 0 || uint64(count)+uint64(rules["ExtraSupplyOption"]) > uint64(len(c.CapturedRoles)) {
|
||||
return nil, fmt.Errorf("CW Supply option count invalid")
|
||||
}
|
||||
count += int64(rules["ExtraSupplyOption"])
|
||||
star := rules["SupplyStar"]
|
||||
if star == 0 {
|
||||
star = 1
|
||||
} // gfPendingSupplyOf clamps this rule to one.
|
||||
category := "Basic"
|
||||
if rules["SupplyEmblems"] != 0 {
|
||||
category = "Emblem"
|
||||
} else if stage.DCAHJELNJNP >= 3 {
|
||||
category = "Craftable"
|
||||
}
|
||||
recipes, err := common.LoadCurrencyWarsOrbRecipes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gear, err := common.LoadCurrencyWarsEquipment(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var equipment []uint32
|
||||
for _, item := range recipes.Equipment {
|
||||
if item.Category == category {
|
||||
if _, ok := gear[item.ID]; !ok {
|
||||
return nil, fmt.Errorf("CW Supply equipment %d is not captured", item.ID)
|
||||
}
|
||||
equipment = append(equipment, item.ID)
|
||||
}
|
||||
}
|
||||
if len(equipment) == 0 {
|
||||
return nil, fmt.Errorf("CW Supply equipment category %s is empty", category)
|
||||
}
|
||||
sort.Slice(equipment, func(i, j int) bool { return equipment[i] < equipment[j] })
|
||||
pool, err := c.NewCapturedCardPool()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
byRarity := make(map[uint32][]uint32)
|
||||
for id, item := range pool.Roles {
|
||||
if item.Disabled || item.Remaining == 0 {
|
||||
continue
|
||||
}
|
||||
if _, err := c.CapturedRoleVariant(id, star); err != nil {
|
||||
continue
|
||||
}
|
||||
byRarity[item.Rarity] = append(byRarity[item.Rarity], id)
|
||||
}
|
||||
for _, ids := range byRarity {
|
||||
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||||
}
|
||||
entries, err := cwNodeTransitions(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var offer *pb.IAKEDFLIOFD
|
||||
for _, entry := range entries {
|
||||
pending := new(pb.CHCHAJIANIM)
|
||||
if err := proto.Unmarshal(entry.RoundWire, pending); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if captured := pending.GetFIKAGBAEOAP(); captured != nil {
|
||||
candidate := proto.Clone(captured).(*pb.IAKEDFLIOFD)
|
||||
candidate.MFBAKLCBPCJ = nil
|
||||
if offer != nil && !proto.Equal(offer, candidate) {
|
||||
return nil, fmt.Errorf("CW Supply envelopes differ")
|
||||
}
|
||||
offer = candidate
|
||||
}
|
||||
}
|
||||
if offer == nil {
|
||||
return nil, fmt.Errorf("CW Supply envelope not captured")
|
||||
}
|
||||
used := map[uint32]bool{}
|
||||
for i := int64(0); i < int64(count); i++ {
|
||||
var id uint32
|
||||
for attempt := 0; attempt < 8; attempt++ {
|
||||
rarity := rules["SupplyRarity"]
|
||||
if rarity == 0 {
|
||||
rarity = min(uint32(rand.IntN(int(stage.DCAHJELNJNP+2)))+1, uint32(5))
|
||||
}
|
||||
// gfPoolRandomRoleFiltered searches the requested rarity first,
|
||||
// then neighbouring rarity buckets at increasing distance.
|
||||
for distance := uint32(0); distance < 5 && id == 0; distance++ {
|
||||
for _, candidate := range []int64{int64(rarity) - int64(distance), int64(rarity) + int64(distance)} {
|
||||
if candidate < 1 || candidate > 5 {
|
||||
continue
|
||||
}
|
||||
ids := byRarity[uint32(candidate)]
|
||||
if len(ids) != 0 {
|
||||
id = ids[rand.IntN(len(ids))]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if id != 0 && !used[id] {
|
||||
break
|
||||
}
|
||||
id = 0
|
||||
}
|
||||
if id == 0 {
|
||||
continue
|
||||
}
|
||||
used[id] = true
|
||||
item := equipment[rand.IntN(len(equipment))]
|
||||
offer.MFBAKLCBPCJ = append(offer.MFBAKLCBPCJ, &pb.NBGDEFLFLMI{RoleId: id, OPPFPECOBEF: &pb.BCCONKJHDJL{FKKGLFOHGGA: pb.MJJIFGINJAJ_MJJIFGINJAJ_BDPIMPJOJBK, FNLMKJLKLFH: id, Num: 1, DisplayValue: &pb.AOHKKFHDALO{NEBNJDMFELO: &pb.AOHKKFHDALO_RoleStar{RoleStar: star}}}, GridFightItemList: []uint32{item}, DHLBEFLOOPM: []*pb.BCCONKJHDJL{{FKKGLFOHGGA: pb.MJJIFGINJAJ_MJJIFGINJAJ_NLFCABFPJHD, FNLMKJLKLFH: item, Num: 1}}})
|
||||
}
|
||||
if offer.DAEMEBCKHIK == 0 || uint64(len(offer.MFBAKLCBPCJ)) < uint64(offer.DAEMEBCKHIK) {
|
||||
return nil, fmt.Errorf("CW Supply has too few valid role options")
|
||||
}
|
||||
chance, item := rules["SupplyEquipChance"], rules["SupplyEquip"]
|
||||
if chance > 100 {
|
||||
return nil, fmt.Errorf("CW Supply equipment chance invalid")
|
||||
}
|
||||
if chance != 0 && item != 0 && stage.DCAHJELNJNP >= rules["SupplyEquipChapter"] {
|
||||
if _, ok := gear[item]; !ok {
|
||||
return nil, fmt.Errorf("CW Supply bonus equipment %d is not captured", item)
|
||||
}
|
||||
if uint32(rand.IntN(100)) < chance {
|
||||
choice := offer.MFBAKLCBPCJ[rand.IntN(len(offer.MFBAKLCBPCJ))]
|
||||
choice.GridFightItemList[0] = item
|
||||
choice.DHLBEFLOOPM[0].FNLMKJLKLFH = item
|
||||
}
|
||||
}
|
||||
return offer, nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package gameserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
pb "gac-hkrpg-go/protocol/proto"
|
||||
)
|
||||
|
||||
func TestCurrencyWarsSupplyOffersUseCapturedRolesAndEquipment(t *testing.T) {
|
||||
c, err := common.LoadCurrencyWarsCatalog()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
run := &pb.MPIMAEMCEFH{NAGDJGMMCBE: []*pb.GridFightGameInfo{{LEOIAOFBBFA: &pb.GridFightGameInfo_KLCPAAHDPJF{KLCPAAHDPJF: &pb.BEEFLEJAOCN{DCAHJELNJNP: 2}}}}}
|
||||
gear, err := common.LoadCurrencyWarsEquipment(c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for n := 0; n < 10; n++ {
|
||||
offer, err := cwGenerateSupplyOfferWithRules(c, run, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if offer.DAEMEBCKHIK == 0 || len(offer.MFBAKLCBPCJ) < int(offer.DAEMEBCKHIK) {
|
||||
t.Fatal("Supply has too few options")
|
||||
}
|
||||
used := map[uint32]bool{}
|
||||
for _, item := range offer.MFBAKLCBPCJ {
|
||||
if item == nil || used[item.RoleId] {
|
||||
t.Fatal("Supply role is missing or duplicated")
|
||||
}
|
||||
used[item.RoleId] = true
|
||||
drop := item.GetOPPFPECOBEF()
|
||||
if drop == nil || drop.FNLMKJLKLFH != item.RoleId {
|
||||
t.Fatal("Supply role display differs")
|
||||
}
|
||||
if _, err := c.CapturedRoleVariant(item.RoleId, drop.GetDisplayValue().GetRoleStar()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(item.GridFightItemList) != 1 || len(item.DHLBEFLOOPM) != 1 || item.GridFightItemList[0] != item.DHLBEFLOOPM[0].FNLMKJLKLFH {
|
||||
t.Fatal("Supply gear display differs")
|
||||
}
|
||||
if _, ok := gear[item.GridFightItemList[0]]; !ok {
|
||||
t.Fatal("Supply gear is not captured")
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, err := cwGenerateSupplyOfferWithRules(c, run, map[string]uint32{"ExtraSupplyOption": ^uint32(0)}); err == nil {
|
||||
t.Fatal("excessive Supply option count must be rejected")
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,17 @@ func getChallengeData() *ChallengeDataConfig {
|
||||
Log("Failed to parse challenge_data.json: " + err.Error())
|
||||
challengeData = ChallengeDataConfig{}
|
||||
}
|
||||
// The imported catalog stores Peak rows under peak.stages; the flat
|
||||
// resource overlays below add Client-specific placement/group data.
|
||||
// Do not turn the section names "stages" and "groups" into fake PeakIDs.
|
||||
var imported struct {
|
||||
Peak struct {
|
||||
Stages map[string]PeakStageData `json:"stages"`
|
||||
} `json:"peak"`
|
||||
}
|
||||
if json.Unmarshal(raw, &imported) == nil && len(imported.Peak.Stages) != 0 {
|
||||
challengeData.Peak = imported.Peak.Stages
|
||||
}
|
||||
challengeData.Countdowns = map[uint32]uint32{}
|
||||
challengeData.TargetRules = map[uint32]ChallengeTargetRule{}
|
||||
if raw, err := common.ReadResourceFile("resources/ChallengeTargetConfig.json"); err == nil {
|
||||
|
||||
@@ -2,6 +2,8 @@ package gameserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gac-hkrpg-go/internal/common"
|
||||
@@ -50,6 +52,11 @@ func init() {
|
||||
|
||||
var finalData []byte
|
||||
finalData = append(finalData, heartbeatLua...)
|
||||
if dir, err := os.Getwd(); err == nil {
|
||||
finalData = append(finalData, []byte("\n_G.__GAC_DIAGNOSTICS_DIR = "+luaQuotedString(filepath.Join(dir, "logs"))+"\n")...)
|
||||
} else {
|
||||
PacketLog("Client diagnostics directory unavailable: " + err.Error())
|
||||
}
|
||||
if s.publicSDKURL != "" {
|
||||
finalData = append(finalData, []byte("\n_G.__GAC_SRTOOL_LIVE_URL = "+luaQuotedString(s.publicSDKURL)+"\n")...)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package gameserver
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPeakSceneCatalogMatchesClientGroups(t *testing.T) {
|
||||
chdirRepositoryRoot(t)
|
||||
data := getChallengeData()
|
||||
if len(data.Peak) == 0 {
|
||||
t.Fatal("no AA stages loaded")
|
||||
}
|
||||
for id, stage := range data.Peak {
|
||||
if stage.GroupID == 0 || stage.Entrance == 0 || stage.Group == 0 || stage.MonsterID == 0 {
|
||||
t.Errorf("stage %s has incomplete placement", id)
|
||||
}
|
||||
if stage.SceneResourceError != "" {
|
||||
t.Errorf("stage %s rejected: %s", id, stage.SceneResourceError)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user