From 5b631d1704b21c5807249a0b4ede891606afcda4 Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Mon, 2 Mar 2026 20:12:39 +0100 Subject: [PATCH] perf(channelserver): cache rengoku_data.bin at startup 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. --- docs/anti-patterns.md | 3 +- server/channelserver/handlers_rengoku.go | 8 +-- server/channelserver/handlers_rengoku_test.go | 36 ++++++++++ server/channelserver/sys_channel_server.go | 35 ++++++++++ .../channelserver/sys_channel_server_test.go | 65 +++++++++++++++++++ 5 files changed, 139 insertions(+), 8 deletions(-) diff --git a/docs/anti-patterns.md b/docs/anti-patterns.md index b2d4be63c..fec719a51 100644 --- a/docs/anti-patterns.md +++ b/docs/anti-patterns.md @@ -71,8 +71,7 @@ if err != nil { **Pattern C — Fail ACK (correct):** Error logged, explicit fail ACK sent. The client shows an appropriate error dialog and stays connected. ```go -if err != nil { - s.logger.Error("Failed to read rengoku_data.bin", zap.Error(err)) +if data == nil { doAckBufFail(s, pkt.AckHandle, nil) return } diff --git a/server/channelserver/handlers_rengoku.go b/server/channelserver/handlers_rengoku.go index 4c708c7fe..d93768435 100644 --- a/server/channelserver/handlers_rengoku.go +++ b/server/channelserver/handlers_rengoku.go @@ -3,8 +3,6 @@ package channelserver import ( "encoding/binary" ps "erupe-ce/common/pascalstring" - "os" - "path/filepath" "erupe-ce/common/byteframe" "erupe-ce/network/mhfpacket" @@ -180,10 +178,8 @@ func handleMsgMhfLoadRengokuData(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfGetRengokuBinary(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetRengokuBinary) - // a (massively out of date) version resides in the game's /dat/ folder or up to date can be pulled from packets - data, err := os.ReadFile(filepath.Join(s.server.erupeConfig.BinPath, "rengoku_data.bin")) - if err != nil { - s.logger.Error("Failed to read rengoku_data.bin", zap.Error(err)) + data := s.server.rengokuBin + if data == nil { doAckBufFail(s, pkt.AckHandle, nil) return } diff --git a/server/channelserver/handlers_rengoku_test.go b/server/channelserver/handlers_rengoku_test.go index dfcd77351..078041b8d 100644 --- a/server/channelserver/handlers_rengoku_test.go +++ b/server/channelserver/handlers_rengoku_test.go @@ -520,6 +520,42 @@ func TestEnumerateRengokuRanking_Applicant(t *testing.T) { } } +// --- handleMsgMhfGetRengokuBinary tests --- + +func TestGetRengokuBinary_Cached(t *testing.T) { + server := createMockServer() + server.rengokuBin = []byte{0x65, 0x63, 0x64, 0x1a, 0xDE, 0xAD} + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetRengokuBinary{AckHandle: 100} + handleMsgMhfGetRengokuBinary(session, pkt) + + select { + case p := <-session.sendPackets: + if len(p.data) == 0 { + t.Error("Response should have data") + } + default: + t.Error("No response packet queued") + } +} + +func TestGetRengokuBinary_NilCache(t *testing.T) { + server := createMockServer() + server.rengokuBin = nil + session := createMockSession(1, server) + + pkt := &mhfpacket.MsgMhfGetRengokuBinary{AckHandle: 100} + handleMsgMhfGetRengokuBinary(session, pkt) + + select { + case <-session.sendPackets: + // fail ACK was sent — expected + default: + t.Error("No response packet queued (expected fail ACK)") + } +} + // Tests consolidated from handlers_coverage3_test.go func TestNonTrivialHandlers_RengokuGo(t *testing.T) { diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index 1091c9c8c..9448222af 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -1,9 +1,12 @@ package channelserver import ( + "encoding/binary" "errors" "fmt" "net" + "os" + "path/filepath" "sync" "time" @@ -107,6 +110,8 @@ type Server struct { questCache *QuestCache + rengokuBin []byte // Cached rengoku_data.bin (ECD-encrypted, served to clients as-is) + handlerTable map[network.PacketID]handlerFunc } @@ -187,6 +192,8 @@ func NewServer(config *Config) *Server { // MezFes s.stages.Store("sl1Ns462p0a0u0", NewStage("sl1Ns462p0a0u0")) + s.rengokuBin = loadRengokuBinary(config.ErupeConfig.BinPath, s.logger) + s.i18n = getLangStrings(s) return s @@ -437,3 +444,31 @@ func (s *Server) Season() uint8 { sid := int64(((s.ID & serverIDHighMask) - serverIDBase) / serverIDStride) return uint8(((TimeAdjusted().Unix() / secsPerDay) + sid) % 3) } + +// ecdMagic is the first 4 bytes of an ECD-encrypted file (little-endian "ecd\x1a"). +const ecdMagic = uint32(0x6563641a) + +// loadRengokuBinary reads and validates rengoku_data.bin from binPath. +// Returns the raw bytes on success, or nil if the file is missing or invalid. +func loadRengokuBinary(binPath string, logger *zap.Logger) []byte { + path := filepath.Join(binPath, "rengoku_data.bin") + data, err := os.ReadFile(path) + if err != nil { + logger.Warn("rengoku_data.bin not found, Hunting Road will be unavailable", + zap.String("path", path), zap.Error(err)) + return nil + } + if len(data) < 4 { + logger.Warn("rengoku_data.bin too small, ignoring", + zap.Int("bytes", len(data))) + return nil + } + if magic := binary.LittleEndian.Uint32(data[:4]); magic != ecdMagic { + logger.Warn("rengoku_data.bin has invalid ECD magic, ignoring", + zap.String("expected", "0x6563641a"), + zap.String("got", fmt.Sprintf("0x%08x", magic))) + return nil + } + logger.Info("Loaded rengoku_data.bin", zap.Int("bytes", len(data))) + return data +} diff --git a/server/channelserver/sys_channel_server_test.go b/server/channelserver/sys_channel_server_test.go index dcb95069e..cc38bf55e 100644 --- a/server/channelserver/sys_channel_server_test.go +++ b/server/channelserver/sys_channel_server_test.go @@ -1,8 +1,11 @@ package channelserver import ( + "encoding/binary" "fmt" "net" + "os" + "path/filepath" "sync" "testing" "time" @@ -727,3 +730,65 @@ func TestFindObjectByChar(t *testing.T) { }) } } + +// --- loadRengokuBinary tests --- + +func TestLoadRengokuBinary_ValidECD(t *testing.T) { + dir := t.TempDir() + // Build a minimal valid ECD file: magic + some payload + data := make([]byte, 16) + binary.LittleEndian.PutUint32(data[:4], ecdMagic) + if err := os.WriteFile(filepath.Join(dir, "rengoku_data.bin"), data, 0644); err != nil { + t.Fatal(err) + } + + logger, _ := zap.NewDevelopment() + result := loadRengokuBinary(dir, logger) + + if result == nil { + t.Fatal("Expected non-nil result for valid ECD file") + } + if len(result) != 16 { + t.Errorf("len = %d, want 16", len(result)) + } +} + +func TestLoadRengokuBinary_MissingFile(t *testing.T) { + dir := t.TempDir() + logger, _ := zap.NewDevelopment() + result := loadRengokuBinary(dir, logger) + + if result != nil { + t.Errorf("Expected nil for missing file, got %d bytes", len(result)) + } +} + +func TestLoadRengokuBinary_TooSmall(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "rengoku_data.bin"), []byte{0x65, 0x63}, 0644); err != nil { + t.Fatal(err) + } + + logger, _ := zap.NewDevelopment() + result := loadRengokuBinary(dir, logger) + + if result != nil { + t.Errorf("Expected nil for too-small file, got %d bytes", len(result)) + } +} + +func TestLoadRengokuBinary_BadMagic(t *testing.T) { + dir := t.TempDir() + data := make([]byte, 16) + binary.LittleEndian.PutUint32(data[:4], 0xDEADBEEF) + if err := os.WriteFile(filepath.Join(dir, "rengoku_data.bin"), data, 0644); err != nil { + t.Fatal(err) + } + + logger, _ := zap.NewDevelopment() + result := loadRengokuBinary(dir, logger) + + if result != nil { + t.Errorf("Expected nil for bad magic, got %d bytes", len(result)) + } +}