mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
SaveColumn and SaveMercenary now check RowsAffected() and return a wrapped ErrCharacterNotFound when 0 rows are updated, preventing silent data loss when a character ID is missing or mismatched. AdjustInt already detects this via its RETURNING scan — no change. Daily mission packet structs (Get/SetDailyMission*) now parse the AckHandle instead of returning NOT IMPLEMENTED, letting handlers send empty-list success ACKs and avoiding client softlocks. Also adds tests for dashboard stats endpoint and for five guild repo methods (SetAllianceRecruiting, RolloverDailyRP, AddWeeklyBonusUsers, InsertKillLog, ClearTreasureHunt) that had no coverage.
320 lines
9.4 KiB
Go
320 lines
9.4 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"erupe-ce/common/byteframe"
|
|
cfg "erupe-ce/config"
|
|
"erupe-ce/network/mhfpacket"
|
|
"math/bits"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func handleMsgMhfGetEtcPoints(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetEtcPoints)
|
|
|
|
dailyTime, err := s.server.charRepo.ReadTime(s.charID, "daily_time", time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC))
|
|
if err != nil {
|
|
s.logger.Warn("Failed to read daily_time for etc points", zap.Error(err))
|
|
}
|
|
if TimeAdjusted().After(dailyTime) {
|
|
if err := s.server.charRepo.ResetDailyQuests(s.charID); err != nil {
|
|
s.logger.Error("Failed to reset daily quests", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
bonusQuests, dailyQuests, promoPoints, err := s.server.charRepo.ReadEtcPoints(s.charID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get etc points", zap.Error(err))
|
|
}
|
|
resp := byteframe.NewByteFrame()
|
|
resp.WriteUint8(3) // Maybe a count of uint32(s)?
|
|
resp.WriteUint32(bonusQuests)
|
|
resp.WriteUint32(dailyQuests)
|
|
resp.WriteUint32(promoPoints)
|
|
doAckBufSucceed(s, pkt.AckHandle, resp.Data())
|
|
}
|
|
|
|
func handleMsgMhfUpdateEtcPoint(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfUpdateEtcPoint)
|
|
|
|
var column string
|
|
switch pkt.PointType {
|
|
case 0:
|
|
column = "bonus_quests"
|
|
case 1:
|
|
column = "daily_quests"
|
|
case 2:
|
|
column = "promo_points"
|
|
default:
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
return
|
|
}
|
|
|
|
value, err := readCharacterInt(s, column)
|
|
if err == nil {
|
|
newVal := max(value+int(pkt.Delta), 0)
|
|
if err := s.server.charRepo.SaveInt(s.charID, column, newVal); err != nil {
|
|
s.logger.Error("Failed to update etc point", zap.Error(err))
|
|
}
|
|
}
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
}
|
|
|
|
func handleMsgMhfUnreserveSrg(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfUnreserveSrg)
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
}
|
|
|
|
func handleMsgMhfKickExportForce(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
|
|
|
|
func handleMsgMhfGetEarthStatus(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetEarthStatus)
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(uint32(TimeWeekStart().Unix())) // Start
|
|
bf.WriteUint32(uint32(TimeWeekNext().Unix())) // End
|
|
bf.WriteInt32(s.server.erupeConfig.EarthStatus)
|
|
bf.WriteInt32(s.server.erupeConfig.EarthID)
|
|
for i, m := range s.server.erupeConfig.EarthMonsters {
|
|
if s.server.erupeConfig.RealClientMode <= cfg.G9 {
|
|
if i == 3 {
|
|
break
|
|
}
|
|
}
|
|
if i == 4 {
|
|
break
|
|
}
|
|
bf.WriteInt32(m)
|
|
}
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
func handleMsgMhfRegistSpabiTime(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
|
|
|
|
func handleMsgMhfGetEarthValue(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetEarthValue)
|
|
type EarthValues struct {
|
|
Value []uint32
|
|
}
|
|
|
|
var earthValues []EarthValues
|
|
switch pkt.ReqType {
|
|
case 1:
|
|
earthValues = []EarthValues{
|
|
{[]uint32{1, 312, 0, 0, 0, 0}},
|
|
{[]uint32{2, 99, 0, 0, 0, 0}},
|
|
}
|
|
case 2:
|
|
earthValues = []EarthValues{
|
|
{[]uint32{1, 5771, 0, 0, 0, 0}},
|
|
{[]uint32{2, 1847, 0, 0, 0, 0}},
|
|
}
|
|
case 3:
|
|
earthValues = []EarthValues{
|
|
{[]uint32{1001, 36, 0, 0, 0, 0}},
|
|
{[]uint32{9001, 3, 0, 0, 0, 0}},
|
|
{[]uint32{9002, 10, 300, 0, 0, 0}},
|
|
}
|
|
}
|
|
|
|
var data []*byteframe.ByteFrame
|
|
for _, i := range earthValues {
|
|
bf := byteframe.NewByteFrame()
|
|
for _, j := range i.Value {
|
|
bf.WriteUint32(j)
|
|
}
|
|
data = append(data, bf)
|
|
}
|
|
doAckEarthSucceed(s, pkt.AckHandle, data)
|
|
}
|
|
|
|
func handleMsgMhfDebugPostValue(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
|
|
|
|
func handleMsgMhfGetRandFromTable(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetRandFromTable)
|
|
bf := byteframe.NewByteFrame()
|
|
for i := uint16(0); i < pkt.Results; i++ {
|
|
bf.WriteUint32(0)
|
|
}
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
func handleMsgMhfGetSenyuDailyCount(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetSenyuDailyCount)
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint16(0)
|
|
bf.WriteUint16(0)
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
// handleMsgMhfGetDailyMissionMaster returns an empty daily mission master list.
|
|
// The full response format is not yet reverse-engineered; count=0 is safe.
|
|
func handleMsgMhfGetDailyMissionMaster(s *Session, p mhfpacket.MHFPacket) {
|
|
if p == nil {
|
|
return
|
|
}
|
|
pkt := p.(*mhfpacket.MsgMhfGetDailyMissionMaster)
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(0) // entry count = 0
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
// handleMsgMhfGetDailyMissionPersonal returns an empty personal daily mission progress list.
|
|
// The full response format is not yet reverse-engineered; count=0 is safe.
|
|
func handleMsgMhfGetDailyMissionPersonal(s *Session, p mhfpacket.MHFPacket) {
|
|
if p == nil {
|
|
return
|
|
}
|
|
pkt := p.(*mhfpacket.MsgMhfGetDailyMissionPersonal)
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(0) // entry count = 0
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
// handleMsgMhfSetDailyMissionPersonal acknowledges a personal daily mission progress write.
|
|
// The full request/response format is not yet reverse-engineered.
|
|
func handleMsgMhfSetDailyMissionPersonal(s *Session, p mhfpacket.MHFPacket) {
|
|
if p == nil {
|
|
return
|
|
}
|
|
pkt := p.(*mhfpacket.MsgMhfSetDailyMissionPersonal)
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
}
|
|
|
|
// Equip skin history buffer sizes per game version
|
|
const (
|
|
skinHistSizeZZ = 3200 // ZZ and newer
|
|
skinHistSizeZ2 = 2560 // Z2 and older
|
|
skinHistSizeZ1 = 1280 // Z1 and older
|
|
)
|
|
|
|
func equipSkinHistSize(mode cfg.Mode) int {
|
|
size := skinHistSizeZZ
|
|
if mode <= cfg.Z2 {
|
|
size = skinHistSizeZ2
|
|
}
|
|
if mode <= cfg.Z1 {
|
|
size = skinHistSizeZ1
|
|
}
|
|
return size
|
|
}
|
|
|
|
func handleMsgMhfGetEquipSkinHist(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetEquipSkinHist)
|
|
size := equipSkinHistSize(s.server.erupeConfig.RealClientMode)
|
|
loadCharacterData(s, pkt.AckHandle, "skin_hist", make([]byte, size))
|
|
}
|
|
|
|
func handleMsgMhfUpdateEquipSkinHist(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfUpdateEquipSkinHist)
|
|
size := equipSkinHistSize(s.server.erupeConfig.RealClientMode)
|
|
data, err := s.server.charRepo.LoadColumnWithDefault(s.charID, "skin_hist", make([]byte, size))
|
|
if err != nil {
|
|
s.logger.Error("Failed to get skin_hist", zap.Error(err))
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
return
|
|
}
|
|
|
|
if pkt.ArmourID < 10000 || pkt.MogType > 4 {
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
return
|
|
}
|
|
bit := int(pkt.ArmourID) - 10000
|
|
sectionSize := size / 5
|
|
if bit/8 >= sectionSize {
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
return
|
|
}
|
|
startByte := sectionSize * int(pkt.MogType)
|
|
byteInd := bit / 8
|
|
bitInByte := bit % 8
|
|
data[startByte+byteInd] |= bits.Reverse8(1 << uint(bitInByte))
|
|
dumpSaveData(s, data, "skinhist")
|
|
if err := s.server.charRepo.SaveColumn(s.charID, "skin_hist", data); err != nil {
|
|
s.logger.Error("Failed to update skin history", zap.Error(err))
|
|
}
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
}
|
|
|
|
func handleMsgMhfGetUdShopCoin(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetUdShopCoin)
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(0)
|
|
doAckSimpleSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
func handleMsgMhfUseUdShopCoin(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
|
|
|
|
func handleMsgMhfGetEnhancedMinidata(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetEnhancedMinidata)
|
|
|
|
data, ok := s.server.minidata.Get(pkt.CharID)
|
|
if !ok {
|
|
data = make([]byte, 1)
|
|
}
|
|
doAckBufSucceed(s, pkt.AckHandle, data)
|
|
}
|
|
|
|
func handleMsgMhfSetEnhancedMinidata(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfSetEnhancedMinidata)
|
|
dumpSaveData(s, pkt.RawDataPayload, "minidata")
|
|
|
|
s.server.minidata.Set(s.charID, pkt.RawDataPayload)
|
|
|
|
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
|
|
}
|
|
|
|
func handleMsgMhfGetLobbyCrowd(s *Session, p mhfpacket.MHFPacket) {
|
|
// this requests a specific server's population but seems to have been
|
|
// broken at some point on live as every example response across multiple
|
|
// servers sends back the exact same information?
|
|
// It can be worried about later if we ever get to the point where there are
|
|
// full servers to actually need to migrate people from and empty ones to
|
|
pkt := p.(*mhfpacket.MsgMhfGetLobbyCrowd)
|
|
const lobbyCrowdResponseSize = 0x320
|
|
doAckBufSucceed(s, pkt.AckHandle, make([]byte, lobbyCrowdResponseSize))
|
|
}
|
|
|
|
// TrendWeapon represents trending weapon usage data.
|
|
type TrendWeapon struct {
|
|
WeaponType uint8
|
|
WeaponID uint16
|
|
}
|
|
|
|
func handleMsgMhfGetTrendWeapon(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfGetTrendWeapon)
|
|
trendWeapons := [14][3]TrendWeapon{}
|
|
for i := uint8(0); i < 14; i++ {
|
|
ids, err := s.server.miscRepo.GetTrendWeapons(i)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for j, id := range ids {
|
|
trendWeapons[i][j].WeaponType = i
|
|
trendWeapons[i][j].WeaponID = id
|
|
}
|
|
}
|
|
|
|
x := uint8(0)
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint8(0)
|
|
for _, weaponType := range trendWeapons {
|
|
for _, weapon := range weaponType {
|
|
bf.WriteUint8(weapon.WeaponType)
|
|
bf.WriteUint16(weapon.WeaponID)
|
|
x++
|
|
}
|
|
}
|
|
_, _ = bf.Seek(0, 0)
|
|
bf.WriteUint8(x)
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
func handleMsgMhfUpdateUseTrendWeaponLog(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfUpdateUseTrendWeaponLog)
|
|
if err := s.server.miscRepo.UpsertTrendWeapon(pkt.WeaponID, pkt.WeaponType); err != nil {
|
|
s.logger.Error("Failed to update trend weapon log", zap.Error(err))
|
|
}
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
}
|