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
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.
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.
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.
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
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)
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.
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.
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.
Ghidra decompilation of mhfo-hd.dll shows all gacha packets use a
trailing uint8 for gacha type (0=normal, 1=stepup, 4=box). The server
ignores it since it derives the type from GachaID.
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.
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.
The Docker image is not public, so the "Option A: Docker" install
method doesn't work for users. Removed it and renumbered the
remaining options. Simplified the Updating section with a link
to docker/README.md for Docker users.
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.
Instead of a generic "Failed to ping" message, detect the actual
cause and show a targeted fix:
- net error → PostgreSQL not reachable, check if it's running
- pq 28P01 → wrong password, update config.json
- pq 3D000 → database doesn't exist, create it
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.
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
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.
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.
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.
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.
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.
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
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.
Backfill key changes from 900 commits since 9.2.0 including
architecture rewrite, game system overhauls, client version
support, and developer tooling. Tag Unreleased as 9.3.0-rc1.
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.
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]
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).
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.
Filed #164-#168 for all 8 remaining TODOs. Added Tracker column
with issue links, fixed stale line numbers, added MhfAddUdPoint
stub entry, and expanded the suggested execution order.
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.
The migrations and channelserver packages both DROP and recreate the
public schema on the shared erupe_test database. With parallel package
execution, they destroy each other's schema mid-migration. Adding -p 1
serializes package execution while preserving t.Parallel() within each
package.
Add REST-idiomatic DELETE method as alias for POST .../delete.
Add 8 router-level tests exercising Bearer auth, invalid tokens,
soft delete, export body decoding, and MaxLauncherHR capping.
Create OpenAPI 3.1.0 specification covering all v2 endpoints.
Cover all previously-untested session handlers (authenticate,
handleDSGN, handleWIIUSGN, handlePSSGN, handlePSNLink, sendCode)
plus validateLogin ban/bcrypt paths, registerPsnToken, and
sanitizeAddr/startSignCapture. Uses a spyConn implementing
network.Conn to capture plaintext packets for assertion.
Raises signserver coverage from 46% to 63%.
Introduces incremental API improvements for custom launcher support
(mhf-iel, stratic-dev's Rust launcher):
- Standardize all error responses to JSON envelopes with error/message
- Add Bearer token auth middleware for v2 routes (legacy body-token preserved)
- Add `returning` (>90d inactive) and `courses` fields to auth response
- Add /v2/ route prefix with HTTP method enforcement
- Add GET /v2/server/status for MezFes, featured weapon, and event status
- Add APIEventRepo for read-only event data access
Closes#44
Extract a Session interface from *discordgo.Session so DiscordBot methods
can be tested with a mock — no live Discord connection required. Add
AddHandler, RegisterCommands, and UserID methods to DiscordBot so
external callers (main.go, sys_channel_server.go) no longer reach
through bot.Session directly. Rewrite tests with a mockSession,
raising discordbot coverage from 12.5% to 66.7%.