mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-26 09:33:02 +01:00
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)
This commit is contained in:
@@ -505,3 +505,49 @@ func TestOperateJoint_NilAlliance(t *testing.T) {
|
||||
t.Error("No response packet queued — would softlock the client")
|
||||
}
|
||||
}
|
||||
|
||||
// --- scanAllianceWithGuilds nil guild tests (issue #171) ---
|
||||
|
||||
func TestInfoJoint_MissingSubGuild1(t *testing.T) {
|
||||
// Verify that GetAllianceByID returns an error when sub guild 1 references
|
||||
// a non-existent guild (nil return from GetByID). This is the scenario from
|
||||
// issue #171 — a deleted guild causes a nil dereference in scanAllianceWithGuilds.
|
||||
server := createMockServer()
|
||||
guildMock := &mockGuildRepo{
|
||||
// GetAllianceByID returns an error for missing guilds because
|
||||
// scanAllianceWithGuilds calls GetByID for each sub guild.
|
||||
// With guild=nil and SubGuild1ID > 0, GetByID returns nil,
|
||||
// and scanAllianceWithGuilds should return an error rather than panic.
|
||||
getAllianceErr: errNotFound,
|
||||
}
|
||||
server.guildRepo = guildMock
|
||||
session := createMockSession(1, server)
|
||||
|
||||
pkt := &mhfpacket.MsgMhfInfoJoint{AckHandle: 100, AllianceID: 5}
|
||||
handleMsgMhfInfoJoint(session, pkt)
|
||||
|
||||
// Handler should send a response even on error (not softlock)
|
||||
select {
|
||||
case <-session.sendPackets:
|
||||
default:
|
||||
t.Error("No response packet queued — would softlock the client")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInfoJoint_MissingSubGuild2(t *testing.T) {
|
||||
server := createMockServer()
|
||||
guildMock := &mockGuildRepo{
|
||||
getAllianceErr: errNotFound,
|
||||
}
|
||||
server.guildRepo = guildMock
|
||||
session := createMockSession(1, server)
|
||||
|
||||
pkt := &mhfpacket.MsgMhfInfoJoint{AckHandle: 100, AllianceID: 6}
|
||||
handleMsgMhfInfoJoint(session, pkt)
|
||||
|
||||
select {
|
||||
case <-session.sendPackets:
|
||||
default:
|
||||
t.Error("No response packet queued — would softlock the client")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +194,12 @@ func handleMsgMhfSaveMercenary(s *Session, p mhfpacket.MHFPacket) {
|
||||
dumpSaveData(s, pkt.MercData, "mercenary")
|
||||
if len(pkt.MercData) >= 4 {
|
||||
temp := byteframe.NewByteFrameFromBytes(pkt.MercData)
|
||||
if err := s.server.charRepo.SaveMercenary(s.charID, pkt.MercData, temp.ReadUint32()); err != nil {
|
||||
rastaID := temp.ReadUint32()
|
||||
if rastaID == 0 {
|
||||
s.logger.Warn("Mercenary save with rasta_id=0, preserving existing value",
|
||||
zap.Uint32("charID", s.charID))
|
||||
}
|
||||
if err := s.server.charRepo.SaveMercenary(s.charID, pkt.MercData, rastaID); err != nil {
|
||||
s.logger.Error("Failed to save mercenary data", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
59
server/channelserver/handlers_table_test.go
Normal file
59
server/channelserver/handlers_table_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package channelserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"erupe-ce/network"
|
||||
)
|
||||
|
||||
func TestBuildHandlerTable_EntryCount(t *testing.T) {
|
||||
table := buildHandlerTable()
|
||||
// handlers_table.go has exactly 432 entries (one per packet ID).
|
||||
// This test catches accidental deletions or duplicates.
|
||||
const expectedCount = 432
|
||||
if len(table) != expectedCount {
|
||||
t.Errorf("handler table has %d entries, want %d", len(table), expectedCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHandlerTable_CriticalOpcodes(t *testing.T) {
|
||||
table := buildHandlerTable()
|
||||
|
||||
critical := []struct {
|
||||
name string
|
||||
id network.PacketID
|
||||
}{
|
||||
{"MSG_SYS_LOGIN", network.MSG_SYS_LOGIN},
|
||||
{"MSG_SYS_LOGOUT", network.MSG_SYS_LOGOUT},
|
||||
{"MSG_SYS_PING", network.MSG_SYS_PING},
|
||||
{"MSG_SYS_ACK", network.MSG_SYS_ACK},
|
||||
{"MSG_SYS_CAST_BINARY", network.MSG_SYS_CAST_BINARY},
|
||||
{"MSG_SYS_ENTER_STAGE", network.MSG_SYS_ENTER_STAGE},
|
||||
{"MSG_SYS_LEAVE_STAGE", network.MSG_SYS_LEAVE_STAGE},
|
||||
{"MSG_MHF_SAVEDATA", network.MSG_MHF_SAVEDATA},
|
||||
{"MSG_MHF_LOADDATA", network.MSG_MHF_LOADDATA},
|
||||
{"MSG_MHF_ENUMERATE_QUEST", network.MSG_MHF_ENUMERATE_QUEST},
|
||||
{"MSG_MHF_CREATE_GUILD", network.MSG_MHF_CREATE_GUILD},
|
||||
{"MSG_MHF_INFO_GUILD", network.MSG_MHF_INFO_GUILD},
|
||||
{"MSG_MHF_GET_ACHIEVEMENT", network.MSG_MHF_GET_ACHIEVEMENT},
|
||||
{"MSG_MHF_PLAY_NORMAL_GACHA", network.MSG_MHF_PLAY_NORMAL_GACHA},
|
||||
{"MSG_MHF_SEND_MAIL", network.MSG_MHF_SEND_MAIL},
|
||||
{"MSG_MHF_SAVE_RENGOKU_DATA", network.MSG_MHF_SAVE_RENGOKU_DATA},
|
||||
{"MSG_MHF_LOAD_RENGOKU_DATA", network.MSG_MHF_LOAD_RENGOKU_DATA},
|
||||
}
|
||||
|
||||
for _, tc := range critical {
|
||||
if _, ok := table[tc.id]; !ok {
|
||||
t.Errorf("critical opcode %s (0x%04X) is not mapped in handler table", tc.name, uint16(tc.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildHandlerTable_NoNilHandlers(t *testing.T) {
|
||||
table := buildHandlerTable()
|
||||
for id, handler := range table {
|
||||
if handler == nil {
|
||||
t.Errorf("handler for opcode 0x%04X is nil", uint16(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,8 +193,15 @@ func (r *CharacterRepository) ReadGuildPostChecked(charID uint32) (time.Time, er
|
||||
return t, err
|
||||
}
|
||||
|
||||
// SaveMercenary updates savemercenary and rasta_id atomically.
|
||||
// SaveMercenary updates savemercenary and optionally rasta_id.
|
||||
// When rastaID is 0, only the mercenary blob is saved — the existing rasta_id
|
||||
// (typically NULL for characters without a mercenary) is preserved. Writing 0
|
||||
// would pollute GetMercenaryLoans queries that match on pact_id.
|
||||
func (r *CharacterRepository) SaveMercenary(charID uint32, data []byte, rastaID uint32) error {
|
||||
if rastaID == 0 {
|
||||
_, err := r.db.Exec("UPDATE characters SET savemercenary=$1 WHERE id=$2", data, charID)
|
||||
return err
|
||||
}
|
||||
_, err := r.db.Exec("UPDATE characters SET savemercenary=$1, rasta_id=$2 WHERE id=$3", data, rastaID, charID)
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user