Commit Graph

1648 Commits

Author SHA1 Message Date
Houmgaor
d1d3bb8698 chore(merge): merge main into develop
Resolves CHANGELOG.md conflict: preserve develop's [Unreleased] block,
insert the [9.3.1] section from main, remove the duplicate
DisableSaveIntegrityCheck entry that had been in [Unreleased].
2026-03-23 11:15:20 +01:00
Houmgaor
e1aa863a1f test(channelserver): reduce polling interval from 10ms to 1ms
LoopDelay is 0 in test servers (bare struct, no Viper defaults), so
sendLoop spins without delay and satisfies conditions in <1ms. The
10ms poll interval was the new bottleneck — dropping to 1ms cuts the
channelserver test suite from ~136s to ~5s.
2026-03-23 11:06:50 +01:00
Houmgaor
d0efc4e81a test(channelserver): replace time.Sleep with polling loops
Blind sleeps accumulate serially (no t.Parallel anywhere) and inflate
under the race detector's scheduling overhead — contributing to the
~136s channelserver test run time.

Replace ~75 arbitrary sleeps (50ms–1s) across 7 test files with 2s
polling loops that exit as soon as the expected condition holds. Sleeps
that are genuinely intentional (race-condition stress tests, cache
expiry, temporal spacing in timestamp tests, backpressure pacing) are
left untouched.
2026-03-23 10:57:01 +01:00
Houmgaor
0c6dc39371 ci: upgrade actions to Node.js 24-compatible versions
Node.js 20 actions are deprecated and will be forced to Node.js 24
starting June 2, 2026. Bump to versions that ship Node.js 24 runtimes:

  actions/checkout        v4 → v6
  actions/setup-go        v5 → v6
  golangci-lint-action    v7 → v9
  actions/upload-artifact v4 → v6
  actions/download-artifact v4 → v8
2026-03-23 10:36:31 +01:00
Houmgaor
635b9890c8 test(broadcast): fix flaky TestBroadcastMHFAllSessions under race detector
The fixed 100ms sleep was too short for sendLoop goroutines to drain
under the race detector's scheduling overhead, causing intermittent
count=4/want=5 failures. Replace with a 2s polling loop that exits
as soon as all sessions report delivery.
v9.3.1
2026-03-23 10:26:29 +01:00
Houmgaor
1f0544fd10 fix(savedata): add recovery hint to integrity check failure log
Admins importing saves from another server instance will hit the
hash mismatch error with no guidance. The log message now tells them
to set DisableSaveIntegrityCheck=true or null the hash for the
specific character via SQL.
2026-03-23 10:15:25 +01:00
Houmgaor
3803fd431b chore(release): prepare 9.3.1 2026-03-23 10:13:00 +01:00
Houmgaor
e6a415310f fix(startup): detect duplicate channel ports before binding
Catches misconfigured port collisions early and reports which channel
already claimed the port, rather than failing with a generic OS error.
2026-03-22 21:25:55 +01:00
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
Houmgaor
dca7152656 docs(hunting-tournament): document tournament RE gaps and branch status
Add hunting-tournament.md covering the 公式狩猟大会 system: game
context (cups, schedule, rewards), what is already implemented in
develop (handlers_tournament.go is mostly functional), and the four
remaining gaps requiring RE — quest clear time recording, ClanID
leaderboard filtering, AcquireTournament reward delivery, and guild
cup soul attribution to Mezeporta Festival.

Documents why feature/hunting-tournament is not mergeable (duplicate
handlers) and preserves its useful findings: ClanID field name on
EnumerateOrder and the festa timing corrections.

Update unimplemented.md open branches summary accordingly.
2026-03-22 20:24:09 +01:00
Houmgaor
93f8c677d9 docs(conquest): document Conquest War RE status and implementation gaps
The feature/conquest branch drifted too far from develop without completing
the core gameplay loop. This doc captures all known packet wire formats,
handler states, confirmed values from captures, and a prioritised table of
unknowns needed before the feature can be implemented cleanly.

Also updates unimplemented.md to link to the new reference.
2026-03-22 20:16:44 +01:00
Houmgaor
63312dac1b docs(fort-attack): document Interceptor's Base event RE status
Add fort-attack-event.md capturing everything known about the fort
attack event system (packet wire formats, register plumbing, DB schema
gap, quest IDs) and what needs reverse-engineering before implementation
is possible. The feature/enum-event branch covered only scheduling and
is not mergeable; its findings are preserved here for future reference.

Update unimplemented.md to point to the new doc and correctly describe
the branch scope.
2026-03-22 20:11:22 +01:00
Houmgaor
f03476e6e0 docs(unimplemented): remove merged branches from open branches summary
feature/return-guild and fix/clan-invites have been merged into develop.
Remove them from the open branches table and clean up the stale branch
reference in the ShutClient handler note.
2026-03-22 19:33:31 +01:00
Houmgaor
56a0173b40 fix(i18n): translate remaining English strings in lang_jp.go
Six strings were left in English in the original Japanese translation:
noOp, kqf.version, ban.* (all five fields), and ravi.version.
2026-03-22 17:11:12 +01:00
Houmgaor
05adda00d7 feat(i18n): add completeness test and fix missing JP strings
TestLangCompleteness uses reflection to walk the i18n struct and fail
on any empty string field, catching incomplete translations at CI time.
Running it immediately found three missing strings in lang_jp.go
(playtime, timer.enabled, timer.disabled), which are now filled in.
CHANGELOG updated with i18n refactor, FR/ES languages, and the new test.
2026-03-22 17:09:16 +01:00
Houmgaor
aff7953ab1 feat(i18n): add French and Spanish translations 2026-03-22 16:59:29 +01:00
Houmgaor
57583fd903 refactor(i18n): split language strings into one file per language
sys_language.go now contains only the i18n struct, Bead type, lookup
helpers, and a minimal getLangStrings dispatcher. All string data lives
in lang_en.go and lang_jp.go, making it straightforward to add a new
language by creating a single self-contained file.
2026-03-22 16:31:35 +01:00
Houmgaor
20ea925359 docs(changelog): add hunting tournament entry to Unreleased 2026-03-22 14:36:56 +01:00
Houmgaor
c714374289 feat(tournament): implement hunting tournament system end-to-end
Wire format for MsgMhfEnterTournamentQuest (0x00D2) derived from
mhfo-hd.dll binary analysis (FUN_114f4280). Five new tables back
the full lifecycle: schedule, cups, sub-events, player registrations,
and run submissions. All six tournament handlers are now DB-driven:

