diff --git a/docs/conquest-war.md b/docs/conquest-war.md new file mode 100644 index 000000000..0881802a9 --- /dev/null +++ b/docs/conquest-war.md @@ -0,0 +1,514 @@ +# Conquest War (討伐征戦 / Seibatsu) + +Tracks what is known about the Conquest War event system and what remains to be +reverse-engineered before it can be fully implemented in Erupe. + +The `feature/conquest` branch (origin) attempted a partial implementation but drifted too far +from `develop` without completing the core gameplay loop and is not mergeable in its current +state. Its findings are incorporated below. + +--- + +## Game Context + +**Conquest War** (討伐征戦, also called *Seibatsu*) is a weekly rotating time-limited event +introduced in the G2 update (July 2013). Players hunt legendary monsters and race to level them +up on a per-player leaderboard. + +The event follows a **three-week, three-phase cycle** tracked server-side as the "Earth" system: + +| Week | Phase | Japanese | Description | +|------|-------|----------|-------------| +| 1 | **Conquest (Seibatsu)** | 討伐征戦 | Hunting phase — players level their monsters | +| 2 | **Pallone Festival** | パローネ祭典 | Side festival event concurrent with conquest rewards | +| 3 | **Tower (Dure)** | 塔 | Tower climbing event for additional rewards | + +### Conquest Mechanics + +- Each player has their own independent monster (not shared with others). +- Players hunt their own monster or join quests at the same or higher level. +- A monster starts at level 1 and caps at **9999**. +- Level gain per hunt: **+5** (no faints), **+3** (one faint), **+1** (multiple faints). +- As the monster levels, its stats scale up, making each subsequent hunt harder. +- At week end, rewards are distributed based on the player's rank on the per-monster + leaderboard. + +### Target Monsters (configurable) + +The live service used **Shantien**, **Disufiroa**, **G-Rank Black Fatalis**, and +**G-Rank Crimson Fatalis**. The branch defaults to monster IDs `[116, 107, 2, 36]` +(Deviljho, Rajang, Rathalos, Gore Magala — suitable for G8 and below, where the original +four are not available). + +For clients at `RealClientMode <= G8`, only the first 3 monsters are exposed; G9+ exposes 4. + +### Reward Distribution Types + +The `DistributionType` field in reward packets uses these sentinel values: + +| Value | Meaning | +|-------|---------| +| `7201` | Item reward (ItemID + quantity) | +| `7202` | N-Points (currency) | +| `7203` | Guild contribution points | + +--- + +## Packet Overview + +Thirteen packets implement the Conquest/Earth system. All live in `network/mhfpacket/`. +None have `Build()` implemented (all return `NOT IMPLEMENTED`) — responses are built +directly in handler code using `byteframe`. + +### `MsgMhfGetEarthStatus` — Client → Server → Client + +Fetches the current Earth event windows and which monsters are active. + +**Request** (`msg_mhf_get_earth_status.go`): +``` +AckHandle uint32 +Unk0 uint32 — unknown; never used by handler +Unk1 uint32 — unknown; never used by handler +``` + +**Response** (built in `handlers_earth.go → handleMsgMhfGetEarthStatus`): +``` +for each active earth event (up to 3: Conquest, Pallone, Tower): + [uint32] StartTime — Unix timestamp + [uint32] EndTime — Unix timestamp + [int32] StatusID — 1 or 2 (Conquest); 11 (Pallone active) or 12 (Pallone reward); 21 (Tower) + [int32] EarthID — unique event ID from DB row + [int32] MonsterID × N — active conquest monsters (3 for G8, 4 for G9+) +``` + +**Status ID semantics**: the difference between `1` and `2` for the Conquest phase is not +known. The branch selects `1` when the hunt week is active and `2` otherwise, but this is +a guess. + +**Current state**: Implemented. Event windows are generated from a single `events` table row +(`event_type = 'earth'`). A 21-day rolling cycle is computed from that row's `start_time`. +Debug mode (`EarthDebug = true`) collapses the windows to week boundaries for faster testing. + +--- + +### `MsgMhfGetEarthValue` — Client → Server → Client + +Fetches numeric values associated with the current Earth event (kill counts, floor tallies, +special flags). + +**Request** (`msg_mhf_get_earth_value.go`): +``` +AckHandle uint32 +Unk0 uint32 — unknown +Unk1 uint32 — unknown +ReqType uint32 — 1, 2, or 3 (see below) +Unk3–Unk6 uint32 — unknown; never used by handler +``` + +**Response**: a variable-length array of 6-uint32 entries, wrapped in `doAckEarthSucceed`. +Each entry: `[ID, Value, Unk, Unk, Unk, Unk]`. The last four fields are always zero in +known captures. + +| ReqType | Known entries | Notes | +|---------|--------------|-------| +| 1 | `{1, 100}`, `{2, 100}` | Block + DureSlays count — exact meaning unclear | +| 2 | `{1, 5771}`, `{2, 1847}` | Block + Floors? — "Floors?" is a guess | +| 3 | `{1001, 36}` getTouhaHistory; `{9001, 3}` getKohouhinDropStopFlag; `{9002, 10, 300}` getKohouhinForceValue | `ttcSetDisableFlag` relationship unknown | + +**Current state**: Implemented with hardcoded values. No database persistence. + +--- + +### `MsgMhfReadBeatLevel` — Client → Server → Client + +Reads the player's current conquest beat levels (monster progress values) from the server. + +**Request** (`msg_mhf_read_beat_level.go`): +``` +AckHandle uint32 +Unk0 uint32 — always 1 in the JP client (hardcoded literal) +ValidIDCount uint32 — always 4 in the JP client +IDs [16]uint32 — always [0x74, 0x6B, 0x02, 0x24, 0, 0, ...] (hardcoded) +``` + +**Response**: `ValidIDCount` entries of `[ID uint32, Value uint32, 0 uint32, 0 uint32]`. +Default value if no DB data: `{0,1, 0,1, 0,1, 0,1}` (level 1 for each slot). + +**Current state**: Fully implemented. Beat levels are read from `characters.conquest_data` +(16-byte BYTEA). Defaults to level 1 if the column is NULL. + +--- + +### `MsgMhfUpdateBeatLevel` — Client → Server → Client + +Saves the player's updated conquest beat levels after a quest. + +**Request** (`msg_mhf_update_beat_level.go`): +``` +AckHandle uint32 +Unk1 uint32 — unknown +Unk2 uint32 — unknown +Data1 [16]int32 — unknown purpose; entirely discarded by the handler +Data2 [16]int32 — beat level data; only first 4 values are stored +``` + +**Response**: `{0x00, 0x00, 0x00, 0x00}`. + +**Current state**: Implemented, but incomplete. Only `Data2[0..3]` is written to the DB. +`Data1` and `Data2[4..15]` are silently ignored. `Unk1`/`Unk2` purposes are unknown. + +--- + +### `MsgMhfReadBeatLevelAllRanking` — Client → Server → Client + +Fetches the global leaderboard for a given monster. + +**Request** (`msg_mhf_read_beat_level_all_ranking.go`): +``` +AckHandle uint32 +Unk0 uint32 +MonsterID int32 — which monster's ranking to fetch +Unk2 int32 — unknown +``` + +**Response structure** (from known captures): +``` +[uint32] Unk +[int32] Unk +[int32] Unk +for each of 100 entries: + [uint32] Rank + [uint32] Level + [32 bytes] HunterName (null-padded) +``` + +**Current state**: Stubbed. Returns 100 zero-filled entries. No database ranking data exists. + +--- + +### `MsgMhfReadBeatLevelMyRanking` — Client → Server → Client + +Fetches the player's own rank on the conquest leaderboard. + +**Request** (`msg_mhf_read_beat_level_my_ranking.go`): +``` +AckHandle uint32 +Unk0 uint32 +Unk1 uint32 +Unk2 [16]int32 — unknown; possibly the same ID array as ReadBeatLevel +``` + +**Current state**: Stubbed. Returns an empty buffer. Response format unknown. + +--- + +### `MsgMhfReadLastWeekBeatRanking` — Client → Server → Client + +Purpose is partially understood: the handler comment says "controls the monster headings for +the other menus". Likely provides context for which monster's data to display. + +**Request** (`msg_mhf_read_last_week_beat_ranking.go`): +``` +AckHandle uint32 +Unk0 uint32 +EarthMonster int32 +``` + +**Response** (current stub): `[EarthMonster uint32, 0, 0, 0]`. Actual format unknown. + +**Current state**: Minimal stub. Response structure not reverse-engineered. + +--- + +### `MsgMhfGetBreakSeibatuLevelReward` — Client → Server → Client + +Returns per-monster level-break milestone rewards (items granted at specific level thresholds). + +**Request** (`msg_mhf_get_break_seibatu_level_reward.go`): +``` +AckHandle uint32 +Unk0 uint32 — unknown; debug-printed but never used +EarthMonster int32 +``` + +**Response**: variable-length array of reward entries via `doAckEarthSucceed`: +``` +[int32] ItemID +[int32] Quantity +[int32] Level — the level threshold at which this reward unlocks +[int32] Unk — always 0 in known data +``` + +**Current state**: Implemented with hardcoded per-monster reward tables. Item IDs were +derived from packet captures. No database backend. + +--- + +### `MsgMhfGetWeeklySeibatuRankingReward` — Client → Server → Client + +Returns reward tables for conquest ranking, Pallone Festival routes, and Tower floors. +The most complex handler in the branch. + +**Request** (`msg_mhf_get_weekly_seibatu_ranking_reward.go`): +``` +AckHandle uint32 +Unk0 uint32 — unknown; debug-printed but never used +Operation uint32 — 1 = conquest ranking, 3 = Pallone festival, 5 = event rewards +ID uint32 — event/route ID (for Op=1: aligns with EarthStatus 1 and 2) +EarthMonster uint32 +``` + +**Response format for Operation = 1** (conquest ranking rewards): +``` +per entry: + [int32] Unk0 + [int32] ItemID + [uint32] Amount + [int32] PlaceFrom + [int32] PlaceTo +``` + +**Response format for Operations 3 and 5** (Pallone/Tower): +``` +per entry: + [int32] Index0 — floor number (Op=5) or place rank (Op=3) + [int32] Index1 + [uint32] Index2 — distribution slot (Op=5 tower dure: 1 or 2) + [int32] DistributionType — 7201/7202/7203 + [int32] ItemID + [int32] Amount +``` + +**Current state**: Implemented with hardcoded tables derived from packet captures. +- Operation 1: All four monsters return the same bracket table (ranks 1–100, 101–1000, + 1000–1001). The tables are identical for all monsters — this may be correct, or captures + were only recorded for one monster. +- Operation 3 (Pallone): 91 entries across 11 routes, all zero-filled — format is known + but content is not. +- Operation 5 (Tower): Tower dure kill rewards (260001) and 155-entry floor reward table + (260003, floors 1–1500) are hardcoded from captures. + +Note in source: "Can only have 10 in each dist" — the maximum entries per distribution slot +before the client discards them is 10. + +--- + +### `MsgMhfGetFixedSeibatuRankingTable` — Client → Server → Client + +Returns a static "fixed" leaderboard (likely a seeded/display ranking, not live player data). +The handler notes this packet is *not* triggered when `EarthStatus == 1`, suggesting it +belongs to the reward-week display rather than the hunt-week display. + +**Request** (`msg_mhf_get_fixed_seibatu_ranking_table.go`): +``` +AckHandle uint32 +Unk0 uint32 — unknown +Unk1 int32 — unknown +EarthMonster int32 +Unk3 int32 — unknown +Unk4 int32 — unknown +``` + +**Response**: up to 9 entries: +``` +[int32] Rank +[int32] Level +[32 bytes] HunterName (null-padded) +``` + +**Current state**: Implemented with 9 hardcoded "Hunter N" placeholder entries per monster. +`Unk1`, `Unk3`, `Unk4` purposes unknown. + +--- + +### `MsgMhfGetSeibattle` — Client → Server → Client + +Fetches Seibattle (guild-vs-guild battle) data. The `GuildID` field suggests this is +guild-specific, but the handler ignores it entirely. + +**Request** (`msg_mhf_get_seibattle.go`): +``` +AckHandle uint32 +Unk0 uint8 +Type uint8 — 1=timetable, 3=key score, 4=career, 5=opponent, 6=convention result, + 7=char score, 8=cur result +GuildID uint32 +Unk3 uint8 — unknown +Unk4 uint16 — unknown +``` + +**Response**: varies by `Type`. Timetable (Type=1) returns 3 eight-hour battle windows +computed from midnight. All other types return zero-filled structs. + +**Current state**: Stubbed. No database queries, no guild-specific data. The seibattle +guild-vs-guild combat system is entirely unimplemented. + +--- + +### `MsgMhfPostSeibattle` — Client → Server → Client + +Submits a seibattle result. All fields are unknown. + +**Request** (`msg_mhf_post_seibattle.go`): +``` +AckHandle uint32 +Unk0 uint8 +Unk1 uint8 +Unk2 uint32 +Unk3 uint8 +Unk4 uint16 +Unk5 uint16 +Unk6 uint8 +``` + +**Current state**: Stubbed. Returns `{0,0,0,0}`. No data is read or persisted. + +--- + +### `MsgMhfGetAdditionalBeatReward` — Client → Server → Client + +Purpose unclear. The handler comment states: *"Actual responses in packet captures are all +just giant batches of null bytes. I'm assuming this is because it used to be tied to an +actual event that no longer triggers in the client."* + +**Request** (`msg_mhf_get_additional_beat_reward.go`): +``` +AckHandle uint32 +Unk0–Unk3 uint32 — all unknown +``` + +**Current state**: Returns 260 (`0x104`) zero bytes. Whether real responses were ever +non-zero is unknown. + +--- + +## Database Schema + +The branch adds two migrations: + +```sql +-- schemas/patch-schema/23-earth.sql +ALTER TYPE event_type ADD VALUE 'earth'; + +-- schemas/patch-schema/24-conquest.sql +ALTER TABLE public.characters ADD COLUMN IF NOT EXISTS conquest_data BYTEA; +``` + +And seeds four conquest quests (`schemas/bundled-schema/ConquestQuests.sql`): +quest IDs `54257`, `54258`, `54277`, `54370` — all `quest_type = 33`, `max_players = 0`. + +**Missing tables** required for a full implementation: + +| Table | Purpose | +|-------|---------| +| `conquest_rankings` | Per-player, per-monster beat level leaderboard | +| `conquest_reward_claims` | Track which level-break and ranking rewards have been claimed | +| `seibattle_scores` | Guild seibattle results and career records | +| `seibattle_schedules` | Persistent timetable (currently computed in memory) | + +--- + +## Configuration + +Two keys were added to `config.go` / `config.json` by the branch: + +| Key | Type | Default | Purpose | +|-----|------|---------|---------| +| `EarthDebug` | bool | `false` | Collapses event windows to week boundaries for testing | +| `EarthMonsters` | []int32 | `[116, 107, 2, 36]` | Active conquest target monster IDs | + +--- + +## What Is Already Understood + +- The three-phase Earth event cycle (Conquest → Pallone → Tower) and its 21-day rolling + window, keyed to a single `events` row. +- `GetEarthStatus` response wire format: per-phase `[Start, End, StatusID, EarthID, MonsterIDs…]`. +- `ReadBeatLevel` request is fully hardcoded by the JP client (IDs `0x74, 0x6B, 0x02, 0x24`); + no dynamic ID resolution is needed. +- Per-character beat level storage: 4 × int32, 16 bytes, in `characters.conquest_data`. +- Level-break reward item IDs and quantities for monsters 116, 107, 2, 36 (from captures). +- Weekly ranking reward brackets for conquest (ranks 1–100, 101–1000, 1000–1001). +- Tower floor reward table (floors 1–1500, item IDs and quantities from captures). +- Tower dure kill reward distributions (dist 1 and 2, from captures). +- `GetWeeklySeibatuRankingReward` response wire format for all three operations. +- `GetFixedSeibatuRankingTable` response wire format (rank + level + 32-byte name). +- `GetBreakSeibatuLevelReward` response wire format (ItemID + Quantity + Level + Unk). +- Distribution type sentinels: `7201` = item, `7202` = N-Points, `7203` = guild contribution. +- The 10-entry-per-distribution-slot limit in weekly seibatu ranking rewards. +- `GetSeibattle` timetable format: 3 × 8-hour windows from midnight. +- Conquest quest IDs: `54257`, `54258`, `54277`, `54370` (type 33). + +--- + +## What Needs RE Before Full Implementation + +### High Priority — blocks any functional gameplay + +| Unknown | Where to look | Notes | +|---------|---------------|-------| +| Semantics of `EarthStatus` IDs 1 vs 2 | Packet captures during hunt week vs reward week | Currently guessed; wrong selection may break phase detection | +| `MsgMhfUpdateBeatLevel.Data1[16]` | Captures with known quest outcome | Second int32 array entirely discarded; may carry the level gain delta | +| `MsgMhfUpdateBeatLevel.Unk1 / Unk2` | Same captures | May carry monster ID or quest ID needed for routing | +| `ReadBeatLevelAllRanking` response structure | Captures from an active leaderboard | Header fields (3 × uint32 before the 100 entries) unknown | +| `ReadBeatLevelMyRanking` response structure | Same | Format entirely unknown; returns empty today | +| `ReadLastWeekBeatRanking` full response | Captures after week rollover | Only monster ID echoed back today | + +### Medium Priority — required for accurate reward flow + +| Unknown | Where to look | Notes | +|---------|---------------|-------| +| `MsgMhfPostSeibattle` all fields (`Unk0–Unk6`) | Captures after a seibattle result | Handler does nothing today; this is the score submission path | +| `GetSeibattle` types 3–8 response formats | Captures for each `Type` value | Currently all return zero structs | +| `GetSeibattle.Unk0 / Unk3 / Unk4` | Same captures | Likely context selectors for guild/season | +| `GetEarthValue.Unk0 / Unk1 / Unk3–Unk6` | Captures across different event phases | 6 of the 8 request fields are unknown | +| `GetEarthStatus.Unk0 / Unk1` | Captures across phases | Never used by the handler; may be version or session flags | +| `GetWeeklySeibatuRankingReward` Op=3 content | Captures during Pallone Festival | 91 entries are zero-filled placeholders | +| Claim tracking semantics | Compare reward endpoint with gacha claim flow | No "claimed" flag exists anywhere in the schema | + +### Low Priority — cosmetic / completeness + +| Unknown | Where to look | Notes | +|---------|---------------|-------| +| `GetFixedSeibatuRankingTable.Unk1 / Unk3 / Unk4` | Captures | Likely unused alignment or version fields | +| `GetBreakSeibatuLevelReward.Unk0` | Captures with different monsters | Debug-printed; may be season or event ID | +| `ReadBeatLevelAllRanking.Unk0 / Unk2` | Captures | Likely pagination or season selectors | +| `GetAdditionalBeatReward` full structure | Captures if the packet is ever non-null | May be permanently dead in the last client version | +| Pallone Festival route semantics | JP wiki / community guides | 11 routes × 13 entries, all content unknown | +| Original live-service event scheduling cadence | JP wiki archives | Cycle length and reset time not publicly documented | + +--- + +## Relation to Other Systems + +**Gacha service** (`svc_gacha.go`): The reward distribution model (distribution type, +item ID, amount, rank brackets) is structurally similar to the gacha reward pipeline. +Conquest reward claiming can likely reuse or adapt `GachaService.ClaimRewards` and its +point transaction infrastructure. + +**Raviente siege** (`sys_channel_server.go`, `handlers_register.go`): Conquest quests may +use the same `MsgMhfRegisterEvent` / semaphore pattern for quest slot management, though +this has not been confirmed with captures. + +**Tower event** (`feature/tower` branch): The Tower phase is part of the same Earth event +cycle. The `GetWeeklySeibatuRankingReward` handler already covers Tower rewards (Op=5). +The two branches should be coordinated or merged. + +--- + +## Known Code Quality Issues in the Branch + +The following must be fixed before any part of this branch is merged: + +- `fmt.Printf` debug prints left in packet `Parse()` methods: + `msg_mhf_get_break_seibatu_level_reward.go`, `msg_mhf_get_weekly_seibatu_ranking_reward.go`, + `msg_mhf_get_fixed_seibatu_ranking_table.go`, `msg_mhf_read_last_week_beat_ranking.go` +- The `cleanupEarthStatus` function wipes `conquest_data` for all characters on event + expiry — this erases history. Completed conquest data should be archived, not deleted. +- The branch introduced a large `handlers.go` consolidation file that deleted existing test + files (`handlers_achievement_test.go`, `channel_isolation_test.go`, etc.). These must be + restored. +- DB access in `handlers_earth.go` uses raw `s.server.db` calls instead of the repo pattern. + Any merge must route these through `CharacterRepo` and `EventRepo` interfaces. +- `EarthMonsters` config currently accepts any IDs; a G9+ client will crash if fewer than 4 + monsters are configured when `RealClientMode > G8`. diff --git a/docs/unimplemented.md b/docs/unimplemented.md index df82c43de..b405632b8 100644 --- a/docs/unimplemented.md +++ b/docs/unimplemented.md @@ -190,7 +190,7 @@ that needs no reply). Others are genuine feature gaps. | Branch | Commits ahead | Handlers targeted | |--------|:---:|-------------------| | `feature/enum-event` | 4 | `EnumerateEvent` scheduling only — not mergeable, see `docs/fort-attack-event.md` | -| `feature/conquest` | 4 | Conquest quest handlers | +| `feature/conquest` | 4 | Conquest quest handlers — not mergeable, see `docs/conquest-war.md` | | `feature/tower` | 4 | Tower handlers | ---