Files
Erupe/server/channelserver/handlers_data.go
Houmgaor 7ea2660335 docs(stubs): annotate empty handlers and add unimplemented reference doc
Add // stub: unimplemented to 70 empty game-feature handlers and
// stub: reserved to 56 protocol-reserved slots in handlers_reserve.go,
making them discoverable via grep. Add docs/unimplemented.md listing all
unimplemented handlers grouped by subsystem with descriptions.
2026-03-19 10:57:09 +01:00

248 lines
8.3 KiB
Go

package channelserver
import (
"erupe-ce/common/stringsupport"
cfg "erupe-ce/config"
"fmt"
"io"
"os"
"path/filepath"
"time"
"erupe-ce/common/byteframe"
"erupe-ce/network/mhfpacket"
"erupe-ce/server/channelserver/compression/deltacomp"
"erupe-ce/server/channelserver/compression/nullcomp"
"go.uber.org/zap"
)
// Save data size limits.
// The largest known decompressed savedata is ZZ at ~147KB. We use generous
// ceilings to accommodate unknown versions while still catching runaway data.
const (
saveDataMaxCompressedPayload = 524288 // 512KB max compressed payload from client
saveDataMaxDecompressedPayload = 1048576 // 1MB max decompressed savedata
)
func handleMsgMhfSavedata(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfSavedata)
// Serialize saves for the same character to prevent concurrent operations
// from racing and defeating corruption detection.
unlock := s.server.charSaveLocks.Lock(s.charID)
defer unlock()
if len(pkt.RawDataPayload) > saveDataMaxCompressedPayload {
s.logger.Warn("Savedata payload exceeds size limit",
zap.Int("len", len(pkt.RawDataPayload)),
zap.Int("max", saveDataMaxCompressedPayload),
zap.Uint32("charID", s.charID),
)
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
characterSaveData, err := GetCharacterSaveData(s, s.charID)
if err != nil {
s.logger.Error("failed to retrieve character save data from db", zap.Error(err), zap.Uint32("charID", s.charID))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
// Snapshot current house tier before applying the update so we can
// restore it if the incoming data is corrupted (issue #92).
prevHouseTier := make([]byte, len(characterSaveData.HouseTier))
copy(prevHouseTier, characterSaveData.HouseTier)
// Var to hold the decompressed savedata for updating the launcher response fields.
if pkt.SaveType == 1 {
// Diff-based update.
// diffs themselves are also potentially compressed
diff, err := nullcomp.DecompressWithLimit(pkt.RawDataPayload, saveDataMaxDecompressedPayload)
if err != nil {
s.logger.Error("Failed to decompress diff", zap.Error(err))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
// Perform diff with bounds checking.
s.logger.Info("Diffing...")
patched, err := deltacomp.ApplyDataDiffWithLimit(diff, characterSaveData.decompSave, saveDataMaxDecompressedPayload)
if err != nil {
s.logger.Error("Failed to apply save diff", zap.Error(err), zap.Uint32("charID", s.charID))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
characterSaveData.decompSave = patched
} else {
dumpSaveData(s, pkt.RawDataPayload, "savedata")
// Regular blob update.
saveData, err := nullcomp.DecompressWithLimit(pkt.RawDataPayload, saveDataMaxDecompressedPayload)
if err != nil {
s.logger.Error("Failed to decompress savedata from packet", zap.Error(err))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
if s.server.erupeConfig.SaveDumps.RawEnabled {
dumpSaveData(s, saveData, "raw-savedata")
}
s.logger.Info("Updating save with blob")
characterSaveData.decompSave = saveData
}
characterSaveData.updateStructWithSaveData()
// Mitigate house theme corruption (issue #92): the game client
// sometimes sends house_tier as -1 (all 0xFF bytes), which causes
// the house theme to vanish on next login. If the new value looks
// corrupted, restore the previous value in both the struct and the
// decompressed blob so Save() persists consistent data.
if len(prevHouseTier) > 0 && characterSaveData.isHouseTierCorrupted() {
s.logger.Warn("Detected corrupted house_tier in save data, restoring previous value",
zap.Binary("corrupted", characterSaveData.HouseTier),
zap.Binary("restored", prevHouseTier),
zap.Uint32("charID", s.charID),
)
characterSaveData.restoreHouseTier(prevHouseTier)
}
s.playtime = characterSaveData.Playtime
s.playtimeTime = time.Now()
// Bypass name-checker if new
if characterSaveData.IsNewCharacter {
s.Name = characterSaveData.Name
}
// Force name to match session to prevent corruption detection false positives
// This handles SJIS/UTF-8 encoding differences and ensures saves succeed across all game versions
if characterSaveData.Name != s.Name && !characterSaveData.IsNewCharacter {
s.logger.Info("Correcting name mismatch in savedata", zap.String("savedata_name", characterSaveData.Name), zap.String("session_name", s.Name))
characterSaveData.Name = s.Name
characterSaveData.updateSaveDataWithStruct()
}
if characterSaveData.Name == s.Name || s.server.erupeConfig.RealClientMode <= cfg.S10 {
if err := characterSaveData.Save(s); err != nil {
s.logger.Error("Failed to save character data", zap.Error(err))
return
}
s.logger.Info("Wrote recompressed savedata back to DB.")
} else {
_ = s.rawConn.Close()
s.logger.Warn("Save cancelled due to corruption.")
if s.server.erupeConfig.DeleteOnSaveCorruption {
if err := s.server.charRepo.SetDeleted(s.charID); err != nil {
s.logger.Error("Failed to mark character as deleted", zap.Error(err))
}
}
return
}
if err := s.server.charRepo.SaveString(s.charID, "name", characterSaveData.Name); err != nil {
s.logger.Error("Failed to update character name in db", zap.Error(err))
}
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}
func grpToGR(n int) uint16 {
var gr int
a := []int{208750, 593400, 993400, 1400900, 2315900, 3340900, 4505900, 5850900, 7415900, 9230900, 11345900, 100000000}
b := []int{7850, 8000, 8150, 9150, 10250, 11650, 13450, 15650, 18150, 21150, 23950}
c := []int{51, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900}
for i := 0; i < len(a); i++ {
if n < a[i] {
if i == 0 {
for {
n -= 500
if n <= 500 {
if n < 0 {
i--
}
break
} else {
i++
for j := 0; j < i; j++ {
n -= 150
}
}
}
gr = i + 2
} else {
n -= a[i-1]
gr = c[i-1]
gr += n / b[i-1]
}
break
}
}
return uint16(gr)
}
func dumpSaveData(s *Session, data []byte, suffix string) {
if !s.server.erupeConfig.SaveDumps.Enabled {
return
} else {
dir := filepath.Join(s.server.erupeConfig.SaveDumps.OutputDir, fmt.Sprintf("%d", s.charID))
path := filepath.Join(s.server.erupeConfig.SaveDumps.OutputDir, fmt.Sprintf("%d", s.charID), fmt.Sprintf("%d_%s.bin", s.charID, suffix))
_, err := os.Stat(dir)
if err != nil {
if os.IsNotExist(err) {
err = os.MkdirAll(dir, os.ModePerm)
if err != nil {
s.logger.Error("Error dumping savedata, could not create folder")
return
}
} else {
s.logger.Error("Error dumping savedata")
return
}
}
err = os.WriteFile(path, data, 0644)
if err != nil {
s.logger.Error("Error dumping savedata, could not write file", zap.Error(err))
}
}
}
func handleMsgMhfLoaddata(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfLoaddata)
if _, err := os.Stat(filepath.Join(s.server.erupeConfig.BinPath, "save_override.bin")); err == nil {
data, readErr := os.ReadFile(filepath.Join(s.server.erupeConfig.BinPath, "save_override.bin"))
if readErr != nil {
s.logger.Error("Failed to read save_override.bin", zap.Error(readErr))
} else {
doAckBufSucceed(s, pkt.AckHandle, data)
return
}
}
data, err := s.server.charRepo.LoadColumn(s.charID, "savedata")
if err != nil || len(data) == 0 {
s.logger.Warn("Failed to load savedata", zap.Uint32("charID", s.charID), zap.Error(err))
_ = s.rawConn.Close() // Terminate the connection
return
}
doAckBufSucceed(s, pkt.AckHandle, data)
decompSaveData, err := nullcomp.DecompressWithLimit(data, saveDataMaxDecompressedPayload)
if err != nil {
s.logger.Error("Failed to decompress savedata", zap.Error(err))
}
bf := byteframe.NewByteFrameFromBytes(decompSaveData)
_, _ = bf.Seek(88, io.SeekStart)
name := bf.ReadNullTerminatedBytes()
s.server.userBinary.Set(s.charID, 1, append(name, []byte{0x00}...))
s.Name = stringsupport.SJISToUTF8Lossy(name)
}
func handleMsgMhfSaveScenarioData(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfSaveScenarioData)
saveCharacterData(s, pkt.AckHandle, "scenariodata", pkt.RawDataPayload, 65536)
}
func handleMsgMhfLoadScenarioData(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfLoadScenarioData)
loadCharacterData(s, pkt.AckHandle, "scenariodata", make([]byte, 10))
}
func handleMsgSysAuthData(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented