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.
CharacterSaveData.Save() silently returned on failure (nil decompressed
data, compression error, DB error) while the caller unconditionally
logged "Saved character data successfully". This made diagnosing save
failures difficult (ref #163).
Save() now returns an error, and all six call sites check it. The
success log in saveAllCharacterData only fires when the save actually
persisted.
Several handler files discarded errors from repository and service
calls, creating nil-dereference risks and silent data corruption:
- guild_adventure: 3 GetByCharID calls could panic on nil guild
- gacha: GetGachaPoints silently returned zero balances on DB error
- house: HasApplication called before nil check on guild;
GetHouseContents error discarded with 7 return values
- distitem: 3 distRepo calls had no error logging
- guild_ops: Disband/Leave service errors were invisible
- shop: gacha type/weight/fpoint lookups had no error logging
- discord: bcrypt error could result in nil password being set
Move business logic for guild disband, resign leadership, leave,
post scout, and answer scout from handlers into GuildService methods.
Handlers now delegate to the service layer and handle only protocol
concerns (packet parsing, ACK responses, cross-channel notifications).
Adds 22 new table-driven service tests and sentinel errors for typed
error handling (ErrNoEligibleLeader, ErrAlreadyInvited, etc.).
DonateRP left in handler due to Session coupling.
Extract business logic from handleMsgMhfOperateGuildMember into
GuildService.OperateMember, establishing the handler→service→repo
layering pattern. The handler is now ~20 lines of protocol glue
(type-assert, map action, call service, send ACK, notify).
GuildService owns authorization checks, repo coordination, mail
composition, and best-effort mail delivery. It accepts plain Go
types (no mhfpacket or Session imports), making it fully testable
with mock repos. Cross-channel notification stays in the handler
since it requires Session.
Adds 7 table-driven service-level tests covering accept/reject/kick,
authorization, repo errors, mail errors, and unknown actions.
The guild daily RP rollover TODO in handlers_guild_ops.go was stale —
the feature was already implemented via lazy rollover in
handlers_guild.go. Several other items in technical-debt.md were also
resolved in prior commits (typos, db guard investigation). Updated
the doc to reflect current state and bumped codecov-action to v5.
Add SJISToUTF8Lossy() that wraps SJISToUTF8() and logs decode errors at
slog.Debug level. Replace all 31 call sites across 17 files that previously
discarded the error with `_, _ =`. This makes garbled text from malformed
SJIS client data debuggable without adding noise at default log levels.
fmt.Sprintf inside zap logger calls defeats structured logging,
making log aggregation and filtering harder. All 6 sites now use
proper zap fields (zap.Uint32, zap.Uint8, zap.String).
LoopDelay had no viper.SetDefault, so omitting it from config.json
caused a zero-value (0 ms) busy-loop in the recv loop. Default is
now 50 ms, matching config.example.json.
main.go always sets both Channels and Registry together, making the
Channels fallback paths dead code. This removes:
- Server.Channels field from the Server struct
- 3 if/else fallback blocks in handlers_session.go (replaced with
Registry.FindChannelForStage, SearchSessions, SearchStages)
- 1 if/else fallback block in handlers_guild_ops.go (replaced with
Registry.NotifyMailToCharID)
- 3 method fallbacks in sys_channel_server.go (WorldcastMHF,
FindSessionByCharID, DisconnectUser now delegate directly)
Updates anti-patterns.md #6 to "accepted design" — Session struct is
appropriate for this game server's handler pattern, and cross-channel
coupling is now fully routed through the ChannelRegistry interface.
19 repository calls across 8 handler files were silently discarding
errors with `_ =`, making database failures invisible. Add structured
logging at appropriate levels: Error for write operations where data
loss may occur (guild saves, member updates), Warn for read operations
where handlers continue with zero-value defaults (application checks,
warehouse loads, item box queries). No control flow changes.
Move all direct DB access from handlers_rengoku.go (11 calls) and
handlers_mail.go (10 calls) into dedicated repository types, continuing
the established extraction pattern.
RengokuRepository provides UpsertScore and GetRanking, replacing a
3-call check/insert/update sequence and an 8-case switch of nearly
identical queries respectively.
MailRepository provides SendMail, SendMailTx, GetListForCharacter,
GetByID, MarkRead, MarkDeleted, SetLocked, and MarkItemReceived.
The old Mail.Send(), Mail.MarkRead(), GetMailListForCharacter, and
GetMailByID free functions are removed. Guild handlers that sent mail
via Mail.Send(s, ...) now call mailRepo directly.
Per anti-patterns.md item #9, guild-related SQL was scattered across
~15 handler files with no repository abstraction. Following the same
pattern established by CharacterRepository, this centralizes all
guilds, guild_characters, and guild_applications table access into a
single GuildRepository (~30 methods).
guild_model.go and handlers_guild_member.go are trimmed to types and
pure business logic only. All handler files (guild_*, festa, mail,
house, mercenary, rengoku) now call s.server.guildRepo methods
instead of direct DB queries or methods on domain objects.
Enable multiple Erupe instances to share a single PostgreSQL database
without destroying each other's state, fix existing data races in
cross-channel access, and lay groundwork for future distributed
channel server deployments.
Phase 1 — DB safety:
- Scope DELETE FROM servers/sign_sessions to this instance's server IDs
- Fix ci++ bug where failed channel start shifted subsequent IDs
Phase 2 — Fix data races in cross-channel access:
- Lock sessions map in FindSessionByCharID and DisconnectUser
- Lock stagesLock in handleMsgSysLockGlobalSema
- Snapshot sessions/stages under lock in TransitMessage types 1-4
- Lock channel when finding mail notification targets
Phase 3 — ChannelRegistry interface:
- Define ChannelRegistry interface with 7 cross-channel operations
- Implement LocalChannelRegistry with proper locking
- Add SessionSnapshot/StageSnapshot immutable copy types
- Delegate WorldcastMHF, FindSessionByCharID, DisconnectUser to Registry
- Migrate LockGlobalSema and guild mail handlers to use Registry
- Add comprehensive tests including concurrent access
Phase 4 — Per-channel enable/disable:
- Add Enabled *bool to EntranceChannelInfo (nil defaults to true)
- Skip disabled channels in startup loop, preserving ID stability
- Add IsEnabled() helper with backward-compatible default
- Update config.example.json with Enabled field