Commit Graph

959 Commits

Author SHA1 Message Date
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
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
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.
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
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
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
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
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
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
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
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
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
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