Commit Graph

1623 Commits

Author SHA1 Message Date
Houmgaor
c43be33680 feat(shutdown): graceful drain + configurable countdown
Add ShutdownAndDrain to the channel server (issue #179 non-breaking
subset): on SIGTERM/SIGINT, force-close all active sessions so that
logoutPlayer runs for each one (saves character data, cleans up stages
and semaphores), then poll until the session map empties or a 30-second
context deadline passes.  Existing Shutdown() is unchanged.

Add ShutdownCountdownSeconds int config field (default 10) alongside
DisableSoftCrash so operators can tune the broadcast countdown without
patching code.  A zero value falls back to 10 for safety.

Fix pre-existing test failures: MsgMhfAddRewardSongCount has a complete
Parse() implementation so it no longer belongs in the "NOT IMPLEMENTED"
parse test list; its handler test is updated to pass a real packet and
assert an ACK response instead of calling with nil.
2026-03-21 01:36:31 +01:00
Houmgaor
366aad0172 docs(changelog): document Diva Defense system with attribution 2026-03-20 17:55:04 +01:00
Houmgaor
2bd92c9ae7 feat(diva): implement Diva Defense (UD) system
Add full Diva Defense / United Defense system: schema, repo layer,
i18n bead names, and RE-verified packet handler implementations.

Schema (0011_diva.sql): diva_beads, diva_beads_assignment,
diva_beads_points, diva_prizes tables; interception_maps/points
columns on guilds and guild_characters.

Seed (DivaDefaults.sql): 26 prize milestones for personal and
guild reward tracks (item_type=26 diva coins).

Repo (DivaRepo): 11 new methods covering bead assignment, point
accumulation, interception point tracking, prize queries, and
cleanup. Mocks wired in test_helpers_test.go.

i18n: Bead struct with EN/JP names for all 18 bead types (IDs
1–25). Session tracks currentBeadIndex (-1 = none assigned).

Packet handlers corrected against mhfo-hd.dll RE findings:
- GetKijuInfo: u8 count, 512-byte desc, color_id+bead_type per entry
- SetKiju: 1-byte ACK; persists bead assignment to DB
- GetUdMyPoint: 8×18-byte entries, no count prefix
- GetUdTotalPointInfo: u8 error + u64[64] + u8[64] + u64 (~585 B)
- GetUdSelectedColorInfo: u8 error + u8[8] = 9 bytes
- GetUdDailyPresentList: correct u16 count format (was wrong hex)
- GetUdNormaPresentList: correct u16 count format (was wrong hex)
- GetUdRankingRewardList: correct u16 count with u32 item_id/qty
- GetRewardSong: 22-byte layout with 0xFFFFFFFF prayer_end sentinel
- AddRewardSongCount: parse implemented (was NOT IMPLEMENTED stub)
2026-03-20 17:52:01 +01:00
Houmgaor
7ff033e36e docs(changelog): remove fix entries already covered by Added section
The rengoku/loadQuestFile fix notes were redundant — the behaviour is
already documented under the JSON loader Added entries.
2026-03-20 17:12:41 +01:00
Houmgaor
127975d0c9 docs: document architectural proposals from chore/reorg analysis
The chore/reorg branch drifted too far to merge cleanly. Captures the
worthwhile ideas as actionable proposals: clientctx removal, lazy packet
priority ordering, SessionStage interface, semaphoreIndex race fix, and
several medium/low-priority restructuring items.
2026-03-20 17:11:26 +01:00
Houmgaor
fec2793ccc docs: document JSON format support in README and CHANGELOG
Add a JSON Format Support section to the README covering quests,
scenarios, and Hunting Road config. Add two missing CHANGELOG entries
for the rengoku .bin-first fix and the quest event board JSON fallback.
2026-03-20 16:41:36 +01:00
Houmgaor
73904965ff feat(quest): add .json fallback in loadQuestFile for event quest board
JSON-authored quests were serveable via MSG_SYS_GET_FILE but invisible
on the event quest board because loadQuestFile only read .bin files.
CompileQuestJSON already produces the uncompressed binary layout that
the event-board parser expects, so no decompression step is needed.

