From 458d8c9397d30e235ad6e2bb30c4a669d79439e9 Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Fri, 20 Feb 2026 21:18:40 +0100 Subject: [PATCH] refactor(channelserver): add numeric column helpers and extract protocol constants Add readCharacterInt/adjustCharacterInt helpers for single-column integer operations on the characters table. Eliminates fmt.Sprintf SQL construction in handlers_misc.go and replaces inline queries across cafe, kouryou, and mercenary handlers. Second round of protocol constant extraction: adds constants_time.go (secsPerDay, secsPerWeek), constants_raviente.go (register IDs, semaphore constants), and named constants across 14 handler files replacing raw hex/numeric literals. Updates anti-patterns doc to mark #4 (magic numbers) as substantially fixed. --- docs/anti-patterns.md | 30 +++--------- server/channelserver/constants_raviente.go | 14 ++++++ server/channelserver/constants_time.go | 7 +++ server/channelserver/handlers_cafe.go | 30 +++++------- server/channelserver/handlers_cast_binary.go | 9 +++- server/channelserver/handlers_commands.go | 4 +- server/channelserver/handlers_diva.go | 5 +- server/channelserver/handlers_event.go | 2 +- server/channelserver/handlers_festa.go | 48 +++++++++++-------- .../channelserver/handlers_guild_mission.go | 5 +- server/channelserver/handlers_helpers.go | 16 +++++++ server/channelserver/handlers_kouryou.go | 9 ++-- server/channelserver/handlers_mercenary.go | 27 ++++++----- server/channelserver/handlers_misc.go | 35 +++++++------- server/channelserver/handlers_plate.go | 22 ++++++--- server/channelserver/handlers_register.go | 17 ++++--- server/channelserver/handlers_reward.go | 3 +- server/channelserver/handlers_semaphore.go | 4 +- server/channelserver/handlers_session.go | 6 ++- server/channelserver/sys_channel_server.go | 13 +++-- 20 files changed, 182 insertions(+), 124 deletions(-) create mode 100644 server/channelserver/constants_raviente.go create mode 100644 server/channelserver/constants_time.go diff --git a/docs/anti-patterns.md b/docs/anti-patterns.md index c2804a60c..eafa9c2f9 100644 --- a/docs/anti-patterns.md +++ b/docs/anti-patterns.md @@ -150,32 +150,16 @@ There is no repository layer, no service layer — just handlers. --- -## 4. Magic Numbers Everywhere +## 4. ~~Magic Numbers Everywhere~~ (Substantially Fixed) -Binary protocol code is full of unexplained numeric literals with no named constants or comments: +**Status:** Two rounds of extraction have replaced the highest-impact magic numbers with named constants: -```go -// handlers_cast_binary.go -bf.WriteUint8(0x02) -bf.WriteUint16(0x00) -bf.Seek(4, io.SeekStart) -``` +- **Round 1** (commit `7c444b0`): `constants_quest.go`, `handlers_guild_info.go`, `handlers_quest.go`, `handlers_rengoku.go`, `handlers_session.go`, `model_character.go` +- **Round 2**: `constants_time.go` (shared `secsPerDay`, `secsPerWeek`), `constants_raviente.go` (register IDs, semaphore constants), plus constants in `handlers_register.go`, `handlers_semaphore.go`, `handlers_session.go`, `handlers_festa.go`, `handlers_diva.go`, `handlers_event.go`, `handlers_mercenary.go`, `handlers_misc.go`, `handlers_plate.go`, `handlers_cast_binary.go`, `handlers_commands.go`, `handlers_reward.go`, `handlers_guild_mission.go`, `sys_channel_server.go` -```go -// handlers_data.go -if dataLen > 0x20000 { ... } -``` +**Remaining:** Unknown protocol fields (e.g., `handlers_diva.go:112-115` `0x19, 0x2D, 0x02, 0x02`) are intentionally left as literals until their meaning is understood. Data tables (monster point tables, item IDs) are data, not protocol constants. Standard empty ACK payloads (`make([]byte, 4)`) are idiomatic Go. -```go -// Various handlers -bf.WriteUint32(0x0A218EAD) // What is this? -``` - -Packet field offsets, sizes, flags, and game constants appear as raw numbers throughout. - -**Impact:** New contributors can't understand what these values mean. Protocol documentation exists only in the developer's memory. Bugs from using the wrong constant are hard to catch. - -**Recommendation:** Define named constants in relevant packages (e.g., `const MaxDataChunkSize = 0x20000`, `const CastBinaryTypePosition = 0x02`). +**Impact:** ~~New contributors can't understand what these values mean.~~ Most protocol-meaningful constants now have names and comments. --- @@ -318,7 +302,7 @@ Database operations use raw `database/sql` with PostgreSQL-specific syntax throu | Severity | Anti-patterns | |----------|--------------| | **High** | ~~Missing ACK responses / softlocks (#2)~~ **Fixed**, no architectural layering (#3), tight DB coupling (#13) | -| **Medium** | Magic numbers (#4), ~~inconsistent binary I/O (#5)~~ **Resolved**, Session god object (#6), ~~copy-paste handlers (#8)~~ **Fixed**, raw SQL duplication (#9) | +| **Medium** | ~~Magic numbers (#4)~~ **Fixed**, ~~inconsistent binary I/O (#5)~~ **Resolved**, Session god object (#6), ~~copy-paste handlers (#8)~~ **Fixed**, raw SQL duplication (#9) | | **Low** | God files (#1), ~~`init()` registration (#10)~~ **Fixed**, ~~inconsistent logging (#12)~~ **Fixed**, mutex granularity (#7), ~~panic-based flow (#11)~~ **Fixed** | ### Root Cause diff --git a/server/channelserver/constants_raviente.go b/server/channelserver/constants_raviente.go new file mode 100644 index 000000000..b96660848 --- /dev/null +++ b/server/channelserver/constants_raviente.go @@ -0,0 +1,14 @@ +package channelserver + +// Raviente register type IDs (used in MsgSysLoadRegister / MsgSysNotifyRegister) +const ( + raviRegisterState = uint32(0x40000) + raviRegisterSupport = uint32(0x50000) + raviRegisterGeneral = uint32(0x60000) +) + +// Raviente semaphore constants +const ( + raviSemaphoreStride = 0x10000 // ID spacing between hs_l0* semaphores + raviSemaphoreMax = uint16(127) // max players per Raviente semaphore +) diff --git a/server/channelserver/constants_time.go b/server/channelserver/constants_time.go new file mode 100644 index 000000000..9605328e5 --- /dev/null +++ b/server/channelserver/constants_time.go @@ -0,0 +1,7 @@ +package channelserver + +// Shared time duration constants (seconds) +const ( + secsPerDay = 86400 // 24 hours + secsPerWeek = 604800 // 7 days +) diff --git a/server/channelserver/handlers_cafe.go b/server/channelserver/handlers_cafe.go index 2997c22fa..25fd299d2 100644 --- a/server/channelserver/handlers_cafe.go +++ b/server/channelserver/handlers_cafe.go @@ -14,25 +14,23 @@ import ( func handleMsgMhfAcquireCafeItem(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfAcquireCafeItem) - var netcafePoints uint32 - err := s.server.db.QueryRow("UPDATE characters SET netcafe_points = netcafe_points - $1 WHERE id = $2 RETURNING netcafe_points", pkt.PointCost, s.charID).Scan(&netcafePoints) + netcafePoints, err := adjustCharacterInt(s, "netcafe_points", -int(pkt.PointCost)) if err != nil { - s.logger.Error("Failed to get netcafe points from db", zap.Error(err)) + s.logger.Error("Failed to deduct netcafe points", zap.Error(err)) } resp := byteframe.NewByteFrame() - resp.WriteUint32(netcafePoints) + resp.WriteUint32(uint32(netcafePoints)) doAckSimpleSucceed(s, pkt.AckHandle, resp.Data()) } func handleMsgMhfUpdateCafepoint(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfUpdateCafepoint) - var netcafePoints uint32 - err := s.server.db.QueryRow("SELECT COALESCE(netcafe_points, 0) FROM characters WHERE id = $1", s.charID).Scan(&netcafePoints) + netcafePoints, err := readCharacterInt(s, "netcafe_points") if err != nil { - s.logger.Error("Failed to get netcate points from db", zap.Error(err)) + s.logger.Error("Failed to get netcafe points", zap.Error(err)) } resp := byteframe.NewByteFrame() - resp.WriteUint32(netcafePoints) + resp.WriteUint32(uint32(netcafePoints)) doAckSimpleSucceed(s, pkt.AckHandle, resp.Data()) } @@ -93,17 +91,16 @@ func handleMsgMhfGetCafeDuration(s *Session, p mhfpacket.MHFPacket) { } } - var cafeTime uint32 - err = s.server.db.QueryRow("SELECT cafe_time FROM characters WHERE id = $1", s.charID).Scan(&cafeTime) + cafeTime, err := readCharacterInt(s, "cafe_time") if err != nil { s.logger.Error("Failed to get cafe time", zap.Error(err)) doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) return } if mhfcourse.CourseExists(30, s.courses) { - cafeTime = uint32(TimeAdjusted().Unix()) - uint32(s.sessionStart) + cafeTime + cafeTime = int(TimeAdjusted().Unix()) - int(s.sessionStart) + cafeTime } - bf.WriteUint32(cafeTime) + bf.WriteUint32(uint32(cafeTime)) if s.server.erupeConfig.RealClientMode >= _config.ZZ { bf.WriteUint16(0) ps.Uint16(bf, fmt.Sprintf(s.server.i18n.cafe.reset, int(cafeReset.Month()), cafeReset.Day()), true) @@ -218,16 +215,11 @@ func handleMsgMhfPostCafeDurationBonusReceived(s *Session, p mhfpacket.MHFPacket } func addPointNetcafe(s *Session, p int) error { - var points int - err := s.server.db.QueryRow("SELECT netcafe_points FROM characters WHERE id = $1", s.charID).Scan(&points) + points, err := readCharacterInt(s, "netcafe_points") if err != nil { return err } - if points+p > s.server.erupeConfig.GameplayOptions.MaximumNP { - points = s.server.erupeConfig.GameplayOptions.MaximumNP - } else { - points += p - } + points = min(points+p, s.server.erupeConfig.GameplayOptions.MaximumNP) if _, err := s.server.db.Exec("UPDATE characters SET netcafe_points=$1 WHERE id=$2", points, s.charID); err != nil { s.logger.Error("Failed to update netcafe points", zap.Error(err)) } diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index 013580ab8..85dabe08c 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -34,8 +34,13 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgSysCastBinary) tmp := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload) - if pkt.BroadcastType == BroadcastTypeStage && pkt.MessageType == BinaryMessageTypeData && len(pkt.RawDataPayload) == 0x10 { - if tmp.ReadUint16() == 0x0002 && tmp.ReadUint8() == 0x18 { + const ( + timerPayloadSize = 0x10 // expected payload length for timer packets + timerSubtype = uint16(0x0002) // timer data subtype identifier + timerFlag = uint8(0x18) // timer flag byte + ) + if pkt.BroadcastType == BroadcastTypeStage && pkt.MessageType == BinaryMessageTypeData && len(pkt.RawDataPayload) == timerPayloadSize { + if tmp.ReadUint16() == timerSubtype && tmp.ReadUint8() == timerFlag { var timer bool if err := s.server.db.QueryRow(`SELECT COALESCE(timer, false) FROM users WHERE id=$1`, s.userID).Scan(&timer); err != nil { s.logger.Error("Failed to get timer setting", zap.Error(err)) diff --git a/server/channelserver/handlers_commands.go b/server/channelserver/handlers_commands.go index 5ec5dd3da..718505536 100644 --- a/server/channelserver/handlers_commands.go +++ b/server/channelserver/handlers_commands.go @@ -44,6 +44,8 @@ func sendDisabledCommandMessage(s *Session, cmd _config.Command) { sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.disabled, cmd.Name)) } +const chatFlagServer = 0x80 // marks a message as server-originated + func sendServerChatMessage(s *Session, message string) { // Make the inside of the casted binary bf := byteframe.NewByteFrame() @@ -51,7 +53,7 @@ func sendServerChatMessage(s *Session, message string) { msgBinChat := &binpacket.MsgBinChat{ Unk0: 0, Type: 5, - Flags: 0x80, + Flags: chatFlagServer, Message: message, SenderName: "Erupe", } diff --git a/server/channelserver/handlers_diva.go b/server/channelserver/handlers_diva.go index b6255e18f..7c1f3ec7d 100644 --- a/server/channelserver/handlers_diva.go +++ b/server/channelserver/handlers_diva.go @@ -15,7 +15,7 @@ import ( const ( divaPhaseDuration = 601200 // 6d 23h = first song phase divaInterlude = 3900 // 65 min = gap between phases - divaWeekDuration = 604800 // 7 days = subsequent phase length + divaWeekDuration = secsPerWeek // 7 days = subsequent phase length divaTotalLifespan = 2977200 // ~34.5 days = full event window ) @@ -76,7 +76,8 @@ func handleMsgMhfGetUdSchedule(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetUdSchedule) bf := byteframe.NewByteFrame() - id, start := uint32(0xCAFEBEEF), uint32(0) + const divaIDSentinel = uint32(0xCAFEBEEF) + id, start := divaIDSentinel, uint32(0) rows, err := s.server.db.Queryx("SELECT id, (EXTRACT(epoch FROM start_time)::int) as start_time FROM events WHERE event_type='diva'") if err != nil { s.logger.Error("Failed to query diva schedule", zap.Error(err)) diff --git a/server/channelserver/handlers_event.go b/server/channelserver/handlers_event.go index 8b0bfaf0d..404b52630 100644 --- a/server/channelserver/handlers_event.go +++ b/server/channelserver/handlers_event.go @@ -175,7 +175,7 @@ func handleMsgMhfGetKeepLoginBoostStatus(s *Session, p mhfpacket.MHFPacket) { } } - boost.WeekCount = uint8((TimeAdjusted().Unix()-boost.Expiration.Unix())/604800 + 1) + boost.WeekCount = uint8((TimeAdjusted().Unix()-boost.Expiration.Unix())/secsPerWeek + 1) if boost.WeekCount >= boost.WeekReq { boost.Active = true diff --git a/server/channelserver/handlers_festa.go b/server/channelserver/handlers_festa.go index bd313a4b5..1a4a61ee3 100644 --- a/server/channelserver/handlers_festa.go +++ b/server/channelserver/handlers_festa.go @@ -103,6 +103,13 @@ func cleanupFesta(s *Session) { } } +// Festa timing constants (all values in seconds) +const ( + festaVotingDuration = 9000 // 150 min voting window + festaRewardDuration = 1240200 // ~14.35 days reward period + festaEventLifespan = 2977200 // ~34.5 days total event window +) + func generateFestaTimestamps(s *Session, start uint32, debug bool) []uint32 { timestamps := make([]uint32, 5) midnight := TimeMidnight() @@ -111,26 +118,26 @@ func generateFestaTimestamps(s *Session, start uint32, debug bool) []uint32 { switch start { case 1: timestamps[0] = midnight - timestamps[1] = timestamps[0] + 604800 - timestamps[2] = timestamps[1] + 604800 - timestamps[3] = timestamps[2] + 9000 - timestamps[4] = timestamps[3] + 1240200 + timestamps[1] = timestamps[0] + secsPerWeek + timestamps[2] = timestamps[1] + secsPerWeek + timestamps[3] = timestamps[2] + festaVotingDuration + timestamps[4] = timestamps[3] + festaRewardDuration case 2: - timestamps[0] = midnight - 604800 + timestamps[0] = midnight - secsPerWeek timestamps[1] = midnight - timestamps[2] = timestamps[1] + 604800 - timestamps[3] = timestamps[2] + 9000 - timestamps[4] = timestamps[3] + 1240200 + timestamps[2] = timestamps[1] + secsPerWeek + timestamps[3] = timestamps[2] + festaVotingDuration + timestamps[4] = timestamps[3] + festaRewardDuration case 3: - timestamps[0] = midnight - 1209600 - timestamps[1] = midnight - 604800 + timestamps[0] = midnight - 2*secsPerWeek + timestamps[1] = midnight - secsPerWeek timestamps[2] = midnight - timestamps[3] = timestamps[2] + 9000 - timestamps[4] = timestamps[3] + 1240200 + timestamps[3] = timestamps[2] + festaVotingDuration + timestamps[4] = timestamps[3] + festaRewardDuration } return timestamps } - if start == 0 || TimeAdjusted().Unix() > int64(start)+2977200 { + if start == 0 || TimeAdjusted().Unix() > int64(start)+festaEventLifespan { cleanupFesta(s) // Generate a new festa, starting midnight tomorrow start = uint32(midnight.Add(24 * time.Hour).Unix()) @@ -139,10 +146,10 @@ func generateFestaTimestamps(s *Session, start uint32, debug bool) []uint32 { } } timestamps[0] = start - timestamps[1] = timestamps[0] + 604800 - timestamps[2] = timestamps[1] + 604800 - timestamps[3] = timestamps[2] + 9000 - timestamps[4] = timestamps[3] + 1240200 + timestamps[1] = timestamps[0] + secsPerWeek + timestamps[2] = timestamps[1] + secsPerWeek + timestamps[3] = timestamps[2] + festaVotingDuration + timestamps[4] = timestamps[3] + festaRewardDuration return timestamps } @@ -174,7 +181,8 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfInfoFesta) bf := byteframe.NewByteFrame() - id, start := uint32(0xDEADBEEF), uint32(0) + const festaIDSentinel = uint32(0xDEADBEEF) + id, start := festaIDSentinel, uint32(0) rows, err := s.server.db.Queryx("SELECT id, (EXTRACT(epoch FROM start_time)::int) as start_time FROM events WHERE event_type='festa'") if err != nil { s.logger.Error("Failed to query festa schedule", zap.Error(err)) @@ -342,7 +350,7 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { var guildID uint32 var guildName string var guildTeam = FestivalColorNone - offset := 86400 * uint32(i) + offset := secsPerDay * uint32(i) if err := s.server.db.QueryRow(` SELECT fs.guild_id, g.name, fr.team, SUM(fs.souls) as _ FROM festa_submissions fs @@ -351,7 +359,7 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) { WHERE EXTRACT(EPOCH FROM fs.timestamp)::int > $1 AND EXTRACT(EPOCH FROM fs.timestamp)::int < $2 GROUP BY fs.guild_id, g.name, fr.team ORDER BY _ DESC LIMIT 1 - `, timestamps[1]+offset, timestamps[1]+offset+86400).Scan(&guildID, &guildName, &guildTeam, &temp); err != nil && !errors.Is(err, sql.ErrNoRows) { + `, timestamps[1]+offset, timestamps[1]+offset+secsPerDay).Scan(&guildID, &guildName, &guildTeam, &temp); err != nil && !errors.Is(err, sql.ErrNoRows) { s.logger.Error("Failed to get festa daily ranking", zap.Error(err)) } bf.WriteUint32(guildID) diff --git a/server/channelserver/handlers_guild_mission.go b/server/channelserver/handlers_guild_mission.go index 76081d484..723fd4136 100644 --- a/server/channelserver/handlers_guild_mission.go +++ b/server/channelserver/handlers_guild_mission.go @@ -56,8 +56,9 @@ func handleMsgMhfGetGuildMissionList(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfGetGuildMissionRecord(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetGuildMissionRecord) - // No guild mission records = 0x190 empty bytes - doAckBufSucceed(s, pkt.AckHandle, make([]byte, 0x190)) + const guildMissionRecordSize = 0x190 + // No guild mission records = empty buffer + doAckBufSucceed(s, pkt.AckHandle, make([]byte, guildMissionRecordSize)) } func handleMsgMhfAddGuildMissionCount(s *Session, p mhfpacket.MHFPacket) { diff --git a/server/channelserver/handlers_helpers.go b/server/channelserver/handlers_helpers.go index d872c8e7a..37aa46bcf 100644 --- a/server/channelserver/handlers_helpers.go +++ b/server/channelserver/handlers_helpers.go @@ -96,6 +96,22 @@ func saveCharacterData(s *Session, ackHandle uint32, column string, data []byte, doAckSimpleSucceed(s, ackHandle, make([]byte, 4)) } +// readCharacterInt reads a single integer column from the characters table. +// Returns 0 for NULL columns via COALESCE. +func readCharacterInt(s *Session, column string) (int, error) { + var value int + err := s.server.db.QueryRow("SELECT COALESCE("+column+", 0) FROM characters WHERE id=$1", s.charID).Scan(&value) + return value, err +} + +// adjustCharacterInt atomically adds delta to an integer column and returns the new value. +// Handles NULL columns via COALESCE (NULL + delta = delta). +func adjustCharacterInt(s *Session, column string, delta int) (int, error) { + var value int + err := s.server.db.QueryRow("UPDATE characters SET "+column+"=COALESCE("+column+", 0)+$1 WHERE id=$2 RETURNING "+column, delta, s.charID).Scan(&value) + return value, err +} + func updateRights(s *Session) { rightsInt := uint32(2) _ = s.server.db.QueryRow("SELECT rights FROM users WHERE id=$1", s.userID).Scan(&rightsInt) diff --git a/server/channelserver/handlers_kouryou.go b/server/channelserver/handlers_kouryou.go index 9bde1fe0f..db3735f8a 100644 --- a/server/channelserver/handlers_kouryou.go +++ b/server/channelserver/handlers_kouryou.go @@ -17,8 +17,7 @@ func handleMsgMhfAddKouryouPoint(s *Session, p mhfpacket.MHFPacket) { zap.Uint32("points_to_add", pkt.KouryouPoints), ) - var points int - err := s.server.db.QueryRow("UPDATE characters SET kouryou_point=COALESCE(kouryou_point + $1, $1) WHERE id=$2 RETURNING kouryou_point", pkt.KouryouPoints, s.charID).Scan(&points) + points, err := adjustCharacterInt(s, "kouryou_point", int(pkt.KouryouPoints)) if err != nil { s.logger.Error("Failed to update KouryouPoint in db", zap.Error(err), @@ -42,8 +41,7 @@ func handleMsgMhfAddKouryouPoint(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfGetKouryouPoint(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetKouryouPoint) - var points int - err := s.server.db.QueryRow("SELECT COALESCE(kouryou_point, 0) FROM characters WHERE id = $1", s.charID).Scan(&points) + points, err := readCharacterInt(s, "kouryou_point") if err != nil { s.logger.Error("Failed to get kouryou_point from db", zap.Error(err), @@ -70,8 +68,7 @@ func handleMsgMhfExchangeKouryouPoint(s *Session, p mhfpacket.MHFPacket) { zap.Uint32("points_to_spend", pkt.KouryouPoints), ) - var points int - err := s.server.db.QueryRow("UPDATE characters SET kouryou_point=kouryou_point - $1 WHERE id=$2 RETURNING kouryou_point", pkt.KouryouPoints, s.charID).Scan(&points) + points, err := adjustCharacterInt(s, "kouryou_point", -int(pkt.KouryouPoints)) if err != nil { s.logger.Error("Failed to exchange Koryo points", zap.Error(err), diff --git a/server/channelserver/handlers_mercenary.go b/server/channelserver/handlers_mercenary.go index d60acb4e2..10b4d44e2 100644 --- a/server/channelserver/handlers_mercenary.go +++ b/server/channelserver/handlers_mercenary.go @@ -41,11 +41,17 @@ func handleMsgMhfLoadLegendDispatch(s *Session, p mhfpacket.MHFPacket) { 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 := 552 + naviLength := hunterNaviSizeG8 if s.server.erupeConfig.RealClientMode <= _config.G7 { - naviLength = 280 + naviLength = hunterNaviSizeG7 } loadCharacterData(s, pkt.AckHandle, "hunternavi", make([]byte, naviLength)) } @@ -67,9 +73,9 @@ func handleMsgMhfSaveHunterNavi(s *Session, p mhfpacket.MHFPacket) { var dataSize int if pkt.IsDataDiff { - naviLength := 552 + naviLength := hunterNaviSizeG8 if s.server.erupeConfig.RealClientMode <= _config.G7 { - naviLength = 280 + naviLength = hunterNaviSizeG7 } var data []byte // Load existing save @@ -203,13 +209,13 @@ func handleMsgMhfReadMercenaryW(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfReadMercenaryW) bf := byteframe.NewByteFrame() - var pactID, cid uint32 + var cid uint32 var name string - _ = s.server.db.QueryRow("SELECT pact_id FROM characters WHERE id=$1", s.charID).Scan(&pactID) + pactID, _ := readCharacterInt(s, "pact_id") 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(uint32(pactID)) bf.WriteUint32(cid) bf.WriteBool(true) // Escort enabled bf.WriteUint32(uint32(TimeAdjusted().Unix())) @@ -232,7 +238,7 @@ func handleMsgMhfReadMercenaryW(s *Session, p mhfpacket.MHFPacket) { continue } loans++ - temp.WriteUint32(pactID) + temp.WriteUint32(uint32(pactID)) temp.WriteUint32(cid) temp.WriteUint32(uint32(TimeAdjusted().Unix())) temp.WriteUint32(uint32(TimeAdjusted().Add(time.Hour * 24 * 7).Unix())) @@ -244,9 +250,8 @@ func handleMsgMhfReadMercenaryW(s *Session, p mhfpacket.MHFPacket) { 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) + gcp, _ := readCharacterInt(s, "gcp") if len(data) == 0 { bf.WriteBool(false) @@ -254,7 +259,7 @@ func handleMsgMhfReadMercenaryW(s *Session, p mhfpacket.MHFPacket) { bf.WriteBool(true) bf.WriteBytes(data) } - bf.WriteUint32(gcp) + bf.WriteUint32(uint32(gcp)) } } diff --git a/server/channelserver/handlers_misc.go b/server/channelserver/handlers_misc.go index 1d2019f26..5d96c4481 100644 --- a/server/channelserver/handlers_misc.go +++ b/server/channelserver/handlers_misc.go @@ -4,7 +4,6 @@ import ( "erupe-ce/common/byteframe" _config "erupe-ce/config" "erupe-ce/network/mhfpacket" - "fmt" "math/bits" "time" @@ -23,7 +22,9 @@ func handleMsgMhfGetEtcPoints(s *Session, p mhfpacket.MHFPacket) { } var bonusQuests, dailyQuests, promoPoints uint32 - _ = s.server.db.QueryRow(`SELECT bonus_quests, daily_quests, promo_points FROM characters WHERE id = $1`, s.charID).Scan(&bonusQuests, &dailyQuests, &promoPoints) + if err := s.server.db.QueryRow(`SELECT bonus_quests, daily_quests, promo_points FROM characters WHERE id = $1`, s.charID).Scan(&bonusQuests, &dailyQuests, &promoPoints); 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) @@ -48,17 +49,11 @@ func handleMsgMhfUpdateEtcPoint(s *Session, p mhfpacket.MHFPacket) { return } - var value int16 - err := s.server.db.QueryRow(fmt.Sprintf(`SELECT %s FROM characters WHERE id = $1`, column), s.charID).Scan(&value) + value, err := readCharacterInt(s, column) if err == nil { - if value+pkt.Delta < 0 { - if _, err := s.server.db.Exec(fmt.Sprintf(`UPDATE characters SET %s = 0 WHERE id = $1`, column), s.charID); err != nil { - s.logger.Error("Failed to reset etc point", zap.Error(err)) - } - } else { - if _, err := s.server.db.Exec(fmt.Sprintf(`UPDATE characters SET %s = %s + $1 WHERE id = $2`, column, column), pkt.Delta, s.charID); err != nil { - s.logger.Error("Failed to update etc point", zap.Error(err)) - } + newVal := max(value+int(pkt.Delta), 0) + if _, err := s.server.db.Exec("UPDATE characters SET "+column+"=$1 WHERE id=$2", newVal, s.charID); err != nil { + s.logger.Error("Failed to update etc point", zap.Error(err)) } } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) @@ -156,13 +151,20 @@ func handleMsgMhfGetDailyMissionPersonal(s *Session, p mhfpacket.MHFPacket) {} func handleMsgMhfSetDailyMissionPersonal(s *Session, p mhfpacket.MHFPacket) {} +// 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 _config.Mode) int { - size := 3200 + size := skinHistSizeZZ if mode <= _config.Z2 { - size = 2560 + size = skinHistSizeZ2 } if mode <= _config.Z1 { - size = 1280 + size = skinHistSizeZ1 } return size } @@ -245,7 +247,8 @@ func handleMsgMhfGetLobbyCrowd(s *Session, p mhfpacket.MHFPacket) { // 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) - doAckBufSucceed(s, pkt.AckHandle, make([]byte, 0x320)) + const lobbyCrowdResponseSize = 0x320 + doAckBufSucceed(s, pkt.AckHandle, make([]byte, lobbyCrowdResponseSize)) } // TrendWeapon represents trending weapon usage data. diff --git a/server/channelserver/handlers_plate.go b/server/channelserver/handlers_plate.go index 8774bd11c..1a548e653 100644 --- a/server/channelserver/handlers_plate.go +++ b/server/channelserver/handlers_plate.go @@ -34,9 +34,19 @@ func handleMsgMhfLoadPlateData(s *Session, p mhfpacket.MHFPacket) { loadCharacterData(s, pkt.AckHandle, "platedata", nil) } +// Plate data size constants +const ( + plateDataMaxPayload = 262144 // max compressed platedata size + plateDataEmptySize = 140000 // empty platedata buffer + plateBoxMaxPayload = 32768 // max compressed platebox size + plateBoxEmptySize = 4800 // empty platebox buffer + plateMysetDefaultLen = 1920 // default platemyset buffer + plateMysetMaxPayload = 4096 // max platemyset payload size +) + func handleMsgMhfSavePlateData(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfSavePlateData) - if len(pkt.RawDataPayload) > 262144 { + if len(pkt.RawDataPayload) > plateDataMaxPayload { s.logger.Warn("PlateData payload too large", zap.Int("len", len(pkt.RawDataPayload))) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return @@ -78,7 +88,7 @@ func handleMsgMhfSavePlateData(s *Session, p mhfpacket.MHFPacket) { } } else { // create empty save if absent - data = make([]byte, 140000) + data = make([]byte, plateDataEmptySize) } // Perform diff and compress it to write back to db @@ -144,7 +154,7 @@ func handleMsgMhfLoadPlateBox(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfSavePlateBox(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfSavePlateBox) - if len(pkt.RawDataPayload) > 32768 { + if len(pkt.RawDataPayload) > plateBoxMaxPayload { s.logger.Warn("PlateBox payload too large", zap.Int("len", len(pkt.RawDataPayload))) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return @@ -173,7 +183,7 @@ func handleMsgMhfSavePlateBox(s *Session, p mhfpacket.MHFPacket) { } } else { // create empty save if absent - data = make([]byte, 4800) + data = make([]byte, plateBoxEmptySize) } // Perform diff and compress it to write back to db @@ -213,12 +223,12 @@ func handleMsgMhfSavePlateBox(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfLoadPlateMyset(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfLoadPlateMyset) - loadCharacterData(s, pkt.AckHandle, "platemyset", make([]byte, 1920)) + loadCharacterData(s, pkt.AckHandle, "platemyset", make([]byte, plateMysetDefaultLen)) } func handleMsgMhfSavePlateMyset(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfSavePlateMyset) - if len(pkt.RawDataPayload) > 4096 { + if len(pkt.RawDataPayload) > plateMysetMaxPayload { s.logger.Warn("PlateMyset payload too large", zap.Int("len", len(pkt.RawDataPayload))) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return diff --git a/server/channelserver/handlers_register.go b/server/channelserver/handlers_register.go index 95ed81e14..ed30d154e 100644 --- a/server/channelserver/handlers_register.go +++ b/server/channelserver/handlers_register.go @@ -19,6 +19,9 @@ func handleMsgMhfRegisterEvent(s *Session, p mhfpacket.MHFPacket) { doAckSimpleSucceed(s, pkt.AckHandle, bf.Data()) } +// ACK error codes from the MHF client +const ackEFailed = uint8(0x41) // _ACK_EFAILED = 65 + func handleMsgMhfReleaseEvent(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfReleaseEvent) @@ -43,7 +46,7 @@ func handleMsgMhfReleaseEvent(s *Session, p mhfpacket.MHFPacket) { s.QueueSendMHF(&mhfpacket.MsgSysAck{ AckHandle: pkt.AckHandle, IsBufferResponse: false, - ErrorCode: 0x41, + ErrorCode: ackEFailed, AckData: []byte{0x00, 0x00, 0x00, 0x00}, }) } @@ -104,11 +107,11 @@ func handleMsgSysLoadRegister(s *Session, p mhfpacket.MHFPacket) { bf.WriteUint8(pkt.Values) for i := uint8(0); i < pkt.Values; i++ { switch pkt.RegisterID { - case 0x40000: + case raviRegisterState: bf.WriteUint32(s.server.raviente.state[i]) - case 0x50000: + case raviRegisterSupport: bf.WriteUint32(s.server.raviente.support[i]) - case 0x60000: + case raviRegisterGeneral: bf.WriteUint32(s.server.raviente.register[i]) } } @@ -122,13 +125,13 @@ func (s *Session) notifyRavi() { } var temp mhfpacket.MHFPacket raviNotif := byteframe.NewByteFrame() - temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: 0x40000} + temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: raviRegisterState} raviNotif.WriteUint16(uint16(temp.Opcode())) _ = temp.Build(raviNotif, s.clientContext) - temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: 0x50000} + temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: raviRegisterSupport} raviNotif.WriteUint16(uint16(temp.Opcode())) _ = temp.Build(raviNotif, s.clientContext) - temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: 0x60000} + temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: raviRegisterGeneral} raviNotif.WriteUint16(uint16(temp.Opcode())) _ = temp.Build(raviNotif, s.clientContext) raviNotif.WriteUint16(0x0010) // End it. diff --git a/server/channelserver/handlers_reward.go b/server/channelserver/handlers_reward.go index 73234c2ae..a25035818 100644 --- a/server/channelserver/handlers_reward.go +++ b/server/channelserver/handlers_reward.go @@ -12,7 +12,8 @@ func handleMsgMhfGetAdditionalBeatReward(s *Session, p mhfpacket.MHFPacket) { // Actual response in packet captures are all just giant batches of null bytes // I'm assuming this is because it used to be tied to an actual event and // they never bothered killing off the packet when they made it static - doAckBufSucceed(s, pkt.AckHandle, make([]byte, 0x104)) + const beatRewardResponseSize = 0x104 + doAckBufSucceed(s, pkt.AckHandle, make([]byte, beatRewardResponseSize)) } func handleMsgMhfGetUdRankingRewardList(s *Session, p mhfpacket.MHFPacket) { diff --git a/server/channelserver/handlers_semaphore.go b/server/channelserver/handlers_semaphore.go index 5f36f7b6e..5aa8da16a 100644 --- a/server/channelserver/handlers_semaphore.go +++ b/server/channelserver/handlers_semaphore.go @@ -79,9 +79,9 @@ func handleMsgSysCreateAcquireSemaphore(s *Session, p mhfpacket.MHFPacket) { suffix, _ := strconv.Atoi(pkt.SemaphoreID[len(pkt.SemaphoreID)-1:]) s.server.semaphore[SemaphoreID] = &Semaphore{ name: pkt.SemaphoreID, - id: uint32((suffix + 1) * 0x10000), + id: uint32((suffix + 1) * raviSemaphoreStride), clients: make(map[*Session]uint32), - maxPlayers: 127, + maxPlayers: raviSemaphoreMax, } } else { s.server.semaphore[SemaphoreID] = NewSemaphore(s, SemaphoreID, 1) diff --git a/server/channelserver/handlers_session.go b/server/channelserver/handlers_session.go index 1aca36db1..9b75a88b5 100644 --- a/server/channelserver/handlers_session.go +++ b/server/channelserver/handlers_session.go @@ -394,6 +394,8 @@ func handleMsgSysIssueLogkey(s *Session, p mhfpacket.MHFPacket) { doAckBufSucceed(s, pkt.AckHandle, resp.Data()) } +const localhostAddrLE = uint32(0x0100007F) // 127.0.0.1 in little-endian + // Kill log binary layout constants const ( killLogHeaderSize = 32 // bytes before monster kill count array @@ -557,7 +559,7 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) { if !local { resp.WriteUint32(binary.LittleEndian.Uint32(r.ip)) } else { - resp.WriteUint32(0x0100007F) + resp.WriteUint32(localhostAddrLE) } resp.WriteUint16(r.port) resp.WriteUint32(r.charID) @@ -757,7 +759,7 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) { if !local { resp.WriteUint32(binary.LittleEndian.Uint32(sr.ip)) } else { - resp.WriteUint32(0x0100007F) + resp.WriteUint32(localhostAddrLE) } resp.WriteUint16(sr.port) diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index 6b7f3070a..1e11294c2 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -312,7 +312,7 @@ func (s *Server) BroadcastChatMessage(message string) { msgBinChat := &binpacket.MsgBinChat{ Unk0: 0, Type: 5, - Flags: 0x80, + Flags: chatFlagServer, Message: message, SenderName: s.name, } @@ -420,8 +420,15 @@ func (s *Server) HasSemaphore(ses *Session) bool { return false } +// Server ID arithmetic constants +const ( + serverIDHighMask = uint16(0xFF00) + serverIDBase = 0x1000 // first server ID offset + serverIDStride = 0x100 // spacing between server IDs +) + // Season returns the current in-game season (0-2) based on server ID and time. func (s *Server) Season() uint8 { - sid := int64(((s.ID & 0xFF00) - 4096) / 256) - return uint8(((TimeAdjusted().Unix() / 86400) + sid) % 3) + sid := int64(((s.ID & serverIDHighMask) - serverIDBase) / serverIDStride) + return uint8(((TimeAdjusted().Unix() / secsPerDay) + sid) % 3) }