Commit Graph

1674 Commits

Author SHA1 Message Date
Houmgaor
803996adac style: gofmt realignment across channelserver 2026-04-06 19:33:32 +02:00
Houmgaor
e48d33ca76 feat(shutdown): passive drain and rename DisableSoftCrash
Shutdown now proceeds in three phases: close listeners immediately on
signal, broadcast the in-game countdown, wait up to ShutdownDrainSeconds
(default 30) for sessions to disconnect naturally via DrainPassive, then
force-close any stragglers. This prevents players from entering new
quests after the countdown starts, and lets mid-quest sessions finish
saving without being killed mid-write. A second SIGINT during passive
drain cancels it so the force-close phase runs immediately.

Renamed DisableSoftCrash -> DisableShutdownCountdown since the flag
controls the countdown, not crash behaviour. Existing config.json files
keep working via a Viper alias on the legacy key.

Closes #179.
2026-04-06 19:32:35 +02:00
Houmgaor
9b0f735335 chore(merge): merge develop into main for 9.4.0 cycle
Brings 53 develop commits (i18n, Diva, campaign, guild invites, save
transfer, return/rookie guilds, hunting tournament, JSON quest/scenario
loaders, Ghidra-derived user binary parsing, and misc fixes) onto main
now that 9.3.2 has been tagged and released.

Resolves two overlap zones:

1. Migration number collision. Main shipped 0010_fix_zero_rasta_id and
   0011_fix_stale_boost_time in 9.3.2; develop had independently
   numbered 0010_campaign..0015_tournament. The migration runner keys
   applied versions by integer, so coexisting files with the same
   numeric prefix would silently skip each other. Develop's files have
   been renumbered to 0016..0021, leaving main's 0010/0011 intact. A
   schema_version rename script is required on any server that had
   already applied the old develop numbers (only frontier.mogapedia.fr
   at the time of this merge).

2. CHANGELOG.md. Develop's in-progress feature entries move into
   [Unreleased] with updated migration references; the [9.3.2] section
   is preserved verbatim.

main.go version string bumped to 9.4.0-dev to mark the new cycle.

Full test suite (go test -race ./...) passes.
2026-04-06 19:06:09 +02:00
Houmgaor
e9510c3b3b chore(release): 9.3.2 v9.3.2 2026-04-06 18:37:18 +02:00
Houmgaor
90875c602a docs(changelog): cover gacha, boost and protbot changes for 9.3.2
Catch Unreleased up to the commits pushed this session:

- empty-bytea gacha crash fix (#175) with the RE citation for the
  ZZ client's CSync_man::putReceive_gacha_item response buffer path
- StructScan warn-logging for misconfigured gacha_items rows
- stray double-ACK removal in handleMsgMhfGetBoostTimeLimit
- pre-1970 boost_time wraparound fix + 0011 healing migration
- protbot --action boost / gacha inspection scenarios
2026-04-06 18:26:24 +02:00
Houmgaor
4ad0012f62 fix(handlers): guard GetBoostTimeLimit against past boost_time
Live-server testing via protbot surfaced an inconsistency between
GetBoostTimeLimit and GetBoostRight: on the same character, the
former reported a far-future boost limit (2288912640 = year 2042)
while the latter correctly reported "expired / available". The two
handlers read the same boost_time row and disagreed.

Root cause: GetBoostTimeLimit was doing a naked uint32(int64) cast
on boostLimit.Unix(). The test character's boost_time was actually
year 1906 (a pre-1970 sentinel left behind by the pre-#187 bug),
whose negative int64 Unix timestamp wraps through uint32 to a huge
positive value the client interprets as a permanently active boost.

Harmonise the guard with GetBoostRight: return 0 whenever the stored
boost_time is not strictly after TimeAdjusted(), covering both the
pre-1970 wraparound and the "already expired" case.

Add a healing migration (0011_fix_stale_boost_time) that NULLs out
any boost_time column older than 1970 or more than 10 years in the
future, so affected characters recover on upgrade without waiting
for a fresh boost start.

Regression test uses the exact year-1906 value observed on the live
frontier.mogapedia.fr test account.
2026-04-06 18:26:15 +02:00
Houmgaor
e2b0a8ad8c style(deltacomp): gofmt realignment 2026-04-06 18:04:34 +02:00
Houmgaor
ca12635234 chore: gitignore CLAUDE.local.md 2026-04-06 18:04:34 +02:00
Houmgaor
8b2667f7a0 fix(handlers): harden gacha_items and boost time limit ACK paths
Both bugs surfaced when running protbot against a live Erupe instance.

LoadColumnWithDefault now treats an empty bytea ('\x', len 0) the same
as NULL. The postgres driver returns a non-nil empty slice for empty
bytea, so the prior `data == nil` check let a zero-byte slice reach
handleMsgMhfReceiveGachaItem, which forwarded it to the client as a
malformed gacha_items response. The MHF client interprets a zero-byte
count field as a protocol error and crashes the gacha menu, matching
the #175 symptom class. The handler also gains a defensive fallback
for len(data) == 0 in case another caller hits the same edge.

handleMsgMhfGetBoostTimeLimit was sending two ACKs for a single
request: doAckBufSucceed with the real payload, then an unconditional
doAckSimpleSucceed on the same ack handle. The second ACK was dead on
arrival (the handle was already consumed) but is a latent protocol
bug. Drop it and update the regression test that was asserting the
buggy 2-packet behavior.
2026-04-06 18:04:10 +02:00
Houmgaor
3e9f3d1b62 feat(protbot): add boost and gacha inspection scenarios
Adds non-destructive test scenarios for the #187 boost-time fix and
the #175 / gacha-logging changes so regressions in those paths can be
caught without a full game client.

- boost: queries GET_BOOST_TIME_LIMIT, GET_BOOST_RIGHT, and
  GET_KEEP_LOGIN_BOOST_STATUS, flagging a zero boost_limit and all-zero
  login boost entries as the expected DisableBoostTime/DisableLoginBoost
  state.
- gacha: snapshots GET_GACHA_POINT and RECEIVE_GACHA_ITEM (freeze=true,
  so temp storage is not cleared), with an opt-in --roll flag that
  exercises PLAY_NORMAL_GACHA end-to-end. Detects the post-#175
  single-byte validation-failure ACK.
2026-04-06 17:58:04 +02:00
Houmgaor
6fa07ae4ae fix(gacha): log and skip gacha_items rows that fail to scan
Previously GetItemsForEntry/GetGuaranteedItems silently swallowed
StructScan errors, so misconfigured rows (item_type > 255 or
item_id/quantity > 65535) disappeared from rewards with no trace,
making config bugs hard to diagnose without a DB dump.

Pass a zap.Logger into GachaRepository and emit a Warn pointing at
the likely cause and the offending entry/gacha ID.
2026-04-06 17:48:12 +02:00
Houmgaor
05ea321d9e fix(migrations): heal rasta_id=0 rows (#163)
The pre-106cf85 SaveMercenary bug could overwrite a character's NULL
rasta_id with 0, a value not drawn from rasta_id_seq that causes silent
save failures. The code fix prevents new occurrences but leaves already
affected rows stuck. Migration 0010 sets rasta_id=NULL where it is 0 so
existing users auto-heal on upgrade.
2026-04-06 17:10:33 +02:00
Houmgaor
49a5069e3d fix(handlers): correct quest tune-value multiplier handling
Two bugs in handleMsgMhfEnumerateQuest affecting reward multipliers:

1. A Value > 0 filter silently dropped any multiplier set to exactly
   0.0 in config, causing the client to fall back to its hardcoded
   default (100%). So ZennyMultiplier: 0.0 produced *full* zenny
   instead of none. Removed the filter so zero values are sent
   verbatim.

2. uint16(float32(0.20) * 100) yields 19, not 20, due to float32
   representation of 0.20 being ~0.19999998. Added a
   multiplierToTuneValue helper using math.Round and applied it to
   all 18 multiplier call sites (HRP/SRP/GRP/GSRP/Zenny/GZenny/
   Material/GMaterial/GCP/GUrgent and NC variants).
2026-04-06 16:45:56 +02:00
Houmgaor
4c725adbb7 test(handlers): regression tests for DisableBoostTime/DisableLoginBoost (#187)
Three focused tests asserting the response payloads when the config flags
are set: GetBoostTimeLimit and GetBoostRight return zero when
DisableBoostTime is true, and UseKeepLoginBoost returns an all-zero ack
when DisableLoginBoost is true. Verified to fail against the pre-fix
source.
2026-04-06 16:22:23 +02:00
Houmgaor
84e72f7d35 fix(handlers): honor DisableLoginBoost and DisableBoostTime fully (#187)
GetBoostTimeLimit and GetBoostRight now respect DisableBoostTime, and
UseKeepLoginBoost now respects DisableLoginBoost. Also fix a latent
zero-time.Time wraparound in GetBoostTimeLimit that caused the
"Boost Time" overlay to appear on fresh characters regardless of
config, since time.Time{}.Unix() cast to uint32 yields a large value
the client interprets as an active timestamp.
2026-04-06 16:16:05 +02:00
Houmgaor
99e6ea26f1 feat(handlers): parse and log user binary types 1-3
Reverse-engineered from mhfo-hd.dll via Ghidra: type 1 = character name,
type 2 = player profile (208B), type 3 = equipment snapshot (384B).
Adds structured zap logging and size validation warnings to
handleMsgSysSetUserBinary.
2026-04-06 16:05:19 +02:00
Houmgaor
883e503ec9 fix(handlers): ack savedata save failure to prevent softlock 2026-04-06 16:05:15 +02:00
Houmgaor
9e41d59bd1 Merge pull request #186 from Brentdbr/patch-1
Update README with further setup instructions
2026-04-01 14:26:35 +02:00
Brent
7521e21fa0 Update README with database setup instructions
Added instructions for using pgAdmin4 to create the database and clarified steps for configuring the application.
2026-03-31 20:28:01 +02:00
Houmgaor
9b3884fc26 chore(merge): merge main into develop
Backports two softlock fixes and playtime regression fix from main.
Also removes handleMsgMhfEnterTournamentQuest from the nil-stub test
list — it's a real DB-backed handler and has its own dedicated test.
2026-03-23 22:50:27 +01:00
Houmgaor
abab6dc3a1 fix(handlers): fix softlock on forge purchases and N-points
MSG_CA_EXCHANGE_ITEM and MSG_MHF_USE_UD_SHOP_COIN had Parse() returning
"NOT IMPLEMENTED". The dispatch loop in handlePacketGroup treats any
Parse error as a silent drop — no ACK is sent, causing the client to
wait indefinitely (softlock). Reported on 9.3.0-rc1 for forge item
purchases and Hunting Road N-point interactions.

Fix follows the pattern from d27da5e: parse only the AckHandle, return
nil from Parse, and respond with doAckBufFail so the client's error
branch exits cleanly without reading response fields.
2026-03-23 22:20:32 +01:00
Houmgaor
72088db4ff fix(savedata): write playtime back to binary blob on save
updateSaveDataWithStruct only wrote RP and KQF into the blob, leaving
the playtime field stale. On each reconnect, GetCharacterSaveData read
the old in-game counter from the blob and reset s.playtime to it,
rolling back all progress accumulated during the previous session.

Playtime is now persisted into the blob alongside RP, using the same
S6 mode guard as the read path in updateStructWithSaveData.
2026-03-23 22:00:06 +01:00
Houmgaor
6f7852cc12 feat(api): add GET /v2/server/info endpoint for launcher compatibility
Exposes the server's configured client mode in a form that mhf-outpost
and other launcher tools can consume to warn users when their local game
version does not match what the server expects.

Returns clientMode (raw, e.g. "G9.1") and manifestId (normalized for
mhf-outpost: lowercase, dots stripped, e.g. "g91"). No auth required.
2026-03-23 19:55:55 +01:00
Houmgaor
9e59ce69e3 fix(tests): fix three CI failures on develop
- alliance recruiting default: change DEFAULT true → false in both
  0001_init.sql and 0004_alliance_recruiting.sql; new alliances should
  not be open for recruitment by default (TestSetAllianceRecruiting)
- guild_invites migration: add IF NOT EXISTS so re-running migrations
  on an existing DB does not fail with "relation already exists"
  (TestMigrateExistingDBWithoutSchemaVersion)
- test character name: shorten "Idem_Rollover_Leader" (19 chars) to
  "IdemRollLeader" to fit the VARCHAR(15) constraint
  (TestRolloverDailyRP_Idempotent)
2026-03-23 12:52:28 +01:00
Houmgaor
19a49fa0ae fix(migrations): remove explicit BEGIN/COMMIT from migrations 0012-0015
applyMigration() already wraps each file in a db.Begin()/tx.Commit()
transaction. The inner BEGIN/COMMIT in these files caused the outer
transaction to commit early, leaving applyMigration trying to insert
into schema_version on an idle connection:
  pq: unexpected transaction status idle

Affected: 0012_guild_invites, 0014_return_guilds, 0015_tournament.
2026-03-23 11:59:57 +01:00
Houmgaor
470a761191 fix(scenario): enforce scenarioChunkSizeLimit in CompileScenarioJSON
The constant was declared and documented but never used, causing a
golangci-lint failure. Wire it into compileScenario() so oversized
chunks are rejected with a clear error rather than silently served
to the client (which discards them per FUN_11525c60 RE findings).
2026-03-23 11:19:50 +01:00
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