Also update rengoku priority tests to match the .bin-first convention
fixed in the previous commit.
2026-03-20 16:39:43 +01:00
Houmgaor
c8ede52809 fix(rengoku): prefer .bin over .json, consistent with quest/scenario loaders
All three binary loaders (quests, scenarios, rengoku) now follow the same
priority: .bin first, .json as fallback. The previous reversed order
(.json first) was confusing for operators with both files present.
2026-03-20 16:35:06 +01:00
Houmgaor
ea51c63e0a refactor(scenario): annotate binary format constants and field roles
Add jkrMagic and scenarioChunkSizeLimit named constants, and expand
comments throughout scenario_json.go to reflect confirmed client
behaviour: the 0x8000-byte per-chunk limit, which C0 metadata fields
the parser actually reads (m[0]–m[6] only), the signed-offset
encoding used for C1 m[8]–m[17], and per-byte roles in the 8-byte
sub-header. Inline magic literal replaced with the named constant.
2026-03-20 16:16:59 +01:00
Houmgaor
9d3e33af8e docs(scenario): add Ghidra RE findings to format documentation
Document client-side parser behaviour confirmed from mhfo-hd.dll:
- Container: 0x8000-byte per-chunk limit enforced by FUN_11525c60
- C0 metadata: client reads only m[0]–m[6]; m[7]–m[9] are not read,
  so the constant-5 (m[8]) and the size-correlated m[9] are opaque to
  the parser and can be set to any value
- C0 m[2]/m[5] role clarified: m[2]=0 is offset to str0 (no-op),
  m[5]=str0_len is offset to str1 from strings section start
- C0 m[6] SceneRef stored as signed short; 0xFFFF = −1 for cat≠0
- C1 metadata: m[8]–m[17] are signed offsets — negative encodes
  position from the post-0xFF dialog data section via ~value formula,
  non-negative encodes position from the strings section start;
  m[18–19] are read as individual bytes, not u16 pairs
2026-03-20 16:12:47 +01:00
Houmgaor
7471e7eaa9 test(scenario): add real-file round-trip tests and decode metadata
Add TestScenarioRoundTrip_RealFiles covering 7 real scenario files
(cat=0/1/3, T101/T103, with and without chunk1). Tests skip gracefully
when game data is absent so CI stays green.

Decoded metadata structure from analysis of 145k+ real scenario files:
- Chunk0 m[0]/m[1] = CategoryID/MainID; m[5] = str0_len (offset to
  str1); m[6] = MainID (cat=0) or 0xFFFF; m[8] = constant 5.
- Chunk1 m[9]=cumOff[2], m[10]=cumOff[1], m[14]=cumOff[3],
  m[15]=total_string_bytes; m[11-13]/m[16-17] = dialog script offsets
  beyond the 0xFF sentinel; m[20] = constant 5; m[21] ≈ data_size.

Update docs/scenario-format.md with full field tables for both chunks.
2026-03-20 14:20:14 +01:00
Houmgaor
a1dfdd330a feat(scenario): add JSON scenario support and JKR type-3 compressor
Closes #172. Scenario files in bin/scenarios/ can now be authored as
.json instead of .bin — the server compiles them to wire format on
load, falling back to .bin if no .json is present.

- Add ParseScenarioBinary / CompileScenarioJSON in scenario_json.go;
  supports sub-header format (strings as UTF-8, metadata as base64),
  inline format, and raw JKR blobs.
- Add PackSimple JKR type-3 (LZ77) compressor in jpk_compress.go,
  ported from ReFrontier JPKEncodeLz.cs; round-trip tested against
  UnpackSimple.
- Fix off-by-one in processDecode (jpk.go): last literal byte was
  silently dropped for data that does not end on a back-reference.
- Wire loadScenarioBinary into handleMsgSysGetFile replacing the
  inline os.ReadFile call; mirrors the existing loadQuestBinary pattern.
- Rewrite docs/scenario-format.md with full container/sub-header spec
  and JSON schema examples.
