From e1a461e892fafa3c48406c460c1c2964b3378304 Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Mon, 27 Oct 2025 01:11:57 +0100 Subject: [PATCH] fix(stage): fix deadlock that was preventing stage change. --- CHANGELOG.md | 3 + .../27-fix-character-defaults.sql | 15 + server/channelserver/handlers_stage.go | 25 +- .../channelserver/sys_channel_server_test.go | 730 ++++++++++++++++++ server/channelserver/sys_session.go | 10 +- 5 files changed, 772 insertions(+), 11 deletions(-) create mode 100644 schemas/patch-schema/27-fix-character-defaults.sql create mode 100644 server/channelserver/sys_channel_server_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 65121d4cb..a31a1f404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Config file handling and validation - Fixes 3 critical race condition in handlers_stage.go. - Fix an issue causing a crash on clans with 0 members. +- Fixed deadlock in zone change causing 60-second timeout when players change zones +- Fixed crash when sending empty packets in QueueSend/QueueSendNonBlocking +- Fixed missing stage transfer packet for empty zones ### Security diff --git a/schemas/patch-schema/27-fix-character-defaults.sql b/schemas/patch-schema/27-fix-character-defaults.sql new file mode 100644 index 000000000..55f9fb4d0 --- /dev/null +++ b/schemas/patch-schema/27-fix-character-defaults.sql @@ -0,0 +1,15 @@ +BEGIN; + +-- Initialize otomoairou (mercenary data) with default empty data for characters that have NULL or empty values +-- This prevents error logs when loading mercenary data during zone transitions +UPDATE characters +SET otomoairou = decode(repeat('00', 10), 'hex') +WHERE otomoairou IS NULL OR length(otomoairou) = 0; + +-- Initialize platemyset (plate configuration) with default empty data for characters that have NULL or empty values +-- This prevents error logs when loading plate data during zone transitions +UPDATE characters +SET platemyset = decode(repeat('00', 1920), 'hex') +WHERE platemyset IS NULL OR length(platemyset) = 0; + +COMMIT; diff --git a/server/channelserver/handlers_stage.go b/server/channelserver/handlers_stage.go index a1e0f55b5..233b58271 100644 --- a/server/channelserver/handlers_stage.go +++ b/server/channelserver/handlers_stage.go @@ -59,7 +59,8 @@ func doStageTransfer(s *Session, ackHandle uint32, stageID string) { s.Unlock() // Tell the client to cleanup its current stage objects. - s.QueueSendMHFNonBlocking(&mhfpacket.MsgSysCleanupObject{}) + // Use blocking send to ensure this critical cleanup packet is not dropped. + s.QueueSendMHF(&mhfpacket.MsgSysCleanupObject{}) // Confirm the stage entry. doAckSimpleSucceed(s, ackHandle, []byte{0x00, 0x00, 0x00, 0x00}) @@ -131,9 +132,11 @@ func doStageTransfer(s *Session, ackHandle uint32, stageID string) { } } - if len(newNotif.Data()) > 2 { - s.QueueSendNonBlocking(newNotif.Data()) - } + // FIX: Always send stage transfer packet, even if empty. + // The client expects this packet to complete the zone change, regardless of content. + // Previously, if newNotif was empty (no users, no objects), no packet was sent, + // causing the client to timeout after 60 seconds. + s.QueueSend(newNotif.Data()) } func destructEmptyStages(s *Session) { @@ -159,7 +162,6 @@ func removeSessionFromStage(s *Session) { // Acquire stage lock to protect concurrent access to clients and objects maps // This prevents race conditions when multiple goroutines access these maps s.stage.Lock() - defer s.stage.Unlock() // Remove client from old stage. delete(s.stage.clients, s) @@ -173,11 +175,18 @@ func removeSessionFromStage(s *Session) { } } - // Now delete the objects after iteration is complete - s.logger.Info("Sending notification to old stage clients") + // Delete from map while still holding lock + for _, object := range objectsToDelete { + delete(s.stage.objects, object.ownerCharID) + } + + // CRITICAL FIX: Unlock BEFORE broadcasting to avoid deadlock + // BroadcastMHF also tries to lock the stage, so we must release our lock first + s.stage.Unlock() + + // Now broadcast the deletions (without holding the lock) for _, object := range objectsToDelete { s.stage.BroadcastMHF(&mhfpacket.MsgSysDeleteObject{ObjID: object.id}, s) - delete(s.stage.objects, object.ownerCharID) } destructEmptyStages(s) diff --git a/server/channelserver/sys_channel_server_test.go b/server/channelserver/sys_channel_server_test.go new file mode 100644 index 000000000..9ef256e7f --- /dev/null +++ b/server/channelserver/sys_channel_server_test.go @@ -0,0 +1,730 @@ +package channelserver + +import ( + "fmt" + "net" + "sync" + "testing" + "time" + + _config "erupe-ce/config" + "erupe-ce/network/clientctx" + "erupe-ce/network/mhfpacket" + + "go.uber.org/zap" +) + +// mockConn implements net.Conn for testing +type mockConn struct { + net.Conn + closeCalled bool + mu sync.Mutex + remoteAddr net.Addr +} + +func (m *mockConn) Close() error { + m.mu.Lock() + defer m.mu.Unlock() + m.closeCalled = true + return nil +} + +func (m *mockConn) RemoteAddr() net.Addr { + if m.remoteAddr != nil { + return m.remoteAddr + } + return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 12345} +} + +func (m *mockConn) Read(b []byte) (n int, err error) { return 0, nil } +func (m *mockConn) Write(b []byte) (n int, err error) { return len(b), nil } +func (m *mockConn) LocalAddr() net.Addr { return &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 54321} } +func (m *mockConn) SetDeadline(t time.Time) error { return nil } +func (m *mockConn) SetReadDeadline(t time.Time) error { return nil } +func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil } + +func (m *mockConn) WasClosed() bool { + m.mu.Lock() + defer m.mu.Unlock() + return m.closeCalled +} + +// createTestServer creates a test server instance +func createTestServer() *Server { + logger, _ := zap.NewDevelopment() + return &Server{ + ID: 1, + logger: logger, + sessions: make(map[net.Conn]*Session), + objectIDs: make(map[*Session]uint16), + stages: make(map[string]*Stage), + semaphore: make(map[string]*Semaphore), + questCacheData: make(map[int][]byte), + questCacheTime: make(map[int]time.Time), + erupeConfig: &_config.Config{ + DebugOptions: _config.DebugOptions{ + LogOutboundMessages: false, + LogInboundMessages: false, + }, + }, + raviente: &Raviente{ + id: 1, + register: make([]uint32, 30), + state: make([]uint32, 30), + support: make([]uint32, 30), + }, + } +} + +// createTestSessionForServer creates a session for a specific server +func createTestSessionForServer(server *Server, conn net.Conn, charID uint32, name string) *Session { + mock := &MockCryptConn{sentPackets: make([][]byte, 0)} + s := &Session{ + logger: server.logger, + server: server, + rawConn: conn, + cryptConn: mock, + sendPackets: make(chan packet, 20), + clientContext: &clientctx.ClientContext{}, + lastPacket: time.Now(), + charID: charID, + Name: name, + } + return s +} + +// TestNewServer tests server initialization +func TestNewServer(t *testing.T) { + logger, _ := zap.NewDevelopment() + config := &Config{ + ID: 1, + Logger: logger, + ErupeConfig: &_config.Config{ + DebugOptions: _config.DebugOptions{}, + }, + Name: "test-server", + } + + server := NewServer(config) + + if server == nil { + t.Fatal("NewServer returned nil") + } + + if server.ID != 1 { + t.Errorf("Server ID = %d, want 1", server.ID) + } + + // Verify default stages are initialized + expectedStages := []string{ + "sl1Ns200p0a0u0", // Mezeporta + "sl1Ns211p0a0u0", // Rasta bar + "sl1Ns260p0a0u0", // Pallone Caravan + "sl1Ns262p0a0u0", // Pallone Guest House 1st Floor + "sl1Ns263p0a0u0", // Pallone Guest House 2nd Floor + "sl2Ns379p0a0u0", // Diva fountain + "sl1Ns462p0a0u0", // MezFes + } + + for _, stageID := range expectedStages { + if _, exists := server.stages[stageID]; !exists { + t.Errorf("Default stage %s not initialized", stageID) + } + } + + // Verify raviente initialization + if server.raviente == nil { + t.Error("Raviente not initialized") + } + if server.raviente.id != 1 { + t.Errorf("Raviente ID = %d, want 1", server.raviente.id) + } +} + +// TestSessionTimeout tests the session timeout mechanism +func TestSessionTimeout(t *testing.T) { + tests := []struct { + name string + lastPacketAge time.Duration + wantTimeout bool + }{ + { + name: "fresh_session_no_timeout", + lastPacketAge: 5 * time.Second, + wantTimeout: false, + }, + { + name: "old_session_should_timeout", + lastPacketAge: 65 * time.Second, + wantTimeout: true, + }, + { + name: "just_under_60s_no_timeout", + lastPacketAge: 59 * time.Second, + wantTimeout: false, + }, + { + name: "just_over_60s_timeout", + lastPacketAge: 61 * time.Second, + wantTimeout: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := createTestServer() + conn := &mockConn{} + session := createTestSessionForServer(server, conn, 1, "TestChar") + + // Set last packet time in the past + session.lastPacket = time.Now().Add(-tt.lastPacketAge) + + server.Lock() + server.sessions[conn] = session + server.Unlock() + + // Run one iteration of session invalidation + for _, sess := range server.sessions { + if time.Since(sess.lastPacket) > time.Second*time.Duration(60) { + server.logger.Info("session timeout", zap.String("Name", sess.Name)) + // Don't actually call logoutPlayer in test, just mark as closed + sess.closed.Store(true) + } + } + + gotTimeout := session.closed.Load() + if gotTimeout != tt.wantTimeout { + t.Errorf("session timeout = %v, want %v (age: %v)", gotTimeout, tt.wantTimeout, tt.lastPacketAge) + } + }) + } +} + +// TestBroadcastMHF tests broadcasting messages to all sessions +func TestBroadcastMHF(t *testing.T) { + server := createTestServer() + + // Create multiple sessions + sessions := make([]*Session, 3) + conns := make([]*mockConn, 3) + for i := 0; i < 3; i++ { + conn := &mockConn{remoteAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 10000 + i}} + conns[i] = conn + sessions[i] = createTestSessionForServer(server, conn, uint32(i+1), fmt.Sprintf("Player%d", i+1)) + + // Start the send loop for this session + go sessions[i].sendLoop() + + server.Lock() + server.sessions[conn] = sessions[i] + server.Unlock() + } + + // Create a test packet + testPkt := &mhfpacket.MsgSysNop{} + + // Broadcast to all except first session + server.BroadcastMHF(testPkt, sessions[0]) + + // Give time for processing + time.Sleep(100 * time.Millisecond) + + // Stop all sessions + for _, sess := range sessions { + sess.closed.Store(true) + } + time.Sleep(50 * time.Millisecond) + + // Verify sessions[0] didn't receive the packet + mock0 := sessions[0].cryptConn.(*MockCryptConn) + if mock0.PacketCount() > 0 { + t.Errorf("Ignored session received %d packets, want 0", mock0.PacketCount()) + } + + // Verify sessions[1] and sessions[2] received the packet + for i := 1; i < 3; i++ { + mock := sessions[i].cryptConn.(*MockCryptConn) + if mock.PacketCount() == 0 { + t.Errorf("Session %d received 0 packets, want 1", i) + } + } +} + +// TestBroadcastMHFAllSessions tests broadcasting to all sessions (no ignored session) +func TestBroadcastMHFAllSessions(t *testing.T) { + server := createTestServer() + + // Create multiple sessions + sessionCount := 5 + sessions := make([]*Session, sessionCount) + for i := 0; i < sessionCount; i++ { + conn := &mockConn{remoteAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 20000 + i}} + session := createTestSessionForServer(server, conn, uint32(i+1), fmt.Sprintf("Player%d", i+1)) + sessions[i] = session + + // Start the send loop + go session.sendLoop() + + server.Lock() + server.sessions[conn] = session + server.Unlock() + } + + // Broadcast to all sessions + testPkt := &mhfpacket.MsgSysNop{} + server.BroadcastMHF(testPkt, nil) + + time.Sleep(100 * time.Millisecond) + + // Stop all sessions + for _, sess := range sessions { + sess.closed.Store(true) + } + time.Sleep(50 * time.Millisecond) + + // Verify all sessions received the packet + receivedCount := 0 + for _, sess := range server.sessions { + mock := sess.cryptConn.(*MockCryptConn) + if mock.PacketCount() > 0 { + receivedCount++ + } + } + + if receivedCount != sessionCount { + t.Errorf("Received count = %d, want %d", receivedCount, sessionCount) + } +} + +// TestFindSessionByCharID tests finding sessions by character ID +func TestFindSessionByCharID(t *testing.T) { + server := createTestServer() + server.Channels = []*Server{server} // Add itself as a channel + + // Create sessions with different char IDs + charIDs := []uint32{100, 200, 300} + for _, charID := range charIDs { + conn := &mockConn{remoteAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: int(30000 + charID)}} + session := createTestSessionForServer(server, conn, charID, fmt.Sprintf("Char%d", charID)) + + server.Lock() + server.sessions[conn] = session + server.Unlock() + } + + tests := []struct { + name string + charID uint32 + wantFound bool + }{ + { + name: "existing_char_100", + charID: 100, + wantFound: true, + }, + { + name: "existing_char_200", + charID: 200, + wantFound: true, + }, + { + name: "non_existing_char", + charID: 999, + wantFound: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + session := server.FindSessionByCharID(tt.charID) + found := session != nil + + if found != tt.wantFound { + t.Errorf("FindSessionByCharID(%d) found = %v, want %v", tt.charID, found, tt.wantFound) + } + + if found && session.charID != tt.charID { + t.Errorf("Found session charID = %d, want %d", session.charID, tt.charID) + } + }) + } +} + +// TestHasSemaphore tests checking if a session has a semaphore +func TestHasSemaphore(t *testing.T) { + server := createTestServer() + conn1 := &mockConn{} + conn2 := &mockConn{} + + session1 := createTestSessionForServer(server, conn1, 1, "Player1") + session2 := createTestSessionForServer(server, conn2, 2, "Player2") + + // Create a semaphore hosted by session1 + sem := &Semaphore{ + id: 1, + name: "test_semaphore", + host: session1, + clients: make(map[*Session]uint32), + } + + server.semaphoreLock.Lock() + server.semaphore["test_semaphore"] = sem + server.semaphoreLock.Unlock() + + // Test session1 has semaphore + if !server.HasSemaphore(session1) { + t.Error("HasSemaphore(session1) = false, want true") + } + + // Test session2 doesn't have semaphore + if server.HasSemaphore(session2) { + t.Error("HasSemaphore(session2) = true, want false") + } +} + +// TestSeason tests the season calculation +func TestSeason(t *testing.T) { + server := createTestServer() + + tests := []struct { + name string + serverID uint16 + }{ + { + name: "server_1", + serverID: 0x1000, + }, + { + name: "server_2", + serverID: 0x1100, + }, + { + name: "server_3", + serverID: 0x1200, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server.ID = tt.serverID + season := server.Season() + + // Season should be 0, 1, or 2 + if season > 2 { + t.Errorf("Season() = %d, want 0-2", season) + } + }) + } +} + +// TestRaviMultiplier tests the Raviente damage multiplier calculation +func TestRaviMultiplier(t *testing.T) { + server := createTestServer() + + // Create a Raviente semaphore (name must end with "3" for getRaviSemaphore) + conn := &mockConn{} + hostSession := createTestSessionForServer(server, conn, 1, "RaviHost") + + sem := &Semaphore{ + id: 1, + name: "hs_l0u3", + host: hostSession, + clients: make(map[*Session]uint32), + } + + server.semaphoreLock.Lock() + server.semaphore["hs_l0u3"] = sem + server.semaphoreLock.Unlock() + + tests := []struct { + name string + clientCount int + register9 uint32 + wantMultiple float64 + }{ + { + name: "small_quest_enough_players", + clientCount: 4, + register9: 0, + wantMultiple: 1.0, + }, + { + name: "small_quest_too_few_players", + clientCount: 2, + register9: 0, + wantMultiple: 2.0, // 4 / 2 + }, + { + name: "large_quest_enough_players", + clientCount: 24, + register9: 10, + wantMultiple: 1.0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up register + server.raviente.register[9] = tt.register9 + + // Add clients to semaphore + sem.clients = make(map[*Session]uint32) + for i := 0; i < tt.clientCount; i++ { + mockConn := &mockConn{} + sess := createTestSessionForServer(server, mockConn, uint32(i+10), fmt.Sprintf("RaviPlayer%d", i)) + sem.clients[sess] = uint32(i + 10) + } + + multiplier := server.GetRaviMultiplier() + if multiplier != tt.wantMultiple { + t.Errorf("GetRaviMultiplier() = %v, want %v", multiplier, tt.wantMultiple) + } + }) + } +} + +// TestUpdateRavi tests Raviente state updates +func TestUpdateRavi(t *testing.T) { + server := createTestServer() + + tests := []struct { + name string + semaID uint32 + index uint8 + value uint32 + update bool + wantValue uint32 + }{ + { + name: "set_support_value", + semaID: 0x50000, + index: 3, + value: 250, + update: false, + wantValue: 250, + }, + { + name: "set_register_value", + semaID: 0x60000, + index: 1, + value: 42, + update: false, + wantValue: 42, + }, + { + name: "increment_register_value", + semaID: 0x60000, + index: 1, + value: 8, + update: true, + wantValue: 50, // Previous test set it to 42 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, newValue := server.UpdateRavi(tt.semaID, tt.index, tt.value, tt.update) + if newValue != tt.wantValue { + t.Errorf("UpdateRavi() new value = %d, want %d", newValue, tt.wantValue) + } + + // Verify the value was actually stored + var storedValue uint32 + switch tt.semaID { + case 0x40000: + storedValue = server.raviente.state[tt.index] + case 0x50000: + storedValue = server.raviente.support[tt.index] + case 0x60000: + storedValue = server.raviente.register[tt.index] + } + + if storedValue != tt.wantValue { + t.Errorf("Stored value = %d, want %d", storedValue, tt.wantValue) + } + }) + } +} + +// TestResetRaviente tests Raviente reset functionality +func TestResetRaviente(t *testing.T) { + server := createTestServer() + + // Set some non-zero values + server.raviente.id = 5 + server.raviente.register[0] = 100 + server.raviente.state[1] = 200 + server.raviente.support[2] = 300 + + // Reset should happen when no Raviente semaphores exist + server.resetRaviente() + + // Verify ID incremented + if server.raviente.id != 6 { + t.Errorf("Raviente ID = %d, want 6", server.raviente.id) + } + + // Verify arrays were reset + for i := 0; i < 30; i++ { + if server.raviente.register[i] != 0 { + t.Errorf("register[%d] = %d, want 0", i, server.raviente.register[i]) + } + if server.raviente.state[i] != 0 { + t.Errorf("state[%d] = %d, want 0", i, server.raviente.state[i]) + } + if server.raviente.support[i] != 0 { + t.Errorf("support[%d] = %d, want 0", i, server.raviente.support[i]) + } + } +} + +// TestBroadcastChatMessage tests chat message broadcasting +func TestBroadcastChatMessage(t *testing.T) { + server := createTestServer() + server.name = "TestServer" + + // Create a session to receive the broadcast + conn := &mockConn{} + session := createTestSessionForServer(server, conn, 1, "Player1") + + // Start the send loop + go session.sendLoop() + + server.Lock() + server.sessions[conn] = session + server.Unlock() + + // Broadcast a message + server.BroadcastChatMessage("Test message") + + time.Sleep(100 * time.Millisecond) + + // Stop the session + session.closed.Store(true) + time.Sleep(50 * time.Millisecond) + + // Verify the session received a packet + mock := session.cryptConn.(*MockCryptConn) + if mock.PacketCount() == 0 { + t.Error("Session didn't receive chat broadcast") + } + + // Verify the packet contains the chat message (basic check) + packets := mock.GetSentPackets() + if len(packets) == 0 { + t.Fatal("No packets sent") + } + + // The packet should be non-empty + if len(packets[0]) == 0 { + t.Error("Empty packet sent for chat message") + } +} + +// TestConcurrentSessionAccess tests thread safety of session map access +func TestConcurrentSessionAccess(t *testing.T) { + server := createTestServer() + + // Run concurrent operations on the session map + var wg sync.WaitGroup + iterations := 100 + + // Concurrent additions + wg.Add(iterations) + for i := 0; i < iterations; i++ { + go func(id int) { + defer wg.Done() + conn := &mockConn{remoteAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 40000 + id}} + session := createTestSessionForServer(server, conn, uint32(id), fmt.Sprintf("Concurrent%d", id)) + + server.Lock() + server.sessions[conn] = session + server.Unlock() + }(i) + } + wg.Wait() + + // Verify all sessions were added + server.Lock() + count := len(server.sessions) + server.Unlock() + + if count != iterations { + t.Errorf("Session count = %d, want %d", count, iterations) + } + + // Concurrent reads + wg.Add(iterations) + for i := 0; i < iterations; i++ { + go func() { + defer wg.Done() + server.Lock() + _ = len(server.sessions) + server.Unlock() + }() + } + wg.Wait() +} + +// TestFindObjectByChar tests finding objects by character ID +func TestFindObjectByChar(t *testing.T) { + server := createTestServer() + + // Create a stage with objects + stage := NewStage("test_stage") + obj1 := &Object{ + id: 1, + ownerCharID: 100, + } + obj2 := &Object{ + id: 2, + ownerCharID: 200, + } + + stage.objects[1] = obj1 + stage.objects[2] = obj2 + + server.stagesLock.Lock() + server.stages["test_stage"] = stage + server.stagesLock.Unlock() + + tests := []struct { + name string + charID uint32 + wantFound bool + wantObjID uint32 + }{ + { + name: "find_char_100_object", + charID: 100, + wantFound: true, + wantObjID: 1, + }, + { + name: "find_char_200_object", + charID: 200, + wantFound: true, + wantObjID: 2, + }, + { + name: "char_not_found", + charID: 999, + wantFound: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + obj := server.FindObjectByChar(tt.charID) + found := obj != nil + + if found != tt.wantFound { + t.Errorf("FindObjectByChar(%d) found = %v, want %v", tt.charID, found, tt.wantFound) + } + + if found && obj.id != tt.wantObjID { + t.Errorf("Found object ID = %d, want %d", obj.id, tt.wantObjID) + } + }) + } +} diff --git a/server/channelserver/sys_session.go b/server/channelserver/sys_session.go index ac69131b0..fc1f5e05b 100644 --- a/server/channelserver/sys_session.go +++ b/server/channelserver/sys_session.go @@ -104,7 +104,9 @@ func (s *Session) Start() { // QueueSend queues a packet (raw []byte) to be sent. func (s *Session) QueueSend(data []byte) { - s.logMessage(binary.BigEndian.Uint16(data[0:2]), data, "Server", s.Name) + if len(data) >= 2 { + s.logMessage(binary.BigEndian.Uint16(data[0:2]), data, "Server", s.Name) + } s.sendPackets <- packet{data, true} } @@ -112,7 +114,9 @@ func (s *Session) QueueSend(data []byte) { func (s *Session) QueueSendNonBlocking(data []byte) { select { case s.sendPackets <- packet{data, true}: - s.logMessage(binary.BigEndian.Uint16(data[0:2]), data, "Server", s.Name) + if len(data) >= 2 { + s.logMessage(binary.BigEndian.Uint16(data[0:2]), data, "Server", s.Name) + } default: s.logger.Warn("Packet queue too full, dropping!") } @@ -163,7 +167,7 @@ func (s *Session) sendLoop() { pkt := <-s.sendPackets err := s.cryptConn.SendPacket(append(pkt.data, []byte{0x00, 0x10}...)) if err != nil { - s.logger.Warn("Failed to send packet") + s.logger.Warn("Failed to send packet", zap.Error(err)) } } time.Sleep(time.Duration(_config.ErupeConfig.LoopDelay) * time.Millisecond)