mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
Move scan loops from handlers into repository methods so that interfaces return typed slices instead of leaking database cursors. This fixes resource leaks (7 of 12 call sites never closed rows) and makes all 12 methods mockable for unit tests. Affected repos: CafeRepo, ShopRepo, EventRepo, RengokuRepo, DivaRepo, ScenarioRepo, MiscRepo, MercenaryRepo. New structs: DivaEvent, MercenaryLoan, GuildHuntCatUsage. EventRepo.GetEventQuests left as-is (requires broader Server refactor).
459 lines
14 KiB
Go
459 lines
14 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"erupe-ce/common/byteframe"
|
|
"erupe-ce/common/stringsupport"
|
|
cfg "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())
|
|
}
|
|
|
|
// Hunter Navi buffer sizes per game version
|
|
const (
|
|
hunterNaviSizeG8 = 552 // G8+ navi buffer size
|
|
hunterNaviSizeG7 = 280 // G7 and older navi buffer size
|
|
)
|
|
|
|
func handleMsgMhfLoadHunterNavi(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfLoadHunterNavi)
|
|
naviLength := hunterNaviSizeG8
|
|
if s.server.erupeConfig.RealClientMode <= cfg.G7 {
|
|
naviLength = hunterNaviSizeG7
|
|
}
|
|
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 := hunterNaviSizeG8
|
|
if s.server.erupeConfig.RealClientMode <= cfg.G7 {
|
|
naviLength = hunterNaviSizeG7
|
|
}
|
|
// Load existing save
|
|
data, err := s.server.charRepo.LoadColumn(s.charID, "hunternavi")
|
|
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.charRepo.SaveColumn(s.charID, "hunternavi", saveOutput)
|
|
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.charRepo.SaveColumn(s.charID, "hunternavi", pkt.RawDataPayload)
|
|
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)
|
|
nextID, err := s.server.mercenaryRepo.NextRastaID()
|
|
if err != nil {
|
|
s.logger.Error("Failed to get next rasta ID", zap.Error(err))
|
|
doAckSimpleFail(s, pkt.AckHandle, nil)
|
|
return
|
|
}
|
|
if err := s.server.charRepo.SaveInt(s.charID, "rasta_id", int(nextID)); 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.charRepo.SaveMercenary(s.charID, pkt.MercData, temp.ReadUint32()); err != nil {
|
|
s.logger.Error("Failed to save mercenary data", zap.Error(err))
|
|
}
|
|
}
|
|
if err := s.server.charRepo.UpdateGCPAndPact(s.charID, pkt.GCP, pkt.PactMercID); 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()
|
|
|
|
pactID, _ := readCharacterInt(s, "pact_id")
|
|
var cid uint32
|
|
var name string
|
|
if pactID > 0 {
|
|
cid, name, _ = s.server.charRepo.FindByRastaID(pactID)
|
|
bf.WriteUint8(1) // numLends
|
|
bf.WriteUint32(uint32(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 {
|
|
loans, err := s.server.mercenaryRepo.GetMercenaryLoans(s.charID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to query mercenary loans", zap.Error(err))
|
|
}
|
|
bf.WriteUint8(uint8(len(loans)))
|
|
for _, loan := range loans {
|
|
bf.WriteUint32(uint32(loan.PactID))
|
|
bf.WriteUint32(loan.CharID)
|
|
bf.WriteUint32(uint32(TimeAdjusted().Unix()))
|
|
bf.WriteUint32(uint32(TimeAdjusted().Add(time.Hour * 24 * 7).Unix()))
|
|
bf.WriteBytes(stringsupport.PaddedString(loan.Name, 18, true))
|
|
}
|
|
|
|
if pkt.Op != 1 && pkt.Op != 4 {
|
|
data, _ := s.server.charRepo.LoadColumn(s.charID, "savemercenary")
|
|
gcp, _ := readCharacterInt(s, "gcp")
|
|
|
|
if len(data) == 0 {
|
|
bf.WriteBool(false)
|
|
} else {
|
|
bf.WriteBool(true)
|
|
bf.WriteBytes(data)
|
|
}
|
|
bf.WriteUint32(uint32(gcp))
|
|
}
|
|
}
|
|
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
func handleMsgMhfReadMercenaryM(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfReadMercenaryM)
|
|
data, _ := s.server.charRepo.LoadColumn(pkt.CharID, "savemercenary")
|
|
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.charRepo.SaveInt(pkt.CID, "pact_id", int(pkt.PactMercID)); err != nil {
|
|
s.logger.Error("Failed to form mercenary loan", zap.Error(err))
|
|
}
|
|
case 1: // Cancel lend
|
|
if err := s.server.charRepo.SaveInt(s.charID, "pact_id", 0); err != nil {
|
|
s.logger.Error("Failed to cancel mercenary lend", zap.Error(err))
|
|
}
|
|
case 2: // Cancel loan
|
|
if err := s.server.charRepo.SaveInt(pkt.CID, "pact_id", 0); 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 {
|
|
catID, err = s.server.mercenaryRepo.NextAirouID()
|
|
if 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.charRepo.SaveColumn(s.charID, "otomoairou", comp); 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 := s.server.guildRepo.GetByCharID(s.charID)
|
|
if err != nil {
|
|
return guildCats
|
|
}
|
|
usages, err := s.server.mercenaryRepo.GetGuildHuntCatsUsed(s.charID)
|
|
if err != nil {
|
|
s.logger.Warn("Failed to get recently used airous", zap.Error(err))
|
|
return guildCats
|
|
}
|
|
|
|
for _, usage := range usages {
|
|
if usage.Start.Add(time.Second * time.Duration(s.server.erupeConfig.GameplayOptions.TreasureHuntPartnyaCooldown)).Before(TimeAdjusted()) {
|
|
for i, j := range stringsupport.CSVElems(usage.CatsUsed) {
|
|
bannedCats[uint32(j)] = i
|
|
}
|
|
}
|
|
}
|
|
|
|
airouData, err := s.server.mercenaryRepo.GetGuildAirou(guild.ID)
|
|
if err != nil {
|
|
s.logger.Warn("Selecting otomoairou based on guild failed", zap.Error(err))
|
|
return guildCats
|
|
}
|
|
|
|
for _, data := range airouData {
|
|
if 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
|
|
}
|