2026-03-20 13:55:40 +01:00
Houmgaor
71b675bf3e docs: rename AUTHORS.md to HISTORY.md and update references
The file documents project lineage and development phases, not just
a flat list of authors — HISTORY.md is a more accurate name and
follows common open-source convention.
2026-03-20 11:56:19 +01:00
Houmgaor
220671e959 docs(authors): fix placement and trim Houmgaor/Mogapédia section 2026-03-20 11:53:35 +01:00
Houmgaor
1c20959b35 docs(authors): add Houmgaor/Mogapédia development phase (2025-present) 2026-03-20 11:52:10 +01:00
Houmgaor
e7180deb77 docs(changelog): reorder Unreleased sections to Added before Fixed 2026-03-20 11:49:27 +01:00
Houmgaor
34f0e89e7b fix(savedata): guard against sub-minimum backup data in recovery
nullcomp's passthrough path returns non-cmp-header data as-is without
error, which is correct for old uncompressed saves. However, a corrupt
backup slot containing garbage shorter than the minimum save layout
(100 bytes) would pass Decompress() and then panic in
updateStructWithSaveData() with a slice-bounds error at the name field
read (offset 88–100).

Add a minSaveSize check after each backup decompresses; skip the slot
if the result is too small. Also document the campaign system and the
fix in CHANGELOG under Unreleased.
2026-03-20 11:46:01 +01:00
Houmgaor
47a07ec52c Merge pull request #182 from Mezeporta/feature/event-tent-clean
feat(campaign): implement Event Tent campaign system
2026-03-20 11:35:56 +01:00
Houmgaor
793a4b4e03 fix(migrations): remove BEGIN/END from 0010_campaign.sql
Migration runner wraps each file in its own transaction via db.Begin().
Having an explicit BEGIN/END inside caused 'unexpected transaction status idle'.
Other migrations use no transaction wrapper.
2026-03-20 11:31:59 +01:00
Houmgaor
90d9b7915a fix(tests): update packet field names after campaign RE renaming 2026-03-20 11:26:40 +01:00
Houmgaor
77e7969579 feat(campaign): implement Event Tent campaign system
Adds the full campaign/event-tent feature:

