Files
Erupe/server/channelserver/handlers_mercenary.go
Houmgaor d456bd23e0 fix(channelserver): handle ignored DB errors and cache userID on session
Silently ignored DB errors in handlers could cause data loss (frontier
point transactions completing without DB writes), reward duplication
(stamp exchange granting items on failed UPDATE), and crashes (tower
mission page=0 causing index-out-of-bounds). House access state
defaulting to 0 on DB failure also bypassed all access controls.

HIGH risk fixes:
- frontier point buy/sell now fails with ACK on DB error
- stamp exchange/stampcard abort on failed UPDATE
- guild meal INSERT returns fail ACK instead of orphaned ID 0
- mercenary/airou creation aborts on failed sequence nextval

MEDIUM risk fixes:
- tower mission page clamped to >= 1 preventing array underflow
- tower RP donation returns early on failed guild state read
- house state defaults to 2 (password-protected) on DB failure
- playtime read failure logged instead of silently resetting RP

Also cache userID on Session at login time, eliminating ~25 redundant
subqueries of the form WHERE u.id=(SELECT c.user_id FROM characters
c WHERE c.id=$1) across shop, gacha, command, and distitem handlers.
2026-02-20 21:06:16 +01:00

478 lines
15 KiB
Go

package channelserver
import (
"erupe-ce/common/byteframe"
"erupe-ce/common/stringsupport"
_config "erupe-ce/config"
"erupe-ce/network/mhfpacket"
"erupe-ce/server/channelserver/compression/deltacomp"
"erupe-ce/server/channelserver/compression/nullcomp"
"go.uber.org/zap"
"io"
"time"
)
func handleMsgMhfLoadPartner(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfLoadPartner)
loadCharacterData(s, pkt.AckHandle, "partner", make([]byte, 9))
}
func handleMsgMhfSavePartner(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfSavePartner)
saveCharacterData(s, pkt.AckHandle, "partner", pkt.RawDataPayload, 65536)
}
func handleMsgMhfLoadLegendDispatch(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfLoadLegendDispatch)
bf := byteframe.NewByteFrame()
legendDispatch := []struct {
Unk uint32
Timestamp uint32
}{
{0, uint32(TimeMidnight().Add(-12 * time.Hour).Unix())},
{0, uint32(TimeMidnight().Add(12 * time.Hour).Unix())},
{0, uint32(TimeMidnight().Add(36 * time.Hour).Unix())},
}
bf.WriteUint8(uint8(len(legendDispatch)))
for _, dispatch := range legendDispatch {
bf.WriteUint32(dispatch.Unk)
bf.WriteUint32(dispatch.Timestamp)
}
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}
func handleMsgMhfLoadHunterNavi(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfLoadHunterNavi)
naviLength := 552
if s.server.erupeConfig.RealClientMode <= _config.G7 {
naviLength = 280
}
loadCharacterData(s, pkt.AckHandle, "hunternavi", make([]byte, naviLength))
}
func handleMsgMhfSaveHunterNavi(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfSaveHunterNavi)
if len(pkt.RawDataPayload) > 4096 {
s.logger.Warn("HunterNavi payload too large", zap.Int("len", len(pkt.RawDataPayload)))
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
}
saveStart := time.Now()
s.logger.Debug("Hunter Navi save request",
zap.Uint32("charID", s.charID),
zap.Bool("is_diff", pkt.IsDataDiff),
zap.Int("data_size", len(pkt.RawDataPayload)),
)
var dataSize int
if pkt.IsDataDiff {
naviLength := 552
if s.server.erupeConfig.RealClientMode <= _config.G7 {
naviLength = 280
}
var data []byte
// Load existing save
err := s.server.db.QueryRow("SELECT hunternavi FROM characters WHERE id = $1", s.charID).Scan(&data)
if err != nil {
s.logger.Error("Failed to load hunternavi",
zap.Error(err),
zap.Uint32("charID", s.charID),
)
}
// Check if we actually had any hunternavi data, using a blank buffer if not.
// This is requried as the client will try to send a diff after character creation without a prior MsgMhfSaveHunterNavi packet.
if len(data) == 0 {
data = make([]byte, naviLength)
}
// Perform diff and compress it to write back to db
s.logger.Debug("Applying Hunter Navi diff",
zap.Uint32("charID", s.charID),
zap.Int("base_size", len(data)),
zap.Int("diff_size", len(pkt.RawDataPayload)),
)
saveOutput := deltacomp.ApplyDataDiff(pkt.RawDataPayload, data)
dataSize = len(saveOutput)
_, err = s.server.db.Exec("UPDATE characters SET hunternavi=$1 WHERE id=$2", saveOutput, s.charID)
if err != nil {
s.logger.Error("Failed to save hunternavi",
zap.Error(err),
zap.Uint32("charID", s.charID),
zap.Int("data_size", dataSize),
)
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
return
}
} else {
dumpSaveData(s, pkt.RawDataPayload, "hunternavi")
dataSize = len(pkt.RawDataPayload)
// simply update database, no extra processing
_, err := s.server.db.Exec("UPDATE characters SET hunternavi=$1 WHERE id=$2", pkt.RawDataPayload, s.charID)
if err != nil {
s.logger.Error("Failed to save hunternavi",
zap.Error(err),
zap.Uint32("charID", s.charID),
zap.Int("data_size", dataSize),
)
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
return
}
}
saveDuration := time.Since(saveStart)
s.logger.Info("Hunter Navi saved successfully",
zap.Uint32("charID", s.charID),
zap.Bool("was_diff", pkt.IsDataDiff),
zap.Int("data_size", dataSize),
zap.Duration("duration", saveDuration),
)
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
}
func handleMsgMhfMercenaryHuntdata(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfMercenaryHuntdata)
if pkt.RequestType == 1 {
// Format:
// uint8 Hunts
// struct Hunt
// uint32 HuntID
// uint32 MonID
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 1))
} else {
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 0))
}
}
func handleMsgMhfEnumerateMercenaryLog(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfEnumerateMercenaryLog)
bf := byteframe.NewByteFrame()
bf.WriteUint32(0)
// Format:
// struct Log
// uint32 Timestamp
// []byte Name (len 18)
// uint8 Unk
// uint8 Unk
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}
func handleMsgMhfCreateMercenary(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfCreateMercenary)
var nextID uint32
if err := s.server.db.QueryRow("SELECT nextval('rasta_id_seq')").Scan(&nextID); err != nil {
s.logger.Error("Failed to get next rasta ID", zap.Error(err))
doAckSimpleFail(s, pkt.AckHandle, nil)
return
}
if _, err := s.server.db.Exec("UPDATE characters SET rasta_id=$1 WHERE id=$2", nextID, s.charID); err != nil {
s.logger.Error("Failed to set rasta ID", zap.Error(err))
doAckSimpleFail(s, pkt.AckHandle, nil)
return
}
bf := byteframe.NewByteFrame()
bf.WriteUint32(nextID)
doAckSimpleSucceed(s, pkt.AckHandle, bf.Data())
}
func handleMsgMhfSaveMercenary(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfSaveMercenary)
if len(pkt.MercData) > 65536 {
s.logger.Warn("Mercenary payload too large", zap.Int("len", len(pkt.MercData)))
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
}
dumpSaveData(s, pkt.MercData, "mercenary")
if len(pkt.MercData) >= 4 {
temp := byteframe.NewByteFrameFromBytes(pkt.MercData)
if _, err := s.server.db.Exec("UPDATE characters SET savemercenary=$1, rasta_id=$2 WHERE id=$3", pkt.MercData, temp.ReadUint32(), s.charID); err != nil {
s.logger.Error("Failed to save mercenary data", zap.Error(err))
}
}
if _, err := s.server.db.Exec("UPDATE characters SET gcp=$1, pact_id=$2 WHERE id=$3", pkt.GCP, pkt.PactMercID, s.charID); err != nil {
s.logger.Error("Failed to update GCP and pact ID", zap.Error(err))
}
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
}
func handleMsgMhfReadMercenaryW(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfReadMercenaryW)
bf := byteframe.NewByteFrame()
var pactID, cid uint32
var name string
_ = s.server.db.QueryRow("SELECT pact_id FROM characters WHERE id=$1", s.charID).Scan(&pactID)
if pactID > 0 {
_ = s.server.db.QueryRow("SELECT name, id FROM characters WHERE rasta_id = $1", pactID).Scan(&name, &cid)
bf.WriteUint8(1) // numLends
bf.WriteUint32(pactID)
bf.WriteUint32(cid)
bf.WriteBool(true) // Escort enabled
bf.WriteUint32(uint32(TimeAdjusted().Unix()))
bf.WriteUint32(uint32(TimeAdjusted().Add(time.Hour * 24 * 7).Unix()))
bf.WriteBytes(stringsupport.PaddedString(name, 18, true))
} else {
bf.WriteUint8(0)
}
if pkt.Op != 2 && pkt.Op != 5 {
var loans uint8
temp := byteframe.NewByteFrame()
rows, err := s.server.db.Query("SELECT name, id, pact_id FROM characters WHERE pact_id=(SELECT rasta_id FROM characters WHERE id=$1)", s.charID)
if err != nil {
s.logger.Error("Failed to query mercenary loans", zap.Error(err))
} else {
defer func() { _ = rows.Close() }()
for rows.Next() {
if err := rows.Scan(&name, &cid, &pactID); err != nil {
continue
}
loans++
temp.WriteUint32(pactID)
temp.WriteUint32(cid)
temp.WriteUint32(uint32(TimeAdjusted().Unix()))
temp.WriteUint32(uint32(TimeAdjusted().Add(time.Hour * 24 * 7).Unix()))
temp.WriteBytes(stringsupport.PaddedString(name, 18, true))
}
}
bf.WriteUint8(loans)
bf.WriteBytes(temp.Data())
if pkt.Op != 1 && pkt.Op != 4 {
var data []byte
var gcp uint32
_ = s.server.db.QueryRow("SELECT savemercenary FROM characters WHERE id=$1", s.charID).Scan(&data)
_ = s.server.db.QueryRow("SELECT COALESCE(gcp, 0) FROM characters WHERE id=$1", s.charID).Scan(&gcp)
if len(data) == 0 {
bf.WriteBool(false)
} else {
bf.WriteBool(true)
bf.WriteBytes(data)
}
bf.WriteUint32(gcp)
}
}
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}
func handleMsgMhfReadMercenaryM(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfReadMercenaryM)
var data []byte
_ = s.server.db.QueryRow("SELECT savemercenary FROM characters WHERE id = $1", pkt.CharID).Scan(&data)
resp := byteframe.NewByteFrame()
if len(data) == 0 {
resp.WriteBool(false)
} else {
resp.WriteBytes(data)
}
doAckBufSucceed(s, pkt.AckHandle, resp.Data())
}
func handleMsgMhfContractMercenary(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfContractMercenary)
switch pkt.Op {
case 0: // Form loan
if _, err := s.server.db.Exec("UPDATE characters SET pact_id=$1 WHERE id=$2", pkt.PactMercID, pkt.CID); err != nil {
s.logger.Error("Failed to form mercenary loan", zap.Error(err))
}
case 1: // Cancel lend
if _, err := s.server.db.Exec("UPDATE characters SET pact_id=0 WHERE id=$1", s.charID); err != nil {
s.logger.Error("Failed to cancel mercenary lend", zap.Error(err))
}
case 2: // Cancel loan
if _, err := s.server.db.Exec("UPDATE characters SET pact_id=0 WHERE id=$1", pkt.CID); err != nil {
s.logger.Error("Failed to cancel mercenary loan", zap.Error(err))
}
}
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}
func handleMsgMhfLoadOtomoAirou(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfLoadOtomoAirou)
loadCharacterData(s, pkt.AckHandle, "otomoairou", make([]byte, 10))
}
func handleMsgMhfSaveOtomoAirou(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfSaveOtomoAirou)
if len(pkt.RawDataPayload) < 2 {
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
}
dumpSaveData(s, pkt.RawDataPayload, "otomoairou")
decomp, err := nullcomp.Decompress(pkt.RawDataPayload[1:])
if err != nil {
s.logger.Error("Failed to decompress airou", zap.Error(err))
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
}
bf := byteframe.NewByteFrameFromBytes(decomp)
save := byteframe.NewByteFrame()
var catsExist uint8
save.WriteUint8(0)
cats := bf.ReadUint8()
for i := 0; i < int(cats); i++ {
dataLen := bf.ReadUint32()
catID := bf.ReadUint32()
if catID == 0 {
if err := s.server.db.QueryRow("SELECT nextval('airou_id_seq')").Scan(&catID); err != nil {
s.logger.Error("Failed to get next airou ID", zap.Error(err))
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
}
}
exists := bf.ReadBool()
data := bf.ReadBytes(uint(dataLen) - 5)
if exists {
catsExist++
save.WriteUint32(dataLen)
save.WriteUint32(catID)
save.WriteBool(exists)
save.WriteBytes(data)
}
}
save.WriteBytes(bf.DataFromCurrent())
_, _ = save.Seek(0, 0)
save.WriteUint8(catsExist)
comp, err := nullcomp.Compress(save.Data())
if err != nil {
s.logger.Error("Failed to compress airou", zap.Error(err))
} else {
comp = append([]byte{0x01}, comp...)
if _, err := s.server.db.Exec("UPDATE characters SET otomoairou=$1 WHERE id=$2", comp, s.charID); err != nil {
s.logger.Error("Failed to save otomoairou", zap.Error(err))
}
}
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}
func handleMsgMhfEnumerateAiroulist(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfEnumerateAiroulist)
resp := byteframe.NewByteFrame()
airouList := getGuildAirouList(s)
resp.WriteUint16(uint16(len(airouList)))
resp.WriteUint16(uint16(len(airouList)))
for _, cat := range airouList {
resp.WriteUint32(cat.ID)
resp.WriteBytes(cat.Name)
resp.WriteUint32(cat.Experience)
resp.WriteUint8(cat.Personality)
resp.WriteUint8(cat.Class)
resp.WriteUint8(cat.WeaponType)
resp.WriteUint16(cat.WeaponID)
resp.WriteUint32(0) // 32 bit unix timestamp, either time at which the cat stops being fatigued or the time at which it started
}
doAckBufSucceed(s, pkt.AckHandle, resp.Data())
}
// Airou represents Airou (felyne companion) data.
type Airou struct {
ID uint32
Name []byte
Task uint8
Personality uint8
Class uint8
Experience uint32
WeaponType uint8
WeaponID uint16
}
func getGuildAirouList(s *Session) []Airou {
var guildCats []Airou
bannedCats := make(map[uint32]int)
guild, err := GetGuildInfoByCharacterId(s, s.charID)
if err != nil {
return guildCats
}
rows, err := s.server.db.Query(`SELECT cats_used FROM guild_hunts gh
INNER JOIN characters c ON gh.host_id = c.id WHERE c.id=$1
`, s.charID)
if err != nil {
s.logger.Warn("Failed to get recently used airous", zap.Error(err))
return guildCats
}
var csvTemp string
var startTemp time.Time
for rows.Next() {
err = rows.Scan(&csvTemp, &startTemp)
if err != nil {
continue
}
if startTemp.Add(time.Second * time.Duration(s.server.erupeConfig.GameplayOptions.TreasureHuntPartnyaCooldown)).Before(TimeAdjusted()) {
for i, j := range stringsupport.CSVElems(csvTemp) {
bannedCats[uint32(j)] = i
}
}
}
rows, err = s.server.db.Query(`SELECT c.otomoairou FROM characters c
INNER JOIN guild_characters gc ON gc.character_id = c.id
WHERE gc.guild_id = $1 AND c.otomoairou IS NOT NULL
ORDER BY c.id LIMIT 60`, guild.ID)
if err != nil {
s.logger.Warn("Selecting otomoairou based on guild failed", zap.Error(err))
return guildCats
}
for rows.Next() {
var data []byte
err = rows.Scan(&data)
if err != nil || len(data) == 0 {
continue
}
// first byte has cat existence in general, can skip if 0
if data[0] == 1 {
decomp, err := nullcomp.Decompress(data[1:])
if err != nil {
s.logger.Warn("decomp failure", zap.Error(err))
continue
}
bf := byteframe.NewByteFrameFromBytes(decomp)
cats := GetAirouDetails(bf)
for _, cat := range cats {
_, exists := bannedCats[cat.ID]
if cat.Task == 4 && !exists {
guildCats = append(guildCats, cat)
}
}
}
}
return guildCats
}
// GetAirouDetails parses Airou data from a ByteFrame.
func GetAirouDetails(bf *byteframe.ByteFrame) []Airou {
catCount := bf.ReadUint8()
cats := make([]Airou, catCount)
for x := 0; x < int(catCount); x++ {
var catDef Airou
// cat sometimes has additional bytes for whatever reason, gift items? timestamp?
// until actual variance is known we can just seek to end based on start
catDefLen := bf.ReadUint32()
catStart, _ := bf.Seek(0, io.SeekCurrent)
catDef.ID = bf.ReadUint32()
_, _ = bf.Seek(1, io.SeekCurrent) // unknown value, probably a bool
catDef.Name = bf.ReadBytes(18) // always 18 len, reads first null terminated string out of section and discards rest
catDef.Task = bf.ReadUint8()
_, _ = bf.Seek(16, io.SeekCurrent) // appearance data and what is seemingly null bytes
catDef.Personality = bf.ReadUint8()
catDef.Class = bf.ReadUint8()
_, _ = bf.Seek(5, io.SeekCurrent) // affection and colour sliders
catDef.Experience = bf.ReadUint32() // raw cat rank points, doesn't have a rank
_, _ = bf.Seek(1, io.SeekCurrent) // bool for weapon being equipped
catDef.WeaponType = bf.ReadUint8() // weapon type, presumably always 6 for melee?
catDef.WeaponID = bf.ReadUint16() // weapon id
_, _ = bf.Seek(catStart+int64(catDefLen), io.SeekStart)
cats[x] = catDef
}
return cats
}