Commit Graph

1059 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
Houmgaor
7657ddbd50 fix: prevent startup panics on databases missing base tables
The catch-up migration now creates the servers table if missing, like
sign_sessions. Startup cleanup queries in main.go use Exec instead of
MustExec so missing tables log warnings rather than panicking.
2026-03-16 00:04:45 +01:00
Houmgaor
31aa02a8e2 fix(migrations): create sign_sessions table before altering it
The catch-up migration assumed sign_sessions already existed, but
databases created from older schema dumps may not have this table.
Adding CREATE TABLE IF NOT EXISTS prevents the migration from failing
with "relation does not exist" on those databases.
2026-03-15 23:53:10 +01:00
Houmgaor
2365b63e9c fix(gacha): validate reward pool before charging currency (#175)
Filter out gacha entries with no items at the query level (EXISTS
subquery) and reorder Play methods to validate the pool before
calling transact(), so players are never charged for a misconfigured
gacha.
2026-03-10 11:28:11 +01:00
Houmgaor
915e9bc0b0 fix(gacha): prevent panics from misconfigured gacha entries (#175)
getRandomEntries panics with rand.Intn(0) when box gacha has more rolls
than available entries. Fix by clamping rolls, guarding empty entries
and zero-weight pools, and fixing an out-of-bounds slice in the receive
handler overflow path.
2026-03-10 11:19:22 +01:00
Houmgaor
bfc5319cb6 fix(guild): fix nil panics causing clan menu softlock (#171)
The crash was in handleMsgMhfGetGuildManageRight where a variable
shadowing bug (guild, err := instead of guild, err =) left the
outer guild pointer nil after the inner GetByID lookup succeeded,
panicking at guild.MemberCount. This is confirmed by the user's
stack trace pointing to handlers_guild.go:234.

Also fix 6 other nil-dereference risks across guild handlers:
- handleMsgMhfArrangeGuildMember: guild nil after GetByID
- handleMsgMhfEnumerateGuildMember: alliance nil when AllianceID > 0
- handleMsgMhfUpdateGuildIcon: guild and characterInfo nil
- handleMsgMhfOperateGuild: guild nil, characterGuildInfo nil
- handleAvoidLeadershipUpdate: characterGuildData nil

Improve panic recovery to log opcode and full stack trace so
future panics can be diagnosed from console screenshots.
2026-03-06 00:15:53 +01:00
Houmgaor
ba7ec122f8 revert: remove SQLite support
An MMO server without multiplayer defeats the purpose. PostgreSQL
is the right choice and Docker Compose already solves the setup
pain. This reverts the common/db wrapper, SQLite schema, config
Driver field, modernc.org/sqlite dependency, and all repo type
changes while keeping the dashboard, wizard, and CI improvements
from the previous commit.
2026-03-05 23:05:55 +01:00
Houmgaor
ecfe58ffb4 feat: add SQLite support, setup wizard enhancements, and live dashboard
Add zero-dependency SQLite mode so users can run Erupe without
PostgreSQL. A transparent db.DB wrapper auto-translates PostgreSQL
SQL ($N placeholders, now(), ::casts, ILIKE, public. prefix,
TRUNCATE) for SQLite at runtime — all 28 repo files use the wrapper
with no per-query changes needed.

Setup wizard gains two new steps: quest file detection with download
link, and gameplay presets (solo/small/community/rebalanced). The API
server gets a /dashboard endpoint with auto-refreshing stats.

CI release workflow now builds and pushes Docker images to GHCR
alongside binary artifacts on tag push.

Key changes:
- common/db: DB/Tx wrapper with 6 SQL translation rules
- server/migrations/sqlite: full SQLite schema (0001-0005)
- config: Database.Driver field ("postgres" or "sqlite")
- main.go: SQLite connection with WAL mode, single writer
- server/setup: quest check + preset selection steps
- server/api: /dashboard with live stats
- .github/workflows: Docker in release, deduplicate docker.yml
2026-03-05 18:00:30 +01:00
Houmgaor
03adb21e99 fix(channelserver): post-RC1 stabilization sprint
Fix rasta_id=0 overwriting NULL in SaveMercenary, which prevented
game state saving for characters without a mercenary (#163).

Also includes:
- CHANGELOG updated with all 10 post-RC1 commits
- Setup wizard fmt.Printf replaced with zap structured logging
- technical-debt.md updated with 6 newly completed items
- Scenario binary format documented (docs/scenario-format.md)
- Tests: alliance nil-guard (#171), handler dispatch table,
  migrations (sorted/SQL/baseline), setup wizard (10 tests),
  protbot protocol sign/entrance/channel (23 tests)
2026-03-05 16:39:15 +01:00
Houmgaor
10ac803a45 fix(channelserver): correct ecdMagic constant byte order (#174)
The constant used file-order bytes (0x6563641a) instead of the value
produced by binary.LittleEndian.Uint32 (0x1A646365), causing
loadRengokuBinary to reject every real ECD-encrypted rengoku_data.bin.
The unit tests masked this because they round-tripped with the same
wrong constant.
2026-03-03 18:23:45 +01:00
Houmgaor
8717fb9b55 fix(guild): add nil guards in cancel and answer scout handlers
GetByID returns (nil, nil) for deleted guilds, and AnswerScout can
return nil result on ErrApplicationMissing, both leading to nil
dereferences.
2026-03-03 18:04:33 +01:00
Houmgaor
8e79fe6834 fix(guild): fix variable shadowing causing nil panic in scout list (#171)
The else branch redeclared guildInfo with := scoping it to the block,
so the outer guildInfo remained nil when reaching ListInvitedCharacters.
Restructure the conditional to assign to the existing variable instead.
2026-03-03 18:01:20 +01:00
Houmgaor
5b631d1704 perf(channelserver): cache rengoku_data.bin at startup
Load and validate rengoku_data.bin once during server initialization
instead of reading it from disk on every client request. The file is
static ECD-encrypted config data (~4.9 KB) that never changes at
runtime. Validation checks file size and ECD magic bytes, logging a
warning if the file is missing or invalid so misconfiguration is
caught before any client connects.
2026-03-02 20:12:39 +01:00
Houmgaor
aee53534a2 fix(guild): add nil guards for alliance guild lookups (#171)
scanAllianceWithGuilds dereferences guild pointers returned by GetByID
without checking for nil. Since GetByID returns (nil, nil) when a guild
is missing, alliances referencing deleted guilds cause nil-pointer
panics. The panic is caught by session recovery but no ACK is sent,
softlocking the client.

Add nil checks in scanAllianceWithGuilds, handleMsgMhfOperateJoint,
handleMsgMhfInfoJoint, and handleMsgMhfInfoGuild so that missing
guilds or alliances produce proper error responses instead of panics.
2026-03-02 19:43:11 +01:00
Houmgaor
07a587213d fix(channelserver): remove false race in PacketDuringLogout test
The test ran handleMsgMhfSavedata and logoutPlayer concurrently on the
same session, triggering data races on s.playtime and Save(). In
production the dispatch loop processes packets sequentially per session,
so this overlap is impossible. Run the operations sequentially to match
real behavior while still validating no data loss.
2026-03-01 18:56:52 +01:00
Houmgaor
6143902f39 test(channelserver): add tests for logoutPlayer, saveAllCharacterData, and transit message
Cover the two most complex untested handlers in handlers_session.go:

- logoutPlayer (8 tests): basic logout, character save path, cafe course
  RP accrual, stage cleanup, host disconnect with MsgSysStageDestruct,
  error resilience for ReadInt/LoadSaveData failures, concurrent logout
- saveAllCharacterData (4 tests): nil save data, load error propagation,
  RP capping logic, playtime accumulation
- handleMsgMhfTransitMessage (6 tests): search by charID (found/not
  found), search by name, search by lobby (IP+port+stageID), party
  finder with stage prefix and rank filtering, localhost IP rewrite
2026-03-01 18:41:59 +01:00
Houmgaor
9a5a8dfb36 fix(migrations): add IF NOT EXISTS guard to alliance recruiting column
Without this guard, migration 0004 fails on databases where the column
already exists, such as during the existing-DB-without-schema-version
upgrade path where 0001 baseline is auto-marked and 0002-0005 re-applied.
2026-02-28 19:26:21 +01:00
Houmgaor
bb16306f91 test(migrations): update expected counts after adding migrations 0004-0005
Test assertions were still expecting 3 total migrations from when only
0001-0003 existed. Updated to reflect 5 migrations (0001-0005).
2026-02-28 18:02:36 +01:00
Houmgaor
fa09e4a39c fix(migrations): drop unused data column from distribution table (#169)
The distribution table had a `data bytea NOT NULL` column that was never
read by the Go code — item data is stored in distribution_items instead.
The NOT NULL constraint forced dummy values in seed data and test inserts.

Remove the column from the baseline schema, seed data, and tests, and
add migration 0005 to drop it from existing databases.
2026-02-27 18:19:57 +01:00
Houmgaor
21f9a79b62 fix(channelserver): correct session handler retail mismatches (#167)
Lobby search now returns only quest-bound players (QuestReserved) instead
of all reserved slots, matching retail behavior. The new field is
pre-collected under server lock before stage iteration to respect
Server.Mutex → Stage.RWMutex lock ordering.

Replaced three TODOs with RE documentation from Ghidra decompilation of
mhfo-hd.dll ZZ:
- Log key off-by-one: putRecord_log/putTerminal_log pass size 0 for the
  key field in ZZ, so the stored key is unused beyond issuance
- User search padding: ZZ per-entry parser confirms 40-byte block via
  memcpy(dst, src+8, 0x28); G2 DLL analysis inconclusive (stripped)
- Player count: field at entry offset 0x08 maps to struct param_1[0xe]
2026-02-27 17:29:32 +01:00
Houmgaor
4c47c8e18f fix(channelserver): correct bookshelf save data pointers for non-ZZ (#164)
The pBookshelfData offsets for G1-Z2, F4-F5, and S6 were off by -14810,
placing bookshelf before houseData in the save blob and reading garbage.
All other 12 save fields have consistent inter-version deltas (36000,
32000, 48000); only bookshelf broke the pattern. Correcting by +14810
restores the gallery-bookshelf gap to 136 bytes (matching ZZ) and aligns
all field deltas across versions.

Supersedes Mezeporta/Erupe#155 (same fix, merge conflict on renamed file).
2026-02-27 16:46:32 +01:00
Houmgaor
d6938f2a27 fix(guild): implement alliance application toggle (#166)
Alliance applications were hardcoded to always-open. Add a `recruiting`
column to guild_alliances and handle OperateJoint actions 0x06 (Allow)
and 0x07 (Deny) confirmed via Wii U debug symbols. Only the parent
guild leader can toggle the setting, matching the existing disband guard.
2026-02-27 14:59:18 +01:00
Houmgaor
7f5d30e2f5 fix: resolve code scanning findings in commands and wizard
Add bounds check (0 to MaxUint32) before casting strconv.Atoi result
to uint32 in the rights command handler. Replace manual allowlist
validation with pq.QuoteIdentifier for CREATE DATABASE to eliminate
the SQL injection finding.
2026-02-27 13:45:56 +01:00