Files
Erupe/server/channelserver/handlers_data.go
Houmgaor 24ccc167fe fix(channelserver): add fail ACKs to silent error paths to prevent client softlocks
Handlers that log errors and return without sending a MsgSysAck leave
the client waiting indefinitely. Add doAckSimpleFail/doAckBufFail to
14 error paths across 4 files, matching the pattern already used in
~70 other error paths across the codebase.

Affected handlers:
- handleMsgMhfGetCafeDuration (1 path)
- handleMsgMhfSavedata (1 path)
- handleMsgMhfArrangeGuildMember (3 paths)
- handleMsgMhfEnumerateGuildMember (5 paths)
- handleMsgSysLogin (4 paths)
- handleMsgSysIssueLogkey (1 path)
2026-02-20 19:35:25 +01:00

208 lines
6.7 KiB
Go

package channelserver
import (
"erupe-ce/common/stringsupport"
_config "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"
)
func handleMsgMhfSavedata(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfSavedata)
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
}
// 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.Decompress(pkt.RawDataPayload)
if err != nil {
s.logger.Error("Failed to decompress diff", zap.Error(err))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
// Perform diff.
s.logger.Info("Diffing...")
characterSaveData.decompSave = deltacomp.ApplyDataDiff(diff, characterSaveData.decompSave)
} else {
dumpSaveData(s, pkt.RawDataPayload, "savedata")
// Regular blob update.
saveData, err := nullcomp.Decompress(pkt.RawDataPayload)
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()
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 <= _config.S10 {
characterSaveData.Save(s)
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.db.Exec("UPDATE characters SET deleted=true WHERE id=$1", s.charID); err != nil {
s.logger.Error("Failed to mark character as deleted", zap.Error(err))
}
}
return
}
_, err = s.server.db.Exec("UPDATE characters SET name=$1 WHERE id=$2", characterSaveData.Name, s.charID)
if 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, _ := os.ReadFile(filepath.Join(s.server.erupeConfig.BinPath, "save_override.bin"))
doAckBufSucceed(s, pkt.AckHandle, data)
return
}
var data []byte
err := s.server.db.QueryRow("SELECT savedata FROM characters WHERE id = $1", s.charID).Scan(&data)
if err != nil || len(data) == 0 {
s.logger.Warn(fmt.Sprintf("Failed to load savedata (CID: %d)", s.charID), zap.Error(err))
_ = s.rawConn.Close() // Terminate the connection
return
}
doAckBufSucceed(s, pkt.AckHandle, data)
decompSaveData, err := nullcomp.Decompress(data)
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.userBinaryPartsLock.Lock()
s.server.userBinaryParts[userBinaryPartID{charID: s.charID, index: 1}] = append(name, []byte{0x00}...)
s.server.userBinaryPartsLock.Unlock()
s.Name, _ = stringsupport.SJISToUTF8(name)
}
func handleMsgMhfSaveScenarioData(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfSaveScenarioData)
if len(pkt.RawDataPayload) > 65536 {
s.logger.Warn("Scenario payload too large", zap.Int("len", len(pkt.RawDataPayload)))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
dumpSaveData(s, pkt.RawDataPayload, "scenario")
_, err := s.server.db.Exec("UPDATE characters SET scenariodata = $1 WHERE id = $2", pkt.RawDataPayload, s.charID)
if err != nil {
s.logger.Error("Failed to update scenario data in db", zap.Error(err))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}
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) {}