refactor(channelserver): migrate inline queries to helpers and define named constants

Migrate 6 character data handlers to use the existing loadCharacterData
and saveCharacterData helpers, eliminating duplicate inline SQL:
- LoadFavoriteQuest, SaveFavoriteQuest, LoadDecoMyset, LoadMezfesData,
  LoadHunterNavi, GetEquipSkinHist

Define named constants replacing magic numbers across handlers:
- Achievement trophy tiers, broadcast/message types, diva phase
  durations, RP accrual rates, kill log layout, semaphore bases,
  quest stage/loading screen IDs

Update anti-patterns doc with accurate line counts, evidence-based
softlock analysis, and revised refactoring priorities.
This commit is contained in:
Houmgaor
2026-02-20 19:46:57 +01:00
parent 24ccc167fe
commit bf983966a0
12 changed files with 224 additions and 121 deletions

View File

@@ -0,0 +1,62 @@
package channelserver
// Raviente quest type codes
const (
QuestTypeSpecialTool = uint8(9)
QuestTypeRegularRaviente = uint8(16)
QuestTypeViolentRaviente = uint8(22)
QuestTypeBerserkRaviente = uint8(40)
QuestTypeExtremeRaviente = uint8(50)
QuestTypeSmallBerserkRavi = uint8(51)
)
// Event quest binary frame offsets
const (
questFrameTimeFlagOffset = 25
questFrameVariant3Offset = 175
)
// Quest body lengths per game version
const (
questBodyLenS6 = 160
questBodyLenF5 = 168
questBodyLenG101 = 192
questBodyLenZ1 = 224
questBodyLenZZ = 320
)
// BackportQuest constants
const (
questRewardTableBase = uint32(96)
questStringPointerOff = 40
questStringTablePadding = 32
questStringCount = 8
)
// BackportQuest fill lengths per version
const (
questBackportFillS6 = uint32(44)
questBackportFillF5 = uint32(52)
questBackportFillG101 = uint32(76)
questBackportFillZZ = uint32(108)
)
// Tune value count limits per game version
const (
tuneLimitG1 = 256
tuneLimitG3 = 283
tuneLimitGG = 315
tuneLimitG61 = 332
tuneLimitG7 = 339
tuneLimitG81 = 396
tuneLimitG91 = 694
tuneLimitG101 = 704
tuneLimitZ2 = 750
tuneLimitZZ = 770
)
// Event quest data size bounds
const (
questDataMaxLen = 896
questDataMinLen = 352
)

View File

@@ -9,6 +9,13 @@ import (
"go.uber.org/zap"
)
// Achievement trophy tier thresholds (bitfield values)
const (
AchievementTrophyBronze = uint8(0x40)
AchievementTrophySilver = uint8(0x60)
AchievementTrophyGold = uint8(0x7F)
)
var achievementCurves = [][]int32{
// 0: HR weapon use, Class use, Tore dailies
{5, 15, 30, 50, 100, 150, 200, 300},
@@ -61,10 +68,10 @@ func GetAchData(id uint8, score int32) Achievement {
ach.NextValue = 15
case 6:
ach.NextValue = 15
ach.Trophy = 0x40
ach.Trophy = AchievementTrophyBronze
case 7:
ach.NextValue = 20
ach.Trophy = 0x60
ach.Trophy = AchievementTrophySilver
}
return ach
} else {
@@ -83,7 +90,7 @@ func GetAchData(id uint8, score int32) Achievement {
}
}
ach.Required = uint32(curve[7])
ach.Trophy = 0x7F
ach.Trophy = AchievementTrophyGold
ach.Progress = ach.Required
return ach
}

View File

