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.
21 KiB
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
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 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.