mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
138 bare db.Exec calls across 22 handler files silently dropped write errors. Each is now wrapped with error check and zap logging. 4 QueryRow sites that legitimately return sql.ErrNoRows during normal operation (new player mezfes, festa rankings, empty guild item box) now filter it out to reduce log noise.
475 lines
15 KiB
Go
475 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)
|
|
var data []byte
|
|
err := s.server.db.QueryRow("SELECT partner FROM characters WHERE id = $1", s.charID).Scan(&data)
|
|
if len(data) == 0 {
|
|
s.logger.Error("Failed to load partner", zap.Error(err))
|
|
data = make([]byte, 9)
|
|
}
|
|
doAckBufSucceed(s, pkt.AckHandle, data)
|
|
}
|
|
|
|
func handleMsgMhfSavePartner(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfSavePartner)
|
|
dumpSaveData(s, pkt.RawDataPayload, "partner")
|
|
_, err := s.server.db.Exec("UPDATE characters SET partner=$1 WHERE id=$2", pkt.RawDataPayload, s.charID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to save partner", zap.Error(err))
|
|
}
|
|
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
|
|
}
|
|
|
|
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
|
|
}
|
|
var data []byte
|
|
err := s.server.db.QueryRow("SELECT hunternavi FROM characters WHERE id = $1", s.charID).Scan(&data)
|
|
if len(data) == 0 {
|
|
s.logger.Error("Failed to load hunternavi", zap.Error(err))
|
|
data = make([]byte, naviLength)
|
|
}
|
|
doAckBufSucceed(s, pkt.AckHandle, data)
|
|
}
|
|
|
|
func handleMsgMhfSaveHunterNavi(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfSaveHunterNavi)
|
|
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.Unk0 == 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)
|
|
bf := byteframe.NewByteFrame()
|
|
var nextID uint32
|
|
_ = s.server.db.QueryRow("SELECT nextval('rasta_id_seq')").Scan(&nextID)
|
|
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))
|
|
}
|
|
bf.WriteUint32(nextID)
|
|
doAckSimpleSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
func handleMsgMhfSaveMercenary(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfSaveMercenary)
|
|
dumpSaveData(s, pkt.MercData, "mercenary")
|
|
if len(pkt.MercData) > 0 {
|
|
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 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)
|
|
var data []byte
|
|
err := s.server.db.QueryRow("SELECT otomoairou FROM characters WHERE id = $1", s.charID).Scan(&data)
|
|
if len(data) == 0 {
|
|
s.logger.Error("Failed to load otomoairou", zap.Error(err))
|
|
data = make([]byte, 10)
|
|
}
|
|
doAckBufSucceed(s, pkt.AckHandle, data)
|
|
}
|
|
|
|
func handleMsgMhfSaveOtomoAirou(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfSaveOtomoAirou)
|
|
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 {
|
|
_ = s.server.db.QueryRow("SELECT nextval('airou_id_seq')").Scan(&catID)
|
|
}
|
|
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())
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|