@@ -34,7 +34,7 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysCastBinary)
tmp := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload)
if pkt.BroadcastType == 0x03 && pkt.MessageType == 0x03 && len(pkt.RawDataPayload) == 0x10 {
if pkt.BroadcastType == BroadcastTypeStage && pkt.MessageType == BinaryMessageTypeData && len(pkt.RawDataPayload) == 0x10 {
if tmp.ReadUint16() == 0x0002 && tmp.ReadUint8() == 0x18 {
var timer bool
if err := s.server.db.QueryRow(`SELECT COALESCE(timer, false) FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, s.charID).Scan(&timer); err != nil {
@@ -50,7 +50,7 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) {
}
if s.server.erupeConfig.DebugOptions.QuestTools {
if pkt.BroadcastType == 0x03 && pkt.MessageType == 0x02 && len(pkt.RawDataPayload) > 32 {
if pkt.BroadcastType == BroadcastTypeStage && pkt.MessageType == BinaryMessageTypeQuest && len(pkt.RawDataPayload) > 32 {
// This is only correct most of the time
tmp.ReadBytes(20)
tmp.SetLE()
@@ -131,7 +131,7 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) {
s.stage.BroadcastMHF(resp, s)
}
case BroadcastTypeServer:
if pkt.MessageType == 1 {
if pkt.MessageType == BinaryMessageTypeChat {
raviSema := s.server.getRaviSemaphore()
if raviSema != nil {
raviSema.BroadcastMHF(resp, s)

View File

@@ -11,6 +11,14 @@ import (
"go.uber.org/zap"
)
// Diva Defense event duration constants (all values in seconds)
const (
divaPhaseDuration = 601200 // 6d 23h = first song phase
divaInterlude = 3900 // 65 min = gap between phases
divaWeekDuration = 604800 // 7 days = subsequent phase length
divaTotalLifespan = 2977200 // ~34.5 days = full event window
)
func cleanupDiva(s *Session) {
if _, err := s.server.db.Exec("DELETE FROM events WHERE event_type='diva'"); err != nil {
s.logger.Error("Failed to delete diva events", zap.Error(err))
@@ -25,29 +33,29 @@ func generateDivaTimestamps(s *Session, start uint32, debug bool) []uint32 {
switch start {
case 1:
timestamps[0] = midnight
timestamps[1] = timestamps[0] + 601200
timestamps[2] = timestamps[1] + 3900
timestamps[3] = timestamps[1] + 604800
timestamps[4] = timestamps[3] + 3900
timestamps[5] = timestamps[3] + 604800
timestamps[1] = timestamps[0] + divaPhaseDuration
timestamps[2] = timestamps[1] + divaInterlude
timestamps[3] = timestamps[1] + divaWeekDuration
timestamps[4] = timestamps[3] + divaInterlude
timestamps[5] = timestamps[3] + divaWeekDuration
case 2:
timestamps[0] = midnight - 605100
timestamps[1] = midnight - 3900
timestamps[0] = midnight - (divaPhaseDuration + divaInterlude)
timestamps[1] = midnight - divaInterlude
timestamps[2] = midnight
timestamps[3] = timestamps[1] + 604800
timestamps[4] = timestamps[3] + 3900
timestamps[5] = timestamps[3] + 604800
timestamps[3] = timestamps[1] + divaWeekDuration
timestamps[4] = timestamps[3] + divaInterlude
timestamps[5] = timestamps[3] + divaWeekDuration
case 3:
timestamps[0] = midnight - 1213800
timestamps[1] = midnight - 608700
timestamps[2] = midnight - 604800
timestamps[3] = midnight - 3900
timestamps[0] = midnight - (divaPhaseDuration + divaInterlude + divaWeekDuration + divaInterlude)
timestamps[1] = midnight - (divaWeekDuration + divaInterlude)
timestamps[2] = midnight - divaWeekDuration
timestamps[3] = midnight - divaInterlude
timestamps[4] = midnight
timestamps[5] = timestamps[3] + 604800
timestamps[5] = timestamps[3] + divaWeekDuration
}
return timestamps
}
if start == 0 || TimeAdjusted().Unix() > int64(start)+2977200 {
if start == 0 || TimeAdjusted().Unix() > int64(start)+divaTotalLifespan {
cleanupDiva(s)
// Generate a new diva defense, starting midnight tomorrow
start = uint32(midnight.Add(24 * time.Hour).Unix())
@@ -56,11 +64,11 @@ func generateDivaTimestamps(s *Session, start uint32, debug bool) []uint32 {
}
}
timestamps[0] = start
timestamps[1] = timestamps[0] + 601200
timestamps[2] = timestamps[1] + 3900
timestamps[3] = timestamps[1] + 604800
timestamps[4] = timestamps[3] + 3900
timestamps[5] = timestamps[3] + 604800
timestamps[1] = timestamps[0] + divaPhaseDuration
timestamps[2] = timestamps[1] + divaInterlude
timestamps[3] = timestamps[1] + divaWeekDuration
timestamps[4] = timestamps[3] + divaInterlude
timestamps[5] = timestamps[3] + divaWeekDuration
return timestamps
}

View File

@@ -22,21 +22,8 @@ func handleMsgMhfSaveMezfesData(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfLoadMezfesData(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfLoadMezfesData)
var data []byte
if err := s.server.db.QueryRow(`SELECT mezfes FROM characters WHERE id=$1`, s.charID).Scan(&data); err != nil && !errors.Is(err, sql.ErrNoRows) {
s.logger.Error("Failed to load mezfes data", zap.Error(err))
}
bf := byteframe.NewByteFrame()
if len(data) > 0 {
bf.WriteBytes(data)
} else {
bf.WriteUint32(0)
bf.WriteUint8(2)
bf.WriteUint32(0)
bf.WriteUint32(0)
bf.WriteUint32(0)
}
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
loadCharacterData(s, pkt.AckHandle, "mezfes",
[]byte{0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
}
func handleMsgMhfEnumerateRanking(s *Session, p mhfpacket.MHFPacket) {

View File

@@ -271,18 +271,11 @@ func handleMsgMhfUpdateMyhouseInfo(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfLoadDecoMyset(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfLoadDecoMyset)
var data []byte
err := s.server.db.QueryRow("SELECT decomyset FROM characters WHERE id = $1", s.charID).Scan(&data)
if err != nil {
s.logger.Error("Failed to load decomyset", zap.Error(err))
defaultData := []byte{0x01, 0x00}
if s.server.erupeConfig.RealClientMode < _config.G10 {
defaultData = []byte{0x00, 0x00}
}
if len(data) == 0 {
data = []byte{0x01, 0x00}
if s.server.erupeConfig.RealClientMode < _config.G10 {
data = []byte{0x00, 0x00}
}
}
doAckBufSucceed(s, pkt.AckHandle, data)
loadCharacterData(s, pkt.AckHandle, "decomyset", defaultData)
}
func handleMsgMhfSaveDecoMyset(s *Session, p mhfpacket.MHFPacket) {

View File

@@ -47,13 +47,7 @@ func handleMsgMhfLoadHunterNavi(s *Session, p mhfpacket.MHFPacket) {
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)
loadCharacterData(s, pkt.AckHandle, "hunternavi", make([]byte, naviLength))
}
func handleMsgMhfSaveHunterNavi(s *Session, p mhfpacket.MHFPacket) {

View File

@@ -170,13 +170,7 @@ func equipSkinHistSize(mode _config.Mode) int {
func handleMsgMhfGetEquipSkinHist(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetEquipSkinHist)
size := equipSkinHistSize(s.server.erupeConfig.RealClientMode)
var data []byte
err := s.server.db.QueryRow("SELECT COALESCE(skin_hist::bytea, $2::bytea) FROM characters WHERE id = $1", s.charID, make([]byte, size)).Scan(&data)
if err != nil {
s.logger.Error("Failed to load skin_hist", zap.Error(err))
data = make([]byte, size)
}
doAckBufSucceed(s, pkt.AckHandle, data)
loadCharacterData(s, pkt.AckHandle, "skin_hist", make([]byte, size))
}
func handleMsgMhfUpdateEquipSkinHist(s *Session, p mhfpacket.MHFPacket) {

View File

@@ -50,7 +50,7 @@ func equal(a, b []byte) bool {
// BackportQuest converts a quest binary to an older format.
func BackportQuest(data []byte, mode _config.Mode) []byte {
wp := binary.LittleEndian.Uint32(data[0:4]) + 96
wp := binary.LittleEndian.Uint32(data[0:4]) + questRewardTableBase
rp := wp + 4
for i := uint32(0); i < 6; i++ {
if i != 0 {
@@ -60,13 +60,13 @@ func BackportQuest(data []byte, mode _config.Mode) []byte {
copy(data[wp:wp+4], data[rp:rp+4])
}
fillLength := uint32(108)
fillLength := questBackportFillZZ
if mode <= _config.S6 {
fillLength = 44
fillLength = questBackportFillS6
} else if mode <= _config.F5 {
fillLength = 52
fillLength = questBackportFillF5
} else if mode <= _config.G101 {
fillLength = 76
fillLength = questBackportFillG101
}
copy(data[wp:wp+fillLength], data[rp:rp+fillLength])
@@ -195,27 +195,13 @@ func seasonConversion(s *Session, questFile string) string {
func handleMsgMhfLoadFavoriteQuest(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfLoadFavoriteQuest)
var data []byte
err := s.server.db.QueryRow("SELECT savefavoritequest FROM characters WHERE id = $1", s.charID).Scan(&data)
if err == nil && len(data) > 0 {
doAckBufSucceed(s, pkt.AckHandle, data)
} else {
doAckBufSucceed(s, pkt.AckHandle, []byte{0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
}
loadCharacterData(s, pkt.AckHandle, "savefavoritequest",
[]byte{0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
}
func handleMsgMhfSaveFavoriteQuest(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfSaveFavoriteQuest)
if len(pkt.Data) > 65536 {
s.logger.Warn("FavoriteQuest payload too large", zap.Int("len", len(pkt.Data)))
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
}
dumpSaveData(s, pkt.Data, "favquest")
if _, err := s.server.db.Exec("UPDATE characters SET savefavoritequest=$1 WHERE id=$2", pkt.Data, s.charID); err != nil {
s.logger.Error("Failed to save favorite quest", zap.Error(err))
}
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
saveCharacterData(s, pkt.AckHandle, "savefavoritequest", pkt.Data, 65536)
}
func loadQuestFile(s *Session, questId int) []byte {

View File

@@ -237,14 +237,14 @@ func logoutPlayer(s *Session) {
timePlayed += sessionTime
if mhfcourse.CourseExists(30, s.courses) {
rpGained = timePlayed / 900
timePlayed = timePlayed % 900
rpGained = timePlayed / rpAccrualCafe
timePlayed = timePlayed % rpAccrualCafe
if _, err := s.server.db.Exec("UPDATE characters SET cafe_time=cafe_time+$1 WHERE id=$2", sessionTime, s.charID); err != nil {
s.logger.Error("Failed to update cafe time", zap.Error(err))
}
} else {
rpGained = timePlayed / 1800
timePlayed = timePlayed % 1800
rpGained = timePlayed / rpAccrualNormal
timePlayed = timePlayed % rpAccrualNormal
}
s.logger.Debug("Session metrics calculated",
@@ -386,13 +386,25 @@ func handleMsgSysIssueLogkey(s *Session, p mhfpacket.MHFPacket) {
doAckBufSucceed(s, pkt.AckHandle, resp.Data())
}
// Kill log binary layout constants
const (
killLogHeaderSize = 32 // bytes before monster kill count array
killLogMonsterCount = 176 // monster table entries
)
// RP accrual rate constants (seconds per RP point)
const (
rpAccrualNormal = 1800 // 30 min per RP without cafe
rpAccrualCafe = 900 // 15 min per RP with cafe course
)
func handleMsgSysRecordLog(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysRecordLog)
if s.server.erupeConfig.RealClientMode == _config.ZZ {
bf := byteframe.NewByteFrameFromBytes(pkt.Data)
_, _ = bf.Seek(32, 0)
_, _ = bf.Seek(killLogHeaderSize, 0)
var val uint8
for i := 0; i < 176; i++ {
for i := 0; i < killLogMonsterCount; i++ {
val = bf.ReadUint8()
if val > 0 && mhfmon.Monsters[i].Large {
if _, err := s.server.db.Exec(`INSERT INTO kill_logs (character_id, monster, quantity, timestamp) VALUES ($1, $2, $3, $4)`, s.charID, i, val, TimeAdjusted()); err != nil {

View File

@@ -327,12 +327,18 @@ func (s *Session) getObjectId() uint32 {
return uint32(s.objectID)<<16 | uint32(s.objectIndex)
}
// Semaphore ID base values
const (
semaphoreBaseDefault = uint32(0x000F0000)
semaphoreBaseAlt = uint32(0x000E0000)
)
// GetSemaphoreID returns the semaphore ID held by the session, varying by semaphore mode.
func (s *Session) GetSemaphoreID() uint32 {
if s.semaphoreMode {
return 0x000E0000 + uint32(s.semaphoreID[1])
return semaphoreBaseAlt + uint32(s.semaphoreID[1])
} else {
return 0x000F0000 + uint32(s.semaphoreID[0])
return semaphoreBaseDefault + uint32(s.semaphoreID[0])
}
}