Update AA synchronization, CW progression and diagnostics

This commit is contained in:
CAtlas0784 committed 2026-10-30 19:19:05 +07:00
1 parent 67a819a4d3
commit 30bee81fe9
30 files changed
+1143 -55

No files matched your search

+4
View File
@@ -254,6 +254,8 @@ Damage Analyst รับ snapshot ระหว่างสู้จาก `Battl
| `battle_last_capture.json`, `battle_diagnostics/` | CmdID, เวลา, SHA-256 และ snapshot ของแต่ละรอบ |
| `Player.log` ของ Client | Exception และจุดที่ Client โหลดข้อมูลไม่สำเร็จ |
HideUI และ FaceCam เขียนข้อมูลวินิจฉัยลง `logs/hide-ui.log` และ `logs/facecam.log` ใน working directory ของ Game server (สำหรับ build `ship` คือโฟลเดอร์ข้าง executable) และยังส่งข้อความเข้า `Player.log` ด้วย หาก Client เขียนโฟลเดอร์นั้นไม่ได้จะบันทึก error ใน `Player.log` สคริปต์ทั้งสองรวมใน build `ship`; การเขียนไฟล์จริงยังต้องตรวจจาก Client
ตำแหน่ง `Player.log` บน Windows สำหรับ Client ที่ใช้ในงานนี้:
```text
@@ -424,6 +426,8 @@ Damage Analyst ใช้ `StatisticsComponent` ของ Battle ปัจจุ
AA cycle: แยกเวลาจำกัดของ Peak ออกจากตาราง MoC แล้ว โดยใช้ค่าที่ capture จาก Client ใน `resources/PeakBattleConfig.json` ทั้ง Start และ Restart; เก็บแพ็กเก็ตผลเต็มเพื่อจับคู่ตัวนับ AA เพิ่มเติม การแก้เลข cycle/ดาวในหน้าผลยังรอตรวจ Client รอบใหม่ ดู [docs/peak-cycle-investigation.md](docs/peak-cycle-investigation.md)
AA round sync (2026-10-30): การเลือกรอบจาก Srtool รีเฟรชรายการผ่าน native `ChallengePeakModule.SendModuleInitRequest` หลังออกจาก Battle/โหมดที่กำลังเล่น โดยไม่แก้ PeakID ของห้องที่กด Client log ยืนยันรับรอบ 3 แล้ว แก้ key ด่านใน scene override ที่ผิดจาก resource และการอ่าน `peak.stages`; ตรวจข้อมูลฉากครบ 44 ด่านผ่าน และผู้ใช้ยืนยันว่าเปลี่ยนรอบจาก Srtool และเข้าเล่นได้แล้ว ดูหลักฐานและข้อจำกัดใน [docs/aa-round-sync.md](docs/aa-round-sync.md)
`GetFriendRecommendLineup` ส่งทีม Prydwen สำหรับ MoC, Pure Fiction, Apocalyptic Shadow และ Anomaly Arbitration โดย resolve AvatarID จาก `resources/AvatarConfig.json` ของ Client ปัจจุบัน และอ่าน Peak IDs ของ AA จาก `ChallengePeakGroupConfig.json` ข้อมูล snapshot แหล่งที่มา และข้อจำกัดของ enum ที่ยัง obfuscate อยู่ระบุใน [docs/prydwen-endgame-recommendations.md](docs/prydwen-endgame-recommendations.md) อัปเดตทุกโหมดด้วย `python tools/update_prydwen_endgame_teams.py` หรือเลือกโหมดด้วย `--mode` ต้องตรวจ route กับหน้าใน Client จริงก่อนยืนยันผล
### CW Preparation และคำสั่งจัดการรอบ (2026-10-20)
+1 -1
View File
@@ -14,7 +14,7 @@ import (
// Keep the ship build self-contained. Probes and generated Lua files are
// deliberately excluded; only the maintained client scripts are embedded.
//
//go:embed resources res.json teleports.json challenge_data.json mission_unlocks.json server.json freesr-data.json persistent hotfix.json versions.json scripts/damage_analyst_live.lua scripts/freecam.lua scripts/hide_ui.lua scripts/navigation_profiles.lua scripts/ping_rainbow.lua
//go:embed resources res.json teleports.json challenge_data.json mission_unlocks.json server.json freesr-data.json persistent hotfix.json versions.json scripts/damage_analyst_live.lua scripts/fps_settings_120.lua scripts/freecam.lua scripts/hide_ui.lua scripts/navigation_profiles.lua scripts/ping_rainbow.lua
var embedded embed.FS
var seedFiles = []string{"server.json", "freesr-data.json", "persistent", "hotfix.json", "versions.json"}
+19 -3
View File
@@ -11,7 +11,7 @@ The protocol's `ChallengePeakGroupDataUpdateScNotify` contains one `ChallengePea
## Source changes
- `internal/gameserver/aa_round_sync.go` builds a resource-validated desired group and selected-group/version digest. The default comes from the same loaded Peak groups as the data handler. It reads selection without writing saves.
- `aa_round_sync.lua` uses the actual current Client's `ChallengePeakModule.SendGetChallengePeakDataRequest()` and observes `CurrentPeakGroupID`. It sends one outstanding request and waits for the native value to match. It does not fabricate a response, alter private state, close a page, change scene, start a Battle, or remap a PeakID.
- `aa_round_sync.lua` uses the actual current Client's `ChallengePeakModule.SendModuleInitRequest()` and observes `CurrentPeakGroupID`. It resolves the exact zero-argument method through reflection because the xLua bridge does not consistently expose native methods as Lua functions. It sends one outstanding request and waits for the native value to match. It does not fabricate a response, alter private state, close a page, change scene, start a Battle, or remap a PeakID.
- Server gating defers while `CURRENT_BATTLE`, `ChallengeRun`, or CW is active; the Client also checks `BattleGamePhase`. Existing native login initialization (`CurrentPeakGroupID == 0`) is allowed to finish without issuing a duplicate request.
- `GetChallengePeakData` retains the existing Peak run's group until that run is left. It logs the actual selected group, number of groups and active-run state. Missing Peak resources now produce a typed error rather than a fabricated group/room list.
- Server README and Thai/English Srtool copy describe deferred native refresh rather than claiming that reopening the page necessarily causes a data request.
@@ -25,8 +25,24 @@ UInt32 get_CurrentPeakGroupID()
Void SendGetChallengePeakDataRequest()
```
This evidence establishes the named native API, not its successful execution or UI refresh. Runtime pointers in the log are process-local and are not shipped or reused.
The earlier metadata evidence established method names only. Runtime pointers in the log are process-local and are not shipped or reused.
`go build ./internal/gameserver` and `node --check ../GaC-srtool/js/i18n.js` passed. No tests, save mutations, executable replacement, restart or Lua staging were performed for this source change. Root must wire `loadAARoundRefreshLua(s)` into heartbeat, build/deploy, and confirm the real request/reply plus `[GAC_AA_ROUND_SYNC] applied` and the displayed round before reporting Client success.
### Runtime correction and deployment — 2026-10-30
Bounded disassembly of the live module's method pointers proved that the misleadingly named `SendGetChallengePeakDataRequest` sends `GetCurChallengePeakCsReq` (8945), which only refreshes the current run. `SendModuleInitRequest` sends `GetChallengePeakDataCsReq` (8942), the full round catalog. Names and IDs are verified against `protocol/proto/cmdid.go`; no aliases were added. Evidence is in `.runtime/aa-send-request.asm`, `aa-agent-send.asm`, `aa-init-request.asm`, and `aa-agent-init.asm`. Examined GameAssembly SHA256: `4aa08a0706f27d2cf9951b61920c179140ece9ea94f7e34422c18e46922939b6`.
After selecting group 3 through the existing SDK endpoint, real Client/server logs showed:
```
[GAC_AA_ROUND_SYNC] requested group=3 previous=1
PEAK_DATA current_group=3 groups=11 active_run=false
[GAC_AA_ROUND_SYNC] applied group=3
```
This confirms the native module accepted the selected group. After deployment the user confirmed that the AA round changed from Srtool and a room could be entered.
Separately, one malformed reference scene key (`6` in group 5) caused strict override validation to reject all AA scenes, including other rounds. It is corrected to stage 501 using the unique missing `PreLevelIDList` entry and matching `NpcMonsterIDList` actor from Client resources; coordinates, actor, entrance and maze group are unchanged. Provenance is stored in `ChallengePeakSceneOverrides.json`. The imported `peak.stages` section is now decoded correctly rather than interpreting the names `stages`/`groups` as fake stage IDs.
`TestPeakSceneCatalogMatchesClientGroups`, Peak config/limit tests and the Game server build passed. All 44 resource stage placements validate. The new executable was installed and the Game server restarted; temporary probes were removed from `scripts/`. These checks do not prove battle/scene success in the Client.
There is intentionally no timeout retry while an in-flight request cannot be distinguished from an unchanged result. A changed native group proves completion; new desired choices can then be requested. Reconnect/module replacement resets local request tracking. All activity schedules and resource room/boss definitions remain intact.
+9 -5
View File
@@ -56,8 +56,12 @@ The reference `gfRouteCreateNode` uses a run-specific neutral camp for Monster
nodes. `gfRoutePlanForRun` reads that camp from an internal FFGO run field; it
is not represented by the saved Client stage's boss camp list. The captured
first battle's monster IDs match more than one native season camp, so they do
not uniquely recover the saved run's neutral camp. The reference uses one wave
of three monsters for the first Monster node and five thereafter, filtering by
the captured maximum tier; determining the run camp is still required before
constructing that layer. EliteBranch generation additionally needs its option,
difficulty and reward contract. No player save was modified by this audit.
not uniquely recover the saved run's neutral camp. As an explicit compatibility
policy, the new Monster factory picks an eligible, random-enabled captured
season camp deterministically from the run ID. This retains one camp for the
same run but does not claim to recover the reference's original choice. The
reference wave counts, tier limit and chapter star weights come from the
revision-bound resource. A focused unit test checks stable camp selection and
valid monster membership; the generated node is not yet verified in Client.
EliteBranch generation additionally needs its option, difficulty and reward
contract. No player save was modified by this implementation.
+58 -9
View File
@@ -11,11 +11,14 @@ These names are joined from `CurrencyWarsCatalog.json` stage routes and
`CurrencyWarsReferenceNodes.json` templates. They describe the template, not
necessarily a run's dynamically substituted node display.
`cwAdvanceRouteNode` currently calls `cwGenerateReferenceEnemyLayer` for every
destination. That factory accepts only Boss and CampMonster; it rejects Supply,
EliteBranch, and Monster. `cwSelectSupply` also requires an exact captured
transition. Consequently a successful 2-1 battle does not establish that 2-2
or later rooms can be played. Constructing the remaining node layers, pending
`cwAdvanceRouteNode` now creates a captured empty Supply container and uses
`cwGenerateReferenceEnemyLayer` for other destinations. The enemy factory
accepts Boss, CampMonster and Monster; it still rejects EliteBranch. The
Monster factory uses the explicit replacement-camp policy documented in
`currency-wars-route-factory.md` because the old run's neutral camp is not
present in its saved protocol data.
Consequently a successful 2-1 battle does not establish that 2-2 or later
rooms can be played. Constructing the remaining node layers, pending
actions and rewards needs their corresponding native/reference rules and a
current-Client check; copying the first Supply's randomized inventory would not
prove the later rooms correct.
@@ -23,10 +26,56 @@ prove the later rooms correct.
`cwCapturedSupplyLayer` derives the empty Supply node container from the
revision-bound transition capture and validates that it has no camp or enemy
branches. It neither copies the captured option list nor modifies a run. It is
not yet connected to route advancement: FFGO's `gfPendingSupplyOf` and
`gfPendingFillSupplyOptions` also choose a chapter/state-dependent offer,
including distinct roles and random equipment. That pending offer remains
unmapped; advancing without it would strand the player in Supply.
now connected to route advancement. The generated RoundBegin path creates a
Supply offer from captured role variants and equipment. SupplyBox is not
implemented. Automated offer
generation checks pass; the new route is not yet verified in the Client.
## Updated FFGO Supply evidence
The user-supplied reference binary has SHA-256
`00a75ef82614977d951250705250c6ad478ef7cdde717c761f0c70244a84ed10`.
Its `gfPendingSupplyOf` reads the run rules `SupplyRarity`, `SupplyStar`,
`SupplyEmblems`, and `SupplyBox`. `gfPendingFillSupplyOptions` reads
`GridFight_BaseSupplyRoleNum`, draws roles and selects equipment from `Basic`,
`Craftable`, or `Emblem`, depending on the chapter and rules. These names were
resolved from the reference binary, not inferred from the earlier dump.
`gfFxHProjectRunState` also projects `ExtraSupplyOption` into the additional
option count consumed by `gfPendingFillSupplyOptions`; the generated offer
uses the saved run rule and checks the combined count before drawing.
The updated reference also calls `gfPendingApplySupplyEquipChance`, which
reads `SupplyEquipChance`, `SupplyEquip`, and `SupplyEquipChapter`. When the
conditions match, it can replace equipment in one randomly selected option.
The generated offer uses saved run rules and validates replacement items
against captured equipment. Pool accounting and all run modifiers need more
comparison; later node families still need implementation and Client checks.
When a Supply selection has no captured next-node transition,
`cwSelectSupply` now delegates to the generated route factory on the same
detached transaction and preserves finish/unlock/level/pending notification
order. The existing captured path remains unchanged. This removes the separate
capture-only restriction after a generated Supply choice, but the factory
still rejects unsupported destination families, including the EliteBranch
following 2-2; the selection remains uncommitted until that family is
implemented. No Client run has exercised this fallback.
For a selected Encounter branch, the battle settlement now grants supported
gold, EXP, or item rewards when the reported progress reaches the branch's
threshold, using the selected server-side drop list on the detached snapshot.
Unmapped reward types fail without committing. Perfect-clear monster Orb
generation is not implemented; Client verification is still outstanding.
The native captured Encounter at route 2801 chapter 1 section 4 contains two
branch displays, each with its own penalty, monster wave and `BOAMLLHPNEC`
drop. A read-only check of the saved run at chapter 2 section 1 generated its
next Supply node and four choices on a detached copy; the saved run was not
written and no Client interaction was performed. The latest FFGO's
`gfRouteFillEliteBranch` constructs branch IDs from the node template's first
parameter and option kind, resolves difficulty through
`gfRouteEliteBranchDifficulty`, and builds the reward in
`gfRouteEliteOptionReward`. These are useful evidence, but matching the later
branches to this Client's option, penalty and difficulty data remains open.
After the final node, `cwAdvanceRouteNode` returns `CW route has no next node`.
At zero HP, `finishCurrencyWarsBattle` returns `CW defeat settlement requires
+12 -6
View File
@@ -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
}
+15 -1
View File
@@ -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")
}
}
+11
View File
@@ -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 {
+7
View File
@@ -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)
}
}
}
+15 -6
View File
@@ -107,11 +107,6 @@
{
"group_id": 5,
"stages": {
"6": {
"npc_monster_id": 5013040,
"map_entrance_id": 3014101,
"maze_group_id": 5
},
"502": {
"npc_monster_id": 4014030,
"map_entrance_id": 3014101,
@@ -126,6 +121,11 @@
"npc_monster_id": 5014020,
"map_entrance_id": 3014101,
"maze_group_id": 5
},
"501": {
"npc_monster_id": 5013040,
"map_entrance_id": 3014101,
"maze_group_id": 5
}
}
},
@@ -283,5 +283,14 @@
},
"source_kind": "reference-server-embedded-data",
"evidence": "FFGO updated 2026-10-26; battle_diagnostics/ffgo-20261026/challenge_peak_extra_data.json",
"verification": "Reference mapping only; installed Client scene/NPC acceptance remains unverified. Battle events and waves remain from existing Client resources."
"verification": "Reference mapping only; installed Client scene/NPC acceptance remains unverified. Battle events and waves remain from existing Client resources.",
"client_stage_key_corrections": [
{
"group_id": 5,
"reference_stage_key": "6",
"client_stage_id": 501,
"npc_monster_id": 5013040,
"basis": "Unique missing stage in ChallengePeakGroupConfig.PreLevelIDList with identical ChallengePeakConfig.NpcMonsterIDList actor; placement values unchanged."
}
]
}
@@ -12,6 +12,7 @@
"minion_filter": "gfRouteMinions RVA0xcdb020"
},
"policy": {
"neutral_camp_compatibility": "stable_random_enabled_season_camp_by_run_id",
"first_chapter_boss_star": 2,
"later_chapter_boss_star": 3,
"first_chapter": 1,
+252
View File
@@ -0,0 +1,252 @@
do
local VERSION = 7
local MODULE_NAMES = {
'Logic.SettingModule.SettingEnum',
'Logic/SettingModule/SettingEnum',
'@BakedLua/Logic/SettingModule/SettingEnum',
'SettingEnum',
}
local state = _G.__GAC_FPS_SETTINGS_120_STATE
local Unity = CS.UnityEngine
local function remove_callback(callback)
if not callback then return end
for _, event in ipairs({
{Unity.Application, 'onBeforeRender'},
{Unity.Camera, 'onPreCull'},
}) do
pcall(function() event[1][event[2]]('-', callback) end)
pcall(function() event[1][event[2]] = event[1][event[2]] - callback end)
end
end
local function add_callback(callback)
for _, event in ipairs({
{Unity.Application, 'onBeforeRender'},
{Unity.Camera, 'onPreCull'},
}) do
local ok = pcall(function() event[1][event[2]]('+', callback) end)
if not ok then
ok = pcall(function()
event[1][event[2]] = event[1][event[2]] + callback
end)
end
if ok then return true, event[1], event[2] end
end
return false
end
if state and state.version ~= VERSION then
remove_callback(state.callback)
if state.module and state.original_getter then
pcall(function()
state.module.get_graphics_data_by_type = state.original_getter
end)
end
state = nil
end
state = state or {version = VERSION, next_check = 0}
_G.__GAC_FPS_SETTINGS_120_STATE = state
local function log(message)
Unity.Debug.Log('[GaC 120 FPS Settings] ' .. message)
end
if not state.load_logged then
state.load_logged = true
log('script loaded; checking the native Graphics settings module')
end
local function add_native_option(list, text_id)
if type(list) ~= 'table' then return false end
local next_key = 1
local has_30_fps = false
local has_60_fps = false
for _, option in ipairs(list) do
if type(option) == 'table' then
if option.setting == 120 then return true, false end
if option.setting == 30 and option.text_id == 'UIText_Setting_Level_30FPS' then
has_30_fps = true
elseif option.setting == 60 and option.text_id == 'UIText_Setting_Level_60FPS' then
has_60_fps = true
end
if type(option.key) == 'number' and option.key >= next_key then
next_key = option.key + 1
end
end
end
if not has_30_fps or not has_60_fps then return false, false end
table.insert(list, {
key = next_key,
text_id = text_id,
setting = 120,
})
return true, true
end
local function patch_native_data(module)
local graphics_data = rawget(module, '_graphics_data')
if type(graphics_data) ~= 'table' then
if not state.data_probe_logged then
state.data_probe_logged = true
log('native _graphics_data is not available on the settings module')
end
return false
end
local found_fps_list = false
local added = false
for setting_type, list in pairs(graphics_data) do
local ok, matched, inserted = pcall(add_native_option, list, 'UIText_Setting_Level_120FPS')
if ok and matched then
found_fps_list = true
if inserted then added = true end
if not state.data_probe_logged then
state.data_probe_logged = true
log('found native FPS rows in _graphics_data[' .. tostring(setting_type) .. ']')
end
elseif not ok and not state.data_probe_logged then
state.data_probe_logged = true
log('_graphics_data probe failed: ' .. tostring(matched))
end
end
if added and not state.row_added then
state.row_added = true
log('added the 120 FPS option to the native Graphics data')
elseif not found_fps_list and not state.data_probe_logged then
state.data_probe_logged = true
log('native _graphics_data is present but contains no matching 30/60 FPS rows')
end
return found_fps_list
end
local function find_setting_module()
for _, name in ipairs(MODULE_NAMES) do
local loaded = package.loaded[name]
if type(loaded) == 'table' and type(loaded.get_graphics_data_by_type) == 'function' then
state.module_alias = name
return loaded
end
end
if not state.require_attempted then
state.require_attempted = true
local outcomes = {}
for _, name in ipairs(MODULE_NAMES) do
local ok, result = pcall(require, name)
if ok and type(result) == 'table' and type(result.get_graphics_data_by_type) == 'function' then
state.module_alias = name
return result
end
local loaded = package.loaded[name]
if type(loaded) == 'table' and type(loaded.get_graphics_data_by_type) == 'function' then
state.module_alias = name
return loaded
end
outcomes[#outcomes + 1] = name .. '=' .. (ok and (type(result) .. ':' .. tostring(result)) or tostring(result))
end
state.require_error = table.concat(outcomes, '; ')
end
local global_registry = rawget(_G, 'G')
for _, entry in ipairs({
{value = rawget(_G, 'SettingModule'), name = '_G.SettingModule'},
{value = rawget(_G, 'SettingEnum'), name = '_G.SettingEnum'},
{value = type(global_registry) == 'table' and rawget(global_registry, 'SettingModule') or nil, name = 'G.SettingModule'},
{value = type(global_registry) == 'table' and rawget(global_registry, 'SettingEnum') or nil, name = 'G.SettingEnum'},
}) do
local candidate = entry.value
if type(candidate) == 'table' and type(candidate.get_graphics_data_by_type) == 'function' then
state.module_alias = entry.name
return candidate
end
end
return nil
end
local function install()
local module = find_setting_module()
if type(module) ~= 'table' then
if not state.module_wait_logged then
state.module_wait_logged = true
log('SettingEnum lookup failed; ' .. tostring(state.require_error))
end
return false
end
local getter = module.get_graphics_data_by_type
if type(getter) ~= 'function' then
if not state.getter_wait_logged then
state.getter_wait_logged = true
log('Settings module loaded, but its graphics-data getter is unavailable')
end
return false
end
if state.module == module then return true end
local fps_text = 'UIText_Setting_Level_120FPS'
local original = getter
module.get_graphics_data_by_type = function(...)
local list = original(...)
local ok, result, inserted = pcall(add_native_option, list, fps_text)
if not ok and state.last_error ~= tostring(result) then
state.last_error = tostring(result)
log('could not add the native 120 FPS option: ' .. state.last_error)
elseif ok and result and inserted and not state.row_added then
state.row_added = true
log('added the 120 FPS option to the native Graphics list')
elseif not state.getter_probe_logged then
state.getter_probe_logged = true
local count = type(list) == 'table' and #list or -1
log('Graphics getter called; returned ' .. type(list) .. ' with ' .. tostring(count) .. ' rows')
end
return list
end
state.module = module
state.original_getter = original
log('ready; patched ' .. tostring(state.module_alias))
patch_native_data(module)
return true
end
local function tick()
local now = Unity.Time.unscaledTime
if now < state.next_check then return end
state.next_check = now + 0.5
local ok, result = pcall(install)
if not ok then
if state.last_error ~= tostring(result) then
state.last_error = tostring(result)
log('waiting for the native Graphics settings: ' .. state.last_error)
end
return
end
if result then
remove_callback(state.callback)
state.callback = nil
end
end
if not state.module and not state.callback then
state.callback = tick
local ok = add_callback(state.callback)
if not ok then
state.callback = nil
log('could not register the Graphics settings hook')
end
end
if not state.module then
tick()
end
end
+30
View File
@@ -124,6 +124,35 @@ local function current_facecam_target()
return nil, nil, nil, true
end
-- Log only changes, so diagnostics stay useful during long battles.
local function log_facecam_target(ok, head, root, id, tracked)
local success, detail = pcall(function()
if not ok then return 'resolve_error=' .. tostring(head) end
return 'tracked=' .. tostring(tracked) .. ' entity=' .. tostring(id)
.. ' model=' .. tostring(root and root.name)
.. ' head=' .. tostring(head and head.name)
.. ' head_parent=' .. tostring(head and head.parent and head.parent.name)
.. ' head_active=' .. tostring(head and head.gameObject.activeInHierarchy)
end)
if not success then detail = 'diagnostic_error=' .. tostring(detail) end
if _G.__FC_FACE_LAST_DIAGNOSTIC ~= detail then
_G.__FC_FACE_LAST_DIAGNOSTIC = detail
local message = '[GaC FaceCam] ' .. detail
CS.UnityEngine.Debug.Log(message)
local written, err = pcall(function()
local dir = _G.__GAC_DIAGNOSTICS_DIR
if not dir or dir == '' then error('server diagnostics directory unavailable') end
CS.System.IO.Directory.CreateDirectory(dir)
local path = CS.System.IO.Path.Combine(dir, 'facecam.log')
CS.System.IO.File.AppendAllText(path, tostring(CS.System.DateTime.Now) .. ' ' .. message .. '\n')
end)
if not written and _G.__FC_FACE_FILE_ERROR ~= tostring(err) then
_G.__FC_FACE_FILE_ERROR = tostring(err)
CS.UnityEngine.Debug.Log('[GaC FaceCam] file log error: ' .. tostring(err))
end
end
end
local function facecam_target(cam)
local ok, head, root = pcall(function()
local battleHead, battleRoot, id, tracked = current_facecam_target()
@@ -152,6 +181,7 @@ local function update_facecam(cam, settings)
if now >= (_G.__FC_FACE_NEXT or 0) then
_G.__FC_FACE_NEXT = now + 0.15
local ok, head, root, id, tracked = pcall(current_facecam_target)
log_facecam_target(ok, head, root, id, tracked)
if ok and tracked and head and root then
_G.__FC_FACE_HEAD, _G.__FC_FACE_ROOT, _G.__FC_FACE_ENTITY = head, root, id
end
+61 -6
View File
@@ -6,6 +6,22 @@ local GameObject = CS.UnityEngine.GameObject
local CanvasGroup = CS.UnityEngine.CanvasGroup
local Camera = CS.UnityEngine.Camera
-- The server supplies its installation's log directory in the heartbeat.
local function diagnostic(message)
CS.UnityEngine.Debug.Log(message)
local ok, err = pcall(function()
local dir = _G.__GAC_DIAGNOSTICS_DIR
if not dir or dir == '' then error('server diagnostics directory unavailable') end
CS.System.IO.Directory.CreateDirectory(dir)
local path = CS.System.IO.Path.Combine(dir, 'hide-ui.log')
CS.System.IO.File.AppendAllText(path, tostring(CS.System.DateTime.Now) .. ' ' .. message .. '\n')
end)
if not ok and _G.__HIDE_UI_FILE_ERROR ~= tostring(err) then
_G.__HIDE_UI_FILE_ERROR = tostring(err)
CS.UnityEngine.Debug.Log('[GaC HideUI] file log error: ' .. tostring(err))
end
end
-- Names observed under UIRoot in this Client, plus the existing battle HUD.
-- Never target an ancestor shared by HUD and character/reward dialogs.
local HUD_PATHS = {
@@ -58,8 +74,9 @@ _G.__HIDE_UI_ENABLED = _G.__HIDE_UI_ENABLED or false
_G.__HIDE_UI_TARGETS = _G.__HIDE_UI_TARGETS or {}
local function restore_hud()
local count, errors = 0, 0
for id, state in pairs(_G.__HIDE_UI_TARGETS) do
pcall(function()
local ok = pcall(function()
if state.group then
if state.added then
CS.UnityEngine.Object.Destroy(state.group)
@@ -70,8 +87,11 @@ local function restore_hud()
end
end
end)
count = count + 1
if not ok then errors = errors + 1 end
_G.__HIDE_UI_TARGETS[id] = nil
end
diagnostic('[GaC HideUI] restored=' .. count .. ' errors=' .. errors)
end
local function protected_name(name)
@@ -91,6 +111,9 @@ local function hide_object(obj)
state = {group=group, added=added, alpha=group.alpha,
raycast=group.blocksRaycasts, interactable=group.interactable}
_G.__HIDE_UI_TARGETS[id] = state
if _G.__HIDE_UI_SCAN_DIAG then
_G.__HIDE_UI_SCAN_DIAG.added = _G.__HIDE_UI_SCAN_DIAG.added + 1
end
end
state.group.alpha = 0
state.group.blocksRaycasts = false
@@ -166,7 +189,14 @@ end
local function hide_hud()
for _, path in ipairs(HUD_PATHS) do
pcall(function() hide_hud_tree(GameObject.Find(path)) end)
local ok, err = pcall(function()
local obj = GameObject.Find(path)
if obj and obj.activeInHierarchy then
table.insert(_G.__HIDE_UI_SCAN_DIAG.paths, path)
end
hide_hud_tree(obj)
end)
if not ok then table.insert(_G.__HIDE_UI_SCAN_DIAG.errors, path .. ': ' .. tostring(err)) end
end
local ok, err = pcall(function()
local flags = CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags), 60)
@@ -176,6 +206,7 @@ local function hide_hud()
or typeof(CS.RPG.Client.BattleGamePhase):GetField("_HudPage", flags)
local manager = CS.RPG.Client.GlobalVars.s_GamePhaseManager
local phase = manager and _G.__HIDE_UI_PHASE_FIELD:GetValue(manager)
_G.__HIDE_UI_SCAN_DIAG.phase = phase and tostring(phase:GetType().Name) or 'none'
if phase and typeof(CS.RPG.Client.BattleGamePhase):IsInstanceOfType(phase) then
local hud = _G.__HIDE_UI_BATTLE_HUD_FIELD:GetValue(phase)
if hud and hud.Trans then hide_hud_tree(hud.Trans.gameObject) end
@@ -189,13 +220,14 @@ local function hide_hud()
end)
if not ok and _G.__HIDE_UI_LAST_ERROR ~= tostring(err) then
_G.__HIDE_UI_LAST_ERROR = tostring(err)
CS.UnityEngine.Debug.Log("[GaC HideUI] Battle HUD: " .. tostring(err))
diagnostic("[GaC HideUI] Battle HUD: " .. tostring(err))
end
pcall(function()
local pagesOK, pagesErr = pcall(function()
local ui = CS.RPG.Client.GlobalVars.s_UIManager
local pages = ui:GetAllPageNames()
for i = 0, pages.Count - 1 do
local name = tostring(pages[i]); local lower = string.lower(name)
table.insert(_G.__HIDE_UI_SCAN_DIAG.pages, name)
local page = ui:GetPageByName(name)
if page and page.ChildDialogs then
for j = 0, page.ChildDialogs.Count - 1 do
@@ -219,16 +251,18 @@ local function hide_hud()
end
hide_rank_controls(GameObject.Find("UIRoot/Page/AvatarMainPage(Clone)/Root/AvatarRankPanel"))
end)
if not pagesOK then table.insert(_G.__HIDE_UI_SCAN_DIAG.errors, 'pages: ' .. tostring(pagesErr)) end
end
function _G.__HIDE_UI_TICK()
pcall(function()
local ok, err = pcall(function()
local frame = Time.frameCount
if _G.__HIDE_UI_LAST_FRAME == frame then return end
_G.__HIDE_UI_LAST_FRAME = frame
local toggled = Input.GetKeyDown(KeyCode.Backslash)
if toggled then
_G.__HIDE_UI_ENABLED = not _G.__HIDE_UI_ENABLED
diagnostic('[GaC HideUI] enabled=' .. tostring(_G.__HIDE_UI_ENABLED))
if not _G.__HIDE_UI_ENABLED then restore_hud() end
end
-- The page/child scan walks Unity UI trees, so keep it to once a second.
@@ -237,10 +271,31 @@ function _G.__HIDE_UI_TICK()
local now = Time.unscaledTime
if toggled or now >= (_G.__HIDE_UI_NEXT_SCAN or 0) then
_G.__HIDE_UI_NEXT_SCAN = now + 1.0
hide_hud()
local started = Time.realtimeSinceStartup
local diag = {added=0, paths={}, pages={}, errors={}, phase='unknown'}
_G.__HIDE_UI_SCAN_DIAG = diag
local scanOK, scanErr = pcall(hide_hud)
_G.__HIDE_UI_SCAN_DIAG = nil
if not scanOK then table.insert(diag.errors, 'scan: ' .. tostring(scanErr)) end
local elapsed = (Time.realtimeSinceStartup - started) * 1000
local signature = diag.phase .. '|' .. table.concat(diag.pages, ',') .. '|'
.. table.concat(diag.paths, ',') .. '|' .. table.concat(diag.errors, ',')
if toggled or diag.added > 0 or #diag.errors > 0 or elapsed >= 20
or signature ~= _G.__HIDE_UI_DIAG_SIGNATURE
or now >= (_G.__HIDE_UI_DIAG_NEXT or 0) then
_G.__HIDE_UI_DIAG_SIGNATURE = signature
_G.__HIDE_UI_DIAG_NEXT = now + 30
diagnostic(string.format('[GaC HideUI] scan_ms=%.2f phase=%s added=%d pages=[%s] paths=[%s] errors=[%s]',
elapsed, diag.phase, diag.added, table.concat(diag.pages, ','),
table.concat(diag.paths, ','), table.concat(diag.errors, ',')))
end
end
end
end)
if not ok and _G.__HIDE_UI_TICK_ERROR ~= tostring(err) then
_G.__HIDE_UI_TICK_ERROR = tostring(err)
diagnostic('[GaC HideUI] tick error: ' .. tostring(err))
end
end
-- Restore targets from the previous policy (including VersionText) on upgrade.
+11
View File
@@ -0,0 +1,11 @@
do if not _G.__gacAAPeakCache1 then _G.__gacAAPeakCache1=true
local ok,e=pcall(function()
local m=CS.RPG.Client.GlobalVars.s_ModuleManager.ChallengePeakModule
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local t=m:GetType();local fields=t:GetFields(flags)
for i=0,fields.Length-1 do local f=fields[i];local v=f:GetValue(m);CS.UnityEngine.Debug.Log('[GAC_AA_CACHE] FIELD '..f.Name..' '..tostring(f.FieldType)..' '..tostring(v))end
local methods=t:GetMethods(flags)
for i=0,methods.Length-1 do local f=methods[i];if string.find(f.Name,'GetChallengePeakData') or string.find(f.Name,'CurrentPeak') or string.find(f.Name,'Clear') or string.find(f.Name,'Reset') then CS.UnityEngine.Debug.Log('[GAC_AA_CACHE] METHOD '..tostring(f)..' HANDLE '..tostring(f.MethodHandle.Value))end end
end)
if not ok then CS.UnityEngine.Debug.Log('[GAC_AA_CACHE] ERROR '..tostring(e))end
end end
+9
View File
@@ -0,0 +1,9 @@
do if not _G.__gacAAStatusNow1 then _G.__gacAAStatusNow1=true
local ok,e=pcall(function()
local d=_G.__GAC_AA_ROUND_REFRESH
local st=_G.__GAC_AA_ROUND_REFRESH_STATE
local m=CS.RPG.Client.GlobalVars.s_ModuleManager.ChallengePeakModule
CS.UnityEngine.Debug.Log('[GAC_AA_STATUS_NOW] current='..tostring(m and m.CurrentPeakGroupID)..' desired='..tostring(d and d.desired_group)..' deferred='..tostring(d and d.deferred)..' pending='..tostring(st and st.pending~=nil)..' error='..tostring(_G.__GAC_AA_ROUND_REFRESH_ERROR))
end)
if not ok then CS.UnityEngine.Debug.Log('[GAC_AA_STATUS_NOW] ERROR '..tostring(e))end
end end
+11
View File
@@ -0,0 +1,11 @@
do if not _G.__gacScheduleAccess1 then _G.__gacScheduleAccess1=true
local ok,e=pcall(function()
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local m=CS.RPG.Client.GlobalVars.s_ModuleManager.ChallengeModule
local methods=m:GetType():GetMethods(flags)
for i=0,methods.Length-1 do local x=methods[i];if string.find(x.Name,'GroupData')then CS.UnityEngine.Debug.Log('[GAC_SCHEDULE_ACCESS] '..tostring(x))end end
local tm=CS.RPG.Client.GlobalVars.s_TimeManager
if tm then local props=tm:GetType():GetProperties(flags);for i=0,props.Length-1 do local p=props[i];if p:GetIndexParameters().Length==0 and (string.find(p.Name,'Stamp') or string.find(p.Name,'Time')) then local ok,v=pcall(function()return p:GetValue(tm,nil)end);CS.UnityEngine.Debug.Log('[GAC_CLOCK_CLIENT] '..p.Name..' '..tostring(ok)..' '..tostring(v))end end end
end)
if not ok then CS.UnityEngine.Debug.Log('[GAC_SCHEDULE_ACCESS] ERROR '..tostring(e))end
end end
+18
View File
@@ -0,0 +1,18 @@
do
local s=_G.__gacScheduleGroup1 or {i=1};_G.__gacScheduleGroup1=s
if s.i>3 then return end
local k=s.i;s.i=k+1
local ok,e=pcall(function()
local m=CS.RPG.Client.GlobalVars.s_ModuleManager.ChallengeModule
local kind=CS.System.Enum.ToObject(typeof(CS.RPG.GameCore.ChallengeGroupType),k)
local g=m:GetLatestScheduleChallengeGroupDataByGroupType(kind)
if not g then return end
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local props=g:GetType():GetProperties(flags)
for i=0,props.Length-1 do local p=props[i];if p:GetIndexParameters().Length==0 then
local n=string.lower(p.Name)
if string.find(n,'schedule') or string.find(n,'time') or string.find(n,'begin') or string.find(n,'end') or string.find(n,'row') or string.find(n,'meta') then local ok,v=pcall(function()return p:GetValue(g,nil)end);CS.UnityEngine.Debug.Log('[GAC_GROUP_SCHEDULE] '..k..' '..p.Name..' '..tostring(p.PropertyType)..' '..tostring(ok)..' '..tostring(v));if ok and v and (string.find(n,'schedule') or string.find(n,'row')) then local j,txt=pcall(function()return CS.Newtonsoft.Json.JsonConvert.SerializeObject(v)end);if j then CS.UnityEngine.Debug.Log('[GAC_GROUP_SCHEDULE_JSON] '..k..' '..p.Name..' '..txt)end end end
end end
end)
if not ok then CS.UnityEngine.Debug.Log('[GAC_GROUP_SCHEDULE] ERROR '..tostring(e))end
end
+12
View File
@@ -0,0 +1,12 @@
do
if not _G.__gacScheduleSurface1 then
_G.__gacScheduleSurface1=true
local ok,e=pcall(function()
local m=CS.RPG.Client.GlobalVars.s_ModuleManager.ChallengeModule
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local methods=m:GetType():GetMethods(flags)
for i=0,methods.Length-1 do local x=methods[i];if string.find(string.lower(x.Name),'schedule') or string.find(string.lower(x.Name),'time') or string.find(string.lower(x.Name),'open') then CS.UnityEngine.Debug.Log('[GAC_SCHEDULE_SURFACE] '..tostring(x)) end end
end)
if not ok then CS.UnityEngine.Debug.Log('[GAC_SCHEDULE_SURFACE] ERROR '..tostring(e)) end
end
end
+12
View File
@@ -0,0 +1,12 @@
-- Bounded read-only schedule probe; remove after capture.
do
local s=_G.__gacEndgameTime8 or {i=1};_G.__gacEndgameTime8=s
local ids={100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,900,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023}
if s.i>#ids then return end
for count=1,8 do if s.i>#ids then return end;local id=ids[s.i];s.i=s.i+1
local ok,e=pcall(function()
local row=CS.RPG.GameCore.ChallengeGroupConfigExcelTable.GetData(id)
local kind=CS.System.Convert.ToInt32(row.ChallengeGroupType);if row and row.ScheduleDataID~=0 and kind>=1 and kind<=3 then local tables={CS.RPG.GameCore.ScheduleDataChallengeMazeConfigExcelTable,CS.RPG.GameCore.ScheduleDataChallengeStoryConfigExcelTable,CS.RPG.GameCore.ScheduleDataChallengeBossConfigExcelTable};local schedule=tables[kind].GetData(row.ScheduleDataID);CS.UnityEngine.Debug.Log('[GAC_ENDGAME_SCHEDULE] '..id..' '..CS.Newtonsoft.Json.JsonConvert.SerializeObject(schedule)) end
end)
if not ok then CS.UnityEngine.Debug.Log('[GAC_ENDGAME_TIME] ERROR '..tostring(e));s.i=#ids+1 end
end end
+22
View File
@@ -0,0 +1,22 @@
do
local s=_G.__gacWindowCapture2 or {i=1};_G.__gacWindowCapture2=s
local ids={100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,900,1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1011,1012,1013,1014,1015,1016,1017,1018,1019,1020,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024,2025,2026,2027,2028,3001,3002,3003,3004,3005,3006,3007,3008,3009,3010,3011,3012,3013,3014,3015,3016,3017,3018,3019,3020,3021,3022,3023}
if s.i>#ids then return end
local m=CS.RPG.Client.GlobalVars.s_ModuleManager.ChallengeModule
for n=1,8 do
if s.i>#ids then return end
local id=ids[s.i];s.i=s.i+1
local ok,e=pcall(function()
local g=m:GetChallengeGroupData(id)
if g and g.IsScheduleChallenge then
CS.UnityEngine.Debug.Log('[GAC_ENDGAME_WINDOW] '..id..' '..g.BeginTimeStamp..' '..g.EndTimeStamp)
if id==ids[#ids] then
local flags=CS.System.Enum.ToObject(typeof(CS.System.Reflection.BindingFlags),60)
local props=g:GetType():GetProperties(flags)
for i=0,props.Length-1 do local p=props[i];if p.PropertyType==typeof(CS.System.Boolean) and p:GetIndexParameters().Length==0 then local ok,v=pcall(function()return p:GetValue(g,nil)end);CS.UnityEngine.Debug.Log('[GAC_GROUP_BOOL] '..p.Name..' '..tostring(ok)..' '..tostring(v))end end
end
end
end)
if not ok then CS.UnityEngine.Debug.Log('[GAC_ENDGAME_WINDOW] ERROR '..id..' '..tostring(e));s.i=#ids+1;return end
end
end