diff --git a/docs/tower.md b/docs/tower.md new file mode 100644 index 000000000..42a28976f --- /dev/null +++ b/docs/tower.md @@ -0,0 +1,511 @@ +# Tower / Sky Corridor (天廊 / Tenrou) + +Tracks what is known about the Tower system and what remains to be reverse-engineered +or implemented in Erupe. + +The core of this system **is already implemented on `develop`** via the repository and +service pattern (`repo_tower.go`, `svc_tower.go`, `handlers_tower.go`). Two branches carry +earlier work: `wip/tower` predates the refactor and uses direct SQL; `feature/tower` merges +`wip/tower` with `feature/earth`. Their useful findings are incorporated below. + +The `feature/tower` branch is **not mergeable in its current state** — it diverged too far +from `develop` and was superseded by the direct integration of tower code into `develop`. +Its main remaining value is the `PresentBox` handler logic and the `TimeTaken`/`CID` +field naming for `MsgMhfPostTowerInfo`. + +--- + +## Game Context + +The **Sky Corridor** (天廊, *Tenrou*) is a permanent dungeon introduced in MHFG6. Players +explore a multi-floor structure built by an ancient civilization on a desolate island. Unlike +quest zones, the Sky Corridor has its own progression systems independent of normal quests. + +### Individual Tower Progression + +- Each clear adds **10 minutes** to the timer; quests consist of 2–4 randomly selected floors. +- Each floor contains 2–3 rooms separated by gates, with Felynes that drop **purple medals**, + fixed-position treasure chests, and shiny crystals. +- Players accumulate **Tower Rank (TR)**, **Tower Rank Points (TRP)**, and + **Tower Skill Points (TSP)**. +- TSP is spent to level up one of 64 tower-specific skills stored server-side. +- Two floor-count columns exist in the DB (`block1`, `block2`), corresponding to the + pre-G7 and G7+ floor tiers respectively. + +### Duremudira (The Guardian) + +**Duremudira** (天廊の番人, Tower Guardian) is the Emperor Ice Dragon — an Elder Dragon +introduced in MHFG6. It appears as an optional boss in the second district of the Sky +Corridor. Slaying it yields **Red and Grey Liquids** used to craft Sky Corridor Gems and +Sigils. + +**Arrogant Duremudira** is a harder variant; less information about it is available in +English sources. + +### Gems (Sky Corridor Decorations) + +Gems are collectibles (30 slots, organized as 6 tiers × 5 per tier) obtained from the Tower. +They slot into equipment to activate skills without consuming armor skill slots. Stored +server-side as a 30-element CSV in `tower.gems`. + +### Tenrouirai (天廊威来) — Guild Mission System + +A guild-parallel challenge system layered over the Sky Corridor. Each guild has a +**mission page** (1-based) containing 3 active missions. Guild members contribute scores +by completing tower runs; when cumulative scores meet all three mission goals, the page +advances (after a guild RP donation threshold is also met). There are 33 hardcoded +missions across 3 blocks. + +**Mission types** (the `Mission` field in `TenrouiraiData`): +| Value | Objective | +|-------|-----------| +| 1 | Floors climbed | +| 2 | Antiques collected | +| 3 | Chests opened | +| 4 | Felynes (cats) saved | +| 5 | TRP acquisition | +| 6 | Monster slays | + +### Relation to the Earth Event Cycle + +The Tower phase is **week 3** of the three-week Earth event rotation (see `docs/conquest-war.md`). +The `GetWeeklySeibatuRankingReward` handler (Operation=5, IDs 260001 and 260003) already +handles Tower dure kill rewards and the 155-entry floor reward table. These do not need to be +reimplemented here. + +--- + +## Database Schema + +All tower tables are defined in `server/migrations/sql/0001_init.sql`. + +### `tower` — per-character progression + +```sql +CREATE TABLE public.tower ( + char_id integer, + tr integer, -- Tower Rank + trp integer, -- Tower Rank Points + tsp integer, -- Tower Skill Points + block1 integer, -- Floor count, era 1 (pre-G7) + block2 integer, -- Floor count, era 2 (G7+) + skills text, -- CSV of 64 skill levels + gems text -- CSV of 30 gem quantities (6 tiers × 5) +); +``` + +`skills` and `gems` default to `EmptyTowerCSV(N)` — comma-separated zeros — when NULL. +Gems are encoded as `tier << 8 | (index_within_tier + 1)` in wire responses. + +### Guild columns (`guilds` and `guild_characters`) + +```sql +-- guilds +tower_mission_page integer DEFAULT 1 -- Current Tenrouirai mission page +tower_rp integer DEFAULT 0 -- Accumulated guild tower RP + +-- guild_characters +tower_mission_1 integer -- Member's score for mission slot 1 +tower_mission_2 integer -- Member's score for mission slot 2 +tower_mission_3 integer -- Member's score for mission slot 3 +``` + +--- + +## Packet Overview + +Ten packets implement the Tower system. All live in `network/mhfpacket/`. None have +`Build()` implemented (all return `NOT IMPLEMENTED`). + +### `MsgMhfGetTowerInfo` — Client → Server → Client + +Fetches character tower data. The `InfoType` field selects what data to return. + +**Request** (`msg_mhf_get_tower_info.go`): +``` +AckHandle uint32 +InfoType uint32 — 1=TR/TRP, 2=TSP+skills, 3=level(pre-G7), 4=history, 5=level(G7+) +Unk0 uint32 — unknown; never used by handler +Unk1 uint32 — unknown; never used by handler +``` + +**Response**: variable-length array of frames via `doAckEarthSucceed`. + +| InfoType | Response per frame | +|----------|-------------------| +| 1 | `TR int32, TRP int32` | +| 2 | `TSP int32, Skills [64]int16` | +| 3, 5 | `Floors int32, Unk1 int32, Unk2 int32, Unk3 int32` — one frame per era (1 for G7, 2 for G8+) | +| 4 | `[5]int16 (history group 0), [5]int16 (history group 1)` | + +**InfoTypes 3 and 5** use the same code path and both return `TowerInfoLevel` entries. +The distinction between them is not understood. The `wip/tower` branch treats them +identically. + +**TowerInfoLevel Unk1/Unk2/Unk3**: three of the four level-entry fields are unknown. +They are hardcoded to `5` in `wip/tower` and `0` on `develop`. Whether they carry +max floor, session count, or display state is not known. + +**InfoType 4 (history)**: returns two groups of 5 × int16. The `wip/tower` branch +hardcodes them as `{1, 2, 3, 4, 5}` / `{1, 2, 3, 4, 5}`. Their meaning (e.g. recent +clear times, floor high scores) is not reverse-engineered. On `develop` they return zeros. + +**Current state on develop**: Implemented. Reads from `repo_tower.GetTowerData()`. +History data is zero-filled (semantics unknown). Level Unk1–Unk3 are zero-filled. + +--- + +### `MsgMhfPostTowerInfo` — Client → Server → Client + +Submits updated tower progress after a quest. + +**Request** (`msg_mhf_post_tower_info.go`): +``` +AckHandle uint32 +InfoType uint32 — 1 or 7 = progress update, 2 = skill purchase +Unk1 uint32 — unknown; logged in debug mode +Skill int32 — skill index to level up (InfoType=2 only) +TR int32 — new Tower Rank to set +TRP int32 — TRP earned (added to existing) +Cost int32 — TSP cost (InfoType=2) or TSP earned (InfoType=1,7) +Unk6 int32 — unknown; logged in debug mode +Unk7 int32 — unknown; logged in debug mode +Block1 int32 — floor count increment (InfoType=1,7) +Unk9 int64 — develop: reads as int64; wip/tower: reads as TimeTaken int32 + CID int32 +``` + +**Field disambiguation — `Unk9` vs `TimeTaken + CID`**: the `wip/tower` branch splits the +final 8 bytes into `TimeTaken int32` (quest duration in seconds) and `CID int32` +(character ID). This interpretation appears more correct than a single int64 — the character +ID would make sense as a submission attribution field. `develop` keeps it as `Unk9 int64` +until confirmed. This should be verified with a packet capture. + +**InfoType 7**: handled identically to InfoType 1. The difference between them is unknown +— it may relate to whether the run included Duremudira or was a normal floor clear. + +**TSP rate note**: the handler comment in both branches says "This might give too much TSP? +No idea what the rate is supposed to be." The `Cost` field is used for both TSP earned +(on progress updates) and TSP spent (on skill purchases); the actual earn rate formula is +unknown. + +**`block2` not written**: `UpdateProgress` only writes `block1`. The `block2` column (G7+ +floor era) is never incremented by the handler. This is likely a bug — `block2` should be +written when the client sends a G7+ floor run. + +**Current state on develop**: Implemented. Calls `towerRepo.UpdateSkills` (InfoType=2) and +`towerRepo.UpdateProgress` (InfoType=1,7). `Unk9`/`Unk1`/`Unk6`/`Unk7` are logged in +debug mode but not acted on. + +--- + +### `MsgMhfGetTenrouirai` — Client → Server → Client + +Fetches Tenrouirai (guild mission) data. + +**Request** (`msg_mhf_get_tenrouirai.go`): +``` +AckHandle uint32 +Unk0 uint8 — unknown; never used +DataType uint8 — 1=mission defs, 2=rewards, 4=guild progress, 5=char scores, 6=guild RP +GuildID uint32 +MissionIndex uint8 — which mission to query scores for (DataType=5 only; 1-3) +Unk4 uint8 — unknown; never used +``` + +**DataType=1 response**: 33 frames, one per mission definition: +``` +[uint8] Block — 1–3 +[uint8] Mission — type (1–6, see table above) +[uint16] Goal — score required +[uint16] Cost — RP cost to unlock/advance +[uint8] Skill1–6 — 6 skill requirement bytes (values: 80, 40, 40, 20, 40, 50) +``` + +**DataType=2 response (rewards)**: `TenrouiraiReward` struct is defined but never +populated. Returns an empty array. Response format: +``` +[uint8] Index +[uint16] Item[0..4] — 5 item IDs +[uint8] Quantity[0..4] — 5 quantities +``` +No captures of a populated reward response are known. + +**DataType=4 response**: 1 frame: +``` +[uint8] Page — current mission page (1-based) +[uint16] Mission1 — aggregated guild score for slot 1 (capped to goal) +[uint16] Mission2 — aggregated guild score for slot 2 (capped to goal) +[uint16] Mission3 — aggregated guild score for slot 3 (capped to goal) +``` + +**DataType=5 response**: N frames, one per guild member with a non-null score: +``` +[int32] Score +[14 bytes] HunterName (null-padded) +``` + +**DataType=6 response**: 1 frame: +``` +[uint8] Unk0 — always 0 +[uint32] RP — guild's accumulated tower RP +[uint32] Unk2 — unknown; always 0 +``` + +**`TenrouiraiKeyScore`** (`Unk0 uint8, Unk1 int32`): defined and included in the +`Tenrouirai` struct but never written into or sent. Likely related to an unimplemented +DataType (possibly 3). Purpose unknown. + +**Current state on develop**: DataTypes 1, 4, 5, 6 implemented. DataType 2 (rewards) +returns empty. `Unk0`, `Unk4`, and `TenrouiraiKeyScore` are unresolved. + +--- + +### `MsgMhfPostTenrouirai` — Client → Server → Client + +Submits Tenrouirai results or donates guild RP. + +**Request** (`msg_mhf_post_tenrouirai.go`): +``` +AckHandle uint32 +Unk0 uint8 +Op uint8 — 1 = submit mission results, 2 = donate RP +GuildID uint32 +Unk1 uint8 — unknown + +Op=1 fields: + Floors uint16 — floors climbed this run + Antiques uint16 — antiques collected + Chests uint16 — chests opened + Cats uint16 — Felynes saved + TRP uint16 — TRP obtained + Slays uint16 — monsters slain + +Op=2 fields: + DonatedRP uint16 — RP to donate + PreviousRP uint16 — prior RP total (from client; used for display only?) + Unk2_0–3 uint16 — unknown; 4 reserved fields +``` + +**Critical gap — Op=1 does nothing**: the handler logs the fields in debug mode and +returns a success ACK, but **does not write any data to the database**. Mission scores +(`guild_characters.tower_mission_1/2/3`) are never updated from quest results. This means +Tenrouirai missions can never actually advance via normal gameplay — the `SUM` aggregation +in DataType=4 will always return zero. + +To fix: the handler needs to determine which of the three active missions the current run +contributes to (based on mission type and the run's stats), then write to the appropriate +`tower_mission_N` column. + +**Op=2 (RP donation)**: implemented. Deducts RP from character save data, updates +`guilds.tower_rp`, and advances the mission page when the cumulative donation threshold +is met. `Unk0`, `Unk1`, and `Unk2_0-3` are parsed but unused. + +**Current state on develop**: Op=2 fully implemented. Op=1 is a no-op. + +--- + +### `MsgMhfGetGemInfo` — Client → Server → Client + +Fetches gem inventory or gem acquisition history. + +**Request** (`msg_mhf_get_gem_info.go`): +``` +AckHandle uint32 +QueryType uint32 — 1=gem inventory, 2=gem history +Unk1 uint32 — unknown +Unk2–Unk6 int32 — unknown; 5 additional fields +``` + +**QueryType=1 response**: 30 frames (one per gem slot): +``` +[uint16] Gem — encoded as (tier << 8) | (index_within_tier + 1) +[uint16] Quantity +``` + +**QueryType=2 response**: N frames (gem history): +``` +[uint16] Gem +[uint16] Message — purpose unknown; likely a display string ID +[uint32] Timestamp — Unix timestamp +[14 bytes] Sender — null-padded character name +``` + +**Current state on develop**: QueryType=1 implemented via `towerRepo.GetGems()`. +QueryType=2 returns empty (the `GemHistory` slice is never populated). `Unk1`–`Unk6` +are parsed but unused; purpose unknown. + +--- + +### `MsgMhfPostGemInfo` — Client → Server → Client + +Adds or transfers gems. + +**Request** (`msg_mhf_post_gem_info.go`): +``` +AckHandle uint32 +Op uint32 — 1=add gem, 2=transfer gem +Unk1 uint32 — unknown +Gem int32 — gem ID encoded as (tier << 8) | (index+1) +Quantity int32 — amount +CID int32 — target character ID (Op=2 likely uses this) +Message int32 — display message ID? purpose unknown +Unk6 int32 — unknown +``` + +**Op=1 (add gem)**: implemented. Decodes the gem index from the `Gem` field, increments +the quantity in the CSV, and saves. Note: the index computation `(pkt.Gem >> 8 * 5) + (pkt.Gem - pkt.Gem&0xFF00 - 1%5)` may have operator precedence issues — verify with captures. + +**Op=2 (transfer gem)**: not implemented. Handler comment: *"no way im doing this for now"*. +The `CID` field likely identifies the recipient character. Format of the response (if any +acknowledgement is sent to the recipient) is unknown. + +**Current state on develop**: Op=1 implemented via `towerService.AddGem()`. Op=2 stub. +`Unk1`, `Message`, `Unk6` purposes unknown. + +--- + +### `MsgMhfPresentBox` — Client → Server → Client + +Fetches or claims items from the Tower present box (a reward inbox for seibatsu/Tower +milestone awards). This packet's field names differ between `develop` and `wip/tower`: + +**Request** — `wip/tower` naming (more accurate than develop's all-`Unk*` version): +``` +AckHandle uint32 +Unk0 uint32 +Operation uint32 — 1=open list, 2=claim item, 3=close +PresentCount uint32 — number of PresentType entries that follow +Unk3 uint32 — unknown +Unk4 uint32 — unknown +Unk5 uint32 — unknown +Unk6 uint32 — unknown +PresentType []uint32 — array of present type IDs (length = PresentCount) +``` + +On `develop`, `Operation` is `Unk1` and `PresentCount` is `Unk2` — the field is correctly +used to drive the `for` loop but the semantic name is lost. + +**Response** for Op=1 and Op=2: N frames via `doAckEarthSucceed`, each: +``` +[uint32] ItemClaimIndex — unique claim ID +[int32] PresentType — echoes the request PresentType +[int32] Unk2–Unk7 — 6 unknown fields (always 0 in captures) +[int32] DistributionType — 7201=item, 7202=N-Points, 7203=guild contribution +[int32] ItemID +[int32] Amount +``` + +**Critical gap — no claim tracking**: `ItemClaimIndex` is a sequential ID that the client +uses for "claimed" state, but the server has no DB table or flag for it. Every call +returns the same hardcoded items, so a player can claim the same rewards repeatedly. + +**Op=3**: returns an empty buffer (close/dismiss). + +**Current state on develop**: handler returns an empty item list (`data` slice is nil). +The `wip/tower` branch has a working hardcoded handler (7 dummy items per `PresentType`) +with the correct response structure. `Unk0`, `Unk3`–`Unk6` purposes unknown. + +**Note**: `wip/tower`'s `MsgMhfPresentBox.Parse()` still contains `fmt.Printf` debug +print statements that must be removed before any merge. + +--- + +### `MsgMhfGetNotice` — Client → Server → Client + +Purpose unknown. Likely fetches in-lobby Tower notices or announcements. + +**Request** (`msg_mhf_get_notice.go`): +``` +AckHandle uint32 +Unk0 uint32 +Unk1 uint32 +Unk2 int32 +``` + +**Current state**: Stub on `develop` — returns `{0, 0, 0, 0}`. Response format unknown. + +--- + +### `MsgMhfPostNotice` — Client → Server → Client + +Purpose unknown. Likely submits a read-receipt or acknowledgement for a Tower notice. + +**Request** (`msg_mhf_post_notice.go`): +``` +AckHandle uint32 +Unk0 uint32 +Unk1 uint32 +Unk2 int32 +Unk3 int32 +``` + +**Current state**: Stub on `develop` — returns `{0, 0, 0, 0}`. Response format unknown. + +--- + +## What Is Already Working on Develop + +- Character tower data (TR, TRP, TSP, skills, floor counts) is read and written via the + full repository pattern. +- Skill levelling (InfoType=2) deducts TSP and increments the correct CSV index. +- Floor progress (InfoType=1,7) updates TR, TRP, TSP, and block1. +- All 33 Tenrouirai mission definitions are hardcoded and served correctly. +- Guild Tenrouirai progress (page, aggregated mission scores) is read and score-capped. +- Per-character Tenrouirai leaderboard (DataType=5) is read from DB. +- Guild tower RP donation (Op=2) deducts player RP, accumulates guild RP, and advances + the mission page when the threshold is met. +- Gem inventory (QueryType=1) is read and returned correctly. +- Gem add (Op=1) updates the CSV at the correct index. + +--- + +## What Needs RE or Implementation + +### Functional bugs (affect gameplay today) + +| Issue | Location | Notes | +|-------|----------|-------| +| `PostTenrouirai` Op=1 is a no-op | `handlers_tower.go` | Mission scores are never written; Tenrouirai cannot advance via normal play | +| `block2` never written | `repo_tower.go → UpdateProgress` | G7+ floor count not persisted; requires captures to confirm which InfoType sends it | +| `PresentBox` returns empty list | `handlers_tower.go` | No items are ever shown; `wip/tower` handler logic can be adapted | +| Present claim tracking absent | DB schema | No "claimed" flag; players can re-claim indefinitely once handler is populated | + +### Unknown packet fields (need captures to resolve) + +| Field | Packet | Notes | +|-------|--------|-------| +| `Unk9 int64` vs `TimeTaken int32 + CID int32` | `MsgMhfPostTowerInfo` | `wip/tower` splits this into two int32; likely correct — needs capture confirmation | +| `Unk1`, `Unk6`, `Unk7` | `MsgMhfPostTowerInfo` | Logged in debug but unused; may encode run metadata | +| `Unk0`, `Unk1` | `MsgMhfGetTowerInfo` | Never used in handler | +| `TowerInfoLevel.Unk1–Unk3` | `GetTowerInfo` InfoType=3,5 | 3 of 4 level-entry fields zero-filled; may be max floor, session count, display state | +| `TowerInfoHistory` 10 × int16 | `GetTowerInfo` InfoType=4 | Two groups of 5; semantics unknown (recent clear times? floor high scores?) | +| InfoType 3 vs 5 distinction | `MsgMhfGetTowerInfo` | Same code path; difference not understood | +| InfoType 7 vs 1 distinction | `MsgMhfPostTowerInfo` | Both update progress; difference not understood | +| `Unk0`, `Unk4` | `MsgMhfGetTenrouirai` | Always 0 in known captures | +| `TenrouiraiKeyScore` | `GetTenrouirai` | Struct defined, never sent; likely an unimplemented DataType | +| `Unk0`, `Unk1`, `Unk2_0–3` | `MsgMhfPostTenrouirai` | 6 parsed fields that are never used | +| `Unk1–Unk6` | `MsgMhfGetGemInfo` | 6 extra fields; may filter by tier, season, or character | +| `Message`, `Unk1`, `Unk6` | `MsgMhfPostGemInfo` | `Message` likely a display string ID for gem transfer notices | +| `Unk0`, `Unk3–Unk6` | `MsgMhfPresentBox` | 5 unknown request fields | +| All fields | `MsgMhfGetNotice`, `MsgMhfPostNotice` | Both packets entirely uncharacterized | + +### Missing features (require further RE + design) + +| Feature | Notes | +|---------|-------| +| Tenrouirai mission score submission | `PostTenrouirai` Op=1 needs to map run stats to the correct mission type and write `tower_mission_N` | +| Tenrouirai rewards (DataType=2) | `TenrouiraiReward` response format is known; item IDs and quantities are not | +| Gem transfer (PostGemInfo Op=2) | Recipient lookup via `CID`; likely requires a notification to the target session | +| Gem history (GetGemInfo QueryType=2) | Response structure is known; DB storage is not — would require a `gem_history` table | +| PresentBox claim tracking | Needs a `present_claims` table or a bitfield on the character | +| Notice system | Both Get/Post are stubs; may be Tower bulletin board or reward notifications | + +--- + +## Relation to the Conquest War Doc + +`docs/conquest-war.md` covers the `GetWeeklySeibatuRankingReward` handler which already +implements the Tower dure kill reward table (Op=5, ID 260001) and the Tower floor reward +table (Op=5, ID 260003). Those are not missing here — they live in `handlers_seibattle.go` +on the `feature/conquest` branch. When that branch is eventually integrated, ensure the +Tower floor reward data is preserved. diff --git a/docs/unimplemented.md b/docs/unimplemented.md index fed55e492..ec50db3d2 100644 --- a/docs/unimplemented.md +++ b/docs/unimplemented.md @@ -192,7 +192,7 @@ that needs no reply). Others are genuine feature gaps. | `feature/enum-event` | 4 | `EnumerateEvent` scheduling only — not mergeable, see `docs/fort-attack-event.md` | | `feature/conquest` | 4 | Conquest quest handlers — not mergeable, see `docs/conquest-war.md` | | `feature/hunting-tournament` | 7 | `EnumerateRanking` / `EnumerateOrder` — not mergeable (duplicates handlers_tournament.go), see `docs/hunting-tournament.md` | -| `feature/tower` | 4 | Tower handlers | +| `feature/tower` | 4 | Tower handlers — superseded by direct integration; see `docs/tower.md` for gaps | ---