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

21 KiB
Raw Blame History

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

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)

-- 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). Unk1Unk6 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:

Requestwip/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, Unk3Unk6 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.