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.
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)
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
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.
Add four new test files covering previously-untested handler functions
to raise total coverage from 57.7% to 60.0%:
- handlers_misc_coverage_test.go: minidata, trend weapons, etc points,
equip skin history
- handlers_cafe_coverage_test.go: cafe duration bonuses, daily cafe,
cafe duration
- handlers_festa_coverage_test.go: mezfes data, festa voting, entry,
charge, prizes, state queries, member enumeration
- handlers_event_coverage_test.go: weekly schedule, login boost,
scenario data, friends/blacklist operations
Also make mockCharacterRepo.ReadEtcPoints configurable to support
etc points handler tests.
The festa handler contained event lifecycle management (cleanup expired
events, create new ones) and the repo enforced a business rule (skip
zero-value soul submissions). Move these into a new FestaService to
keep repos as pure data access and consolidate business logic.
The tower repo had business logic beyond simple CRUD: AddGem used a
fetch-transform-save pattern, progress capping was inline in the
handler, and RP donation orchestrated multiple repo calls with
conditional page advancement. Move these into a new TowerService
following the established service layer pattern.
Enables isolated unit tests for tower, festa, rengoku, diva, event,
misc, mercenary, and cafe handlers. All 21 repo interfaces now have
mock implementations in repo_mocks_test.go.
mockGuildRepoForMail and mockGuildRepoOps each implemented different
subsets of the 68-method GuildRepo interface. Adding any new method
required updating both mocks. Merged into a single mockGuildRepo with
configurable struct fields for error injection and no-op defaults for
the rest.
Introduce MailService as a convenience layer between handlers/services
and MailRepo. Provides Send, SendSystem, SendGuildInvite, and
BroadcastToGuild methods that encapsulate the boolean flag combinations.
GuildService now depends on MailService instead of MailRepo directly,
simplifying its mail-sending calls from verbose SendMail(..., false, true)
to clean SendSystem(recipientID, subject, body).
Guild mail broadcast logic moved from handleMsgMhfSendMail into
MailService.BroadcastToGuild.
Players could never claim monthly guild items because the handler
always returned 0x01 (claimed). Now tracks per-character per-type
(standard/HLC/EXC) claim timestamps in the stamps table, comparing
against the current month boundary to determine claim eligibility.
Adds MonthStart() to gametime, extends StampRepo with
GetMonthlyClaimed/SetMonthlyClaimed, and includes schema migration
31-monthly-items.sql.
Cover critical paths that previously had no test coverage:
- Session: login success/error paths, ping, logkey, record log,
global sema lock/unlock, rights reload, announce
- Gacha: point queries, coin deduction, item receive with overflow
and freeze, normal/stepup/box gacha play, stepup status lifecycle,
weighted random selection
- Shop: enumeration across all shop types, exchange purchases,
fpoint-to-item and item-to-fpoint exchange, fpoint exchange list
with Z2 vs ZZ encoding
- Plate: load/save for platedata, platebox, platemyset with
oversized payload rejection, diff path, and cache invalidation
Add mockSessionRepo, mockGachaRepo, mockShopRepo, and
mockUserRepoGacha to support the new test scenarios. Add
loadColumnErr field to mockCharacterRepo for diff-path error
testing.
Cover 5 more handler files with mock-based unit tests, bringing
package coverage from 43.7% to 47.7%. Extend mockGuildRepoOps with
alliance, cooking, adventure, treasure hunt, and hunt data methods.
Cover the 4 handler files that had no tests: handlers_guild_ops.go,
handlers_guild_scout.go, handlers_guild_board.go, and handlers_items.go.
44 new tests exercise the error-logging paths added in 8fe6f60 and the
core handler logic (disband, resign, apply, leave, accept/reject/kick,
scout answer, message board CRUD, weekly stamps, item box parsing).
New mock types: mockGuildRepoOps (enhanced guild repo with configurable
errors and state tracking), mockUserRepoForItems, mockStampRepoForItems,
mockHouseRepoForItems. Coverage rises from 41.1% to 43.7%.
Fix errcheck violations across 11 repo files by wrapping deferred
rows.Close() and tx.Rollback() calls to discard the error return.
Fix unchecked Scan/Exec calls in guild store tests. Fix staticcheck
SA9003 empty branch in test helpers.
Add 6 mock-based unit tests for GetCharacterSaveData covering nil
savedata, sql.ErrNoRows, DB errors, compressed round-trip,
new-character skip, and config mode/pointer propagation.
Eliminate the last three direct DB accesses from handler code:
- CharacterRepo.LoadSaveData: replaces db.Query in GetCharacterSaveData,
using QueryRow instead of Query+Next for cleaner single-row access
- EventRepo.GetEventQuests, UpdateEventQuestStartTime, BeginTx: moves
event quest enumeration and rotation queries behind the repo layer
- UserRepo.BanUser: consolidates permanent/temporary ban upserts into a
single method with nil/*time.Time semantics
Leverage the new repository interfaces to test handler logic without
a database. Adds shared mock implementations (achievement, mail,
character, goocoo, guild) and 32 new handler tests covering
achievement, mail, cafe/boost, and goocoo handlers.