- EnumerateRanking: returns active tournament schedule with cups and
  sub-events; computes phase state byte from timestamps
- EnumerateOrder: returns per-event leaderboard ranked by submission
  time, with SJIS-encoded character and guild names
- InfoTournament: exposes tournament detail and player registration
  state across all three query types
- EntryTournament: registers player and returns entry handle used by
  the client in the subsequent EnterTournamentQuest packet
- EnterTournamentQuest: parses the previously-unimplemented packet and
  records the run in tournament_results
- AcquireTournament: stubs rewards (item IDs not yet reversed)

Seed data (TournamentDefaults.sql) reproduces tournament #150 cups and
sub-events so a fresh install has a working tournament immediately.
2026-03-22 14:30:37 +01:00
Houmgaor
5ee9a0e635 feat(guild): implement rookie and return guild assignment
New/returning players are now auto-assigned to temporary holding guilds
on MSG_MHF_ENTRY_ROOKIE_GUILD (pkt.Unk=0 → rookie guild, ≥1 → comeback
guild). Guilds are created on demand and capped at 60 members. Players
leave via the OperateGuildGraduateRookie/Return actions. The guild info
response now reports isReturnGuild from the DB instead of hardcoded false.
Migration 0014_return_guilds adds return_type to the guilds table.
2026-03-22 00:27:05 +01:00
Houmgaor
5fe1b22550 feat(save-transfer): add saveutil CLI and token-gated import endpoint
Adds two complementary paths for transferring character save data between
Erupe instances without breaking the SHA-256 integrity check system:

- `cmd/saveutil/`: admin CLI with `import`, `export`, `grant-import`, and
  `revoke-import` subcommands. Direct DB access; no server running required.
- `POST /v2/characters/{id}/import`: player-facing API endpoint gated behind
  a one-time token issued by `saveutil grant-import` (default TTL 24 h).
  Token is validated and consumed atomically to prevent TOCTOU races.
- Migration `0013_save_transfer`: `savedata_import_token` and
  `savedata_import_token_expiry` columns on `characters` table.
- Both paths decompress incoming savedata and recompute the SHA-256 hash
  server-side, so the integrity check remains valid after import.
- README documents both methods and the per-character hash-reset workaround.

Closes #183.
2026-03-21 20:14:58 +01:00
Houmgaor
0ea399f135 feat(config): add DisableSaveIntegrityCheck flag for save transfers
The SHA-256 integrity check introduced in migration 0007 blocks saves
when a character's savedata blob is imported from another server instance,
because the stored hash in the target DB no longer matches the new blob.

Adding DisableSaveIntegrityCheck (default: false) lets server operators
bypass the check to unblock cross-server save transfers. A warning is
logged each time the check is skipped so the flag's use is auditable.

Documents the per-character SQL alternative in CHANGELOG:
  UPDATE characters SET savedata_hash = NULL WHERE id = <id>

Closes #183.
2026-03-21 19:38:16 +01:00
Houmgaor
dbbfb927f8 feat(guild): separate scout invitations into guild_invites table
Scout invitations were stored in guild_applications with type 'invited',
forcing the scout list response to use charID as the invitation ID — a
known hack that made CancelGuildScout semantically incorrect.

Introduce a dedicated guild_invites table (migration 0012) with a serial
PK. The scout list now returns real invite IDs and actual InvitedAt
timestamps. CancelGuildScout cancels by PK. AcceptInvite and DeclineInvite
operate on guild_invites while player-applied applications remain in
guild_applications unchanged.
2026-03-21 17:59:25 +01:00
Houmgaor
a67b10abbc docs(unimplemented): reflect diva branch merge + count drop to 66
Remove feature/diva and feature/event-tent branch references now that
their handlers have been merged or cleaned up; update handler count
from 68 to 66.
2026-03-21 17:23:40 +01:00
Houmgaor
106cf85eb7 fix(repo): detect silent save failures + partial daily mission stubs
SaveColumn and SaveMercenary now check RowsAffected() and return a
wrapped ErrCharacterNotFound when 0 rows are updated, preventing
silent data loss when a character ID is missing or mismatched.
AdjustInt already detects this via its RETURNING scan — no change.

Daily mission packet structs (Get/SetDailyMission*) now parse the
AckHandle instead of returning NOT IMPLEMENTED, letting handlers
send empty-list success ACKs and avoiding client softlocks.

Also adds tests for dashboard stats endpoint and for five guild
repo methods (SetAllianceRecruiting, RolloverDailyRP,
AddWeeklyBonusUsers, InsertKillLog, ClearTreasureHunt) that
had no coverage.
2026-03-21 01:49:28 +01:00
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