Packet layer:
- MsgMhfApplyCampaign: Unk0/Unk1/Unk2 → CampaignID/Code (null-terminated 16-byte string)
- MsgMhfAcquireItem: Unk0/Length/Unk1 → RewardIDs []uint32
- MsgMhfEnumerateItem: remove Unk0/Unk1 (RE'd: zeroed + always-2, ignored)
- MsgMhfStateCampaign: Unk1 → NullPadding (RE'd: always zero)
- MsgMhfTransferItem: Unk0/Unk1/Unk2 → QuestID/ItemType/Quantity (RE'd)

Handler layer (handlers_campaign.go):
- handleMsgMhfEnumerateCampaign: reads campaigns, categories, links from DB;
  prefix moved into pascal string slot 3 of each event entry (RE confirmed
  3-section response format — removes spurious intermediate section)
- handleMsgMhfStateCampaign: returns stamp count and redeemable flag
- handleMsgMhfApplyCampaign: validates and records code redemption
- handleMsgMhfEnumerateItem: lists rewards gated by stamp count
- handleMsgMhfAcquireItem: marks rewards as claimed
- handleMsgMhfTransferItem: records campaign quest completion (item_type=9)

Quest gating (handlers_quest.go):
- makeEventQuest: for QuestTypeSpecialTool, check campaign stamp count and
  deadline before allowing the quest (WriteBool true/false)

Database:
- 0010_campaign.sql: 8-table schema (campaigns, categories, links, rewards,
  claimed, state, codes, quest)
- CampaignDemo.sql: community-researched live game campaign data
2026-03-20 11:22:25 +01:00
Houmgaor
97ef09be64 docs(changelog): add rengoku JSON config to Unreleased 2026-03-20 00:08:48 +01:00
Houmgaor
34335b023d feat(rengoku): support rengoku_data.json as editable config source
Operators can now define Hunting Road configuration in a plain JSON file
(rengoku_data.json) instead of maintaining an opaque pre-encrypted binary.
The JSON is parsed, validated, assembled into the binary layout, and
ECD-encrypted at startup; rengoku_data.bin is still used as a fallback.

JSON schema covers both road modes (multi/solo) with typed floor and
spawn-table entries — floor number, spawn-table index, point multipliers,
and per-slot monster ID/variant/weighting fields. Out-of-range references
are caught at load time before any bytes are written.
2026-03-20 00:07:34 +01:00
Houmgaor
5c2fde5cfd feat(rengoku): validate and log Hunting Road config on startup
Port ECD encryption/decryption from ReFrontier (C#) and FrontierTextHandler
(Python) into common/decryption. The cipher uses a 32-bit LCG key stream with
an 8-round Feistel-like nibble transformation and CFB chaining; all six key
sets are supported, key 4 being the default for all MHF files.

On startup, loadRengokuBinary now decrypts (ECD) and decompresses (JKR) the
binary to validate pointer bounds and entry counts, then logs a structured
summary (floor counts, spawn table counts, unique monster IDs). Failures are
non-fatal — the encrypted blob is still cached and served to clients unchanged,
preserving existing behaviour. Closes #173.
2026-03-19 23:59:34 +01:00
Houmgaor
08e7de2c5e feat(savedata): recover from rotating backups on hash mismatch
When primary savedata fails its SHA-256 integrity check, query
savedata_backups in recency order and return the first slot that
decompresses cleanly. Recovery is read-only — the next successful
Save() overwrites the primary with fresh data and a new hash,
self-healing the corruption transparently.

Closes #178
2026-03-19 19:28:30 +01:00
Houmgaor
6139e90968 docs(changelog): add quest JSON feature to Unreleased 2026-03-19 18:23:46 +01:00
Houmgaor
e827ecf7d4 feat(quests): implement all remaining binary sections in JSON format
Implement the 8 pointer-addressed sections that were previously written
as null pointers in the quest JSON compiler and parser:

- questAreaPtr → MapSections (ptMapSection pointer array + minion spawns)
- areaTransitionsPtr → AreaTransitions (per-zone floatSet arrays, 52B each)
- areaMappingPtr → AreaMappings (32-byte coordinate mapping entries)
- mapInfoPtr → MapInfo (qMapID + returnBC_ID, 8 bytes)
- gatheringPointsPtr → GatheringPoints (per-zone 24-byte gatheringPoint entries)
- areaFacilitiesPtr → AreaFacilities (per-zone facPoint blocks with sentinel)
- someStringsPtr → SomeString/QuestTypeString (two u32 ptrs + Shift-JIS data)
- gatheringTablesPtr → GatheringTables (pointer array → GatherItem[] lists)

Also set gatheringTablesQty and area1Zones in generalQuestProperties from
JSON data, and validate zone-length consistency between AreaTransitions,
GatheringPoints, and AreaFacilities arrays.

Round-trip tests cover all new sections to ensure compile(parse(bin)) == bin.
2026-03-19 18:20:00 +01:00
Houmgaor
c64260275b feat(quests): support JSON quest files alongside .bin
Adds human-readable JSON as an alternative quest format for bin/quests/.
The server tries .bin first (full backward compatibility), then falls back
to .json and compiles it to the MHF binary wire format on the fly.

JSON quests cover all documented fields: text (UTF-8 → Shift-JIS),
objectives (all 12 types), monster spawns, reward tables, supply box,
stages, rank requirements, variant flags, and forced equipment.

Also adds ParseQuestBinary for the reverse direction, enabling tools to
round-trip quests and verify that JSON-compiled output is bit-for-bit
identical to a hand-authored .bin for the same quest data.

49 tests: compiler, parser, 13 round-trip scenarios, and golden byte-
level assertions covering every section of the binary layout.
2026-03-19 17:56:50 +01:00
Houmgaor
0911d15709 docs: remove completed improvements plan
All 7 items are done. History is preserved in git.
2026-03-19 16:31:42 +01:00
Houmgaor
4605fdd6bb docs(stubs): annotate open branches on unimplemented handlers
Add branch references inline per handler and a summary table at the
bottom mapping each open branch to the handlers it targets.
2026-03-19 16:12:55 +01:00
Houmgaor
d2e35c3be3 fix(ci): exclude Docker build artifact from release download
download-artifact picked up the Docker build provenance artifact
(Mezeporta~Erupe~39YWA8.dockerbuild) which fails to extract after
5 retries. Filter to only *-amd64 artifacts.
v9.3.0
2026-03-19 15:57:51 +01:00
Houmgaor
24428c8409 fix(ci): remove www/ copy from release archive
www/ contains only a gitignored jp/ subdirectory with dev notes.
The directory is absent in CI, causing cp to fail on release builds.
2026-03-19 15:52:19 +01:00
Houmgaor
90948bfb71 chore(release): prepare 9.3.0
Promote [Unreleased] to [9.3.0] - 2026-03-19 in CHANGELOG and mark
the bookshelf save pointer fix as Done in improvements.md (it was
applied post-RC1 but the doc still said Pending).
2026-03-19 15:18:33 +01:00
Houmgaor
39d93f6eed docs(changelog): log G-rank Workshop/Cog softlock fix (#180) 2026-03-19 14:37:11 +01:00
Houmgaor
d27da5ec86 fix(items): stop G-rank Workshop/Cog softlock on missing ACK
MSG_MHF_GET_EXTRA_INFO (0xA6) and MSG_MHF_GET_COG_INFO (0xC3) had
Parse() returning NOT IMPLEMENTED. The dispatch loop treats any Parse
error as a hard drop — no ACK is ever sent, so the client waits
indefinitely and effectively soft-locks when entering the G-rank
Workshop or Master Felyne (Cog) screens.

Fix: parse AckHandle (the only field we can confirm from the protocol)
and respond with doAckBufFail so the client receives a well-formed
buf-type ACK with error code 1. The client's fail branch for these
requests exits cleanly without reading response fields, avoiding the
read-past-EOF crash that an empty success ACK would cause.

The full response format for both packets is still unknown; a complete
implementation requires further RE. The TODO comments mark the gap.

Fixes #180.
2026-03-19 14:35:38 +01:00
Houmgaor
7ea2660335 docs(stubs): annotate empty handlers and add unimplemented reference doc
Add // stub: unimplemented to 70 empty game-feature handlers and
// stub: reserved to 56 protocol-reserved slots in handlers_reserve.go,
making them discoverable via grep. Add docs/unimplemented.md listing all
unimplemented handlers grouped by subsystem with descriptions.
2026-03-19 10:57:09 +01:00
Houmgaor
a6025be8b7 fix(festa): filter trials and rewards for Forward.5 compatibility (#156)
Skip trials referencing monsters added after em106 (Odibatorasu, the
last F5 monster) and filter out item 7011 which does not exist before
G1, preventing client crashes on Forward.4/5 servers.

Also logs the pre-existing bookshelf save data pointer fix (already
applied during the savedata refactor) in the CHANGELOG.
2026-03-18 23:19:25 +01:00
Houmgaor
4fa6d5b05e docs: remove completed anti-patterns and technical-debt docs
All tracked items in both files have been resolved or accepted.
The content is superseded by CLAUDE.md architecture notes and commit
history; retaining fully-struck-through docs adds noise without value.
2026-03-18 22:44:26 +01:00
Houmgaor
835f97d3c2 fix(shutdown): force-stop on second SIGINT during countdown
A second Ctrl+C/SIGINT while the 10-second shutdown countdown is
running now exits immediately instead of being silently dropped.
The signal channel is buffered to 2 so the second signal is captured,
and the countdown select exits via os.Exit(1) on receipt.
2026-03-18 21:36:24 +01:00
Houmgaor
8785ebc21a fix(achievement): fix test failures from migration 0008 side effects
GetAllScores used SELECT * which broke when displayed_levels column was
added — now uses explicit column names. DisplayedAchievement handler
panicked on nil achievementService in empty-handler smoke tests — added
nil guard. Updated msg_build_test.go for renamed tactics point fields.
2026-03-18 12:14:31 +01:00
Houmgaor
792dcd5d91 feat(diva): implement Diva Defense point accumulation (#168)
RE'd putAdd_ud_point (FUN_114fd490) and putAdd_ud_tactics_point
(FUN_114fe9c0) from the ZZ client DLL via Ghidra decompilation.

MsgMhfAddUdPoint fields: QuestPoints (sum of 11 category accumulators
earned per quest) and BonusPoints (kiju prayer song multiplier extra).
MsgMhfAddUdTacticsPoint fields: QuestID and TacticsPoints.

Adds diva_points table (migration 0009) for per-character per-event
point tracking, with UPSERT-based atomic accumulation in the handler.
2026-03-18 12:09:44 +01:00
Houmgaor
61d85e749f feat(achievement): add rank-up notifications (#165)
RE'd putDisplayed_achievement from ZZ client DLL via Ghidra: the packet
sends opcode + 1 zero byte with no achievement ID, acting as a blanket
"I saw everything" signal.

Server changes:
- Track per-character last-displayed levels in new displayed_levels
  column (migration 0008)
- GetAchievement compares current vs displayed levels per entry
- DisplayedAchievement snapshots current levels to clear notifications
- Repo, service, mock, and 3 new service tests

Protbot changes:
- New --action achievement: fetches achievements, shows rank-up markers,
  sends DISPLAYED_ACHIEVEMENT, re-fetches to verify notifications clear
- Packet builders for GET/ADD/DISPLAYED_ACHIEVEMENT
2026-03-18 11:35:31 +01:00
Houmgaor
476882e1fb test(mercenary): cover SaveMercenary rastaID=0 guard (#163)
Verify that SaveMercenary with rastaID=0 preserves both an existing
numeric rasta_id and a NULL rasta_id, preventing the silent data
corruption that broke game state saving.
2026-03-18 10:48:52 +01:00
Houmgaor
6b5bbf6d0b fix(migrations): derive migration counts dynamically in tests
Hardcoded counts (5) broke CI when migrations 0006 and 0007 were added.
Use readMigrations() to compute expected values so future migrations
don't require test updates.
2026-03-17 19:33:11 +01:00
Houmgaor
197164bc94 docs: log savedata corruption defense in CHANGELOG
Merge migrations 0007 and 0008 into a single 0007_savedata_integrity
migration since neither has been released yet.
2026-03-17 19:30:43 +01:00
Houmgaor
01b829d0e9 feat(savedata): add tier 2 integrity protections
Strengthen savedata persistence against corruption and race conditions:

- SHA-256 checksum: hash the decompressed blob on every save, store in
  new savedata_hash column, verify on load to detect silent corruption.
  Pre-existing characters with no hash are silently upgraded on next save.
- Atomic transactions: wrap character data + house data + hash + backup
  into a single DB transaction via SaveCharacterDataAtomic, so a crash
  mid-save never leaves partial state.
- Per-character save mutex: CharacterLocks (sync.Map of charID → Mutex)
  serializes concurrent saves for the same character, preventing races
  that could defeat corruption detection. Different characters remain
  fully independent.

Migration 0008 adds the savedata_hash column to the characters table.
2026-03-17 19:21:55 +01:00
Houmgaor
d578e68b79 docs(gacha): clarify G1-GG vs ZZ configuration to prevent client crashes (#175)
The G1-GG gacha code path (PR #150) included example data in comments
that used non-zero item_type/item_number/item_quantity on entry_type=100
rows. Users copying these values for ZZ servers caused client crashes
because the ZZ client interprets those fields as material requirements.

- Replace unclear PR #150 comment with explicit WARNING against using
  G1-GG example values for ZZ servers
- Add gacha configuration guide header to GachaDemo.sql explaining the
  3-table system, entry types, and item type codes
- Add Mega Potion example showing correct way to add custom items
- Reference Enumerations.md for the full item type list
2026-03-17 19:11:59 +01:00
Houmgaor
b40217c7fe feat(savedata): add tier 1 data integrity protections
Prevent savedata corruption and denial-of-service by adding four layers
of protection to the save pipeline:

- Bounded decompression (nullcomp.DecompressWithLimit): caps output size
  to prevent OOM from crafted payloads that expand to exhaust memory
- Bounds-checked delta patching (deltacomp.ApplyDataDiffWithLimit):
  validates offsets before writing, returns errors for negative offsets,
  truncated patches, and oversized output; ApplyDataDiff now returns
  original data on error instead of partial corruption
- Size limits on save handlers: rejects compressed payloads >512KB and
  decompressed data >1MB before processing; applied to main savedata,
  platedata, and platebox diff paths
- Rotating savedata backups: 3 slots per character with 30-minute
  interval, snapshots the previous state before overwriting, backed by
  new savedata_backups table (migration 0007)
2026-03-17 19:03:43 +01:00
Houmgaor
5009a37d19 fix: create user_binary row on character creation (#176)
New characters were missing their user_binary record, preventing them
from entering their house. Both sign server and API character creation
paths now insert the row. A backfill migration fixes existing databases.
2026-03-16 17:11:55 +01:00
Houmgaor
1ae7dffe7b fix(gacha): add items to reward pool test entries
GetRewardPool now requires entries to have items (EXISTS clause from
2365b63), but the ordering test created entries without items.
2026-03-16 00:07:46 +01:00