Files
Erupe/docs/tower.md
Houmgaor 842426e001 docs(tower): document Sky Corridor system status and remaining gaps
Most of the Tower/Tenrouirai system is already implemented on develop.
This doc captures what works, what is broken (PostTenrouirai Op=1 no-op,
block2 never written, PresentBox empty), and the remaining unknown packet
fields — so future contributors know exactly where to focus effort.

Also updates unimplemented.md to note the feature/tower branch was
superseded by direct integration into develop.
2026-03-22 20:35:56 +01:00

512 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 24 randomly selected floors.
- Each floor contains 23 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 Unk1Unk3 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 — 13
[uint8] Mission — type (16, see table above)
[uint16] Goal — score required
[uint16] Cost — RP cost to unlock/advance
[uint8] Skill16 — 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_03 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
Unk2Unk6 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] Unk2Unk7 — 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.Unk1Unk3` | `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_03` | `MsgMhfPostTenrouirai` | 6 parsed fields that are never used |
| `Unk1Unk6` | `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`, `Unk3Unk6` | `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.