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.
This commit is contained in:
Houmgaor
2026-02-20 21:18:40 +01:00
parent 28bf6e93fb
commit 458d8c9397
20 changed files with 182 additions and 124 deletions

View File

@@ -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 - **Round 1** (commit `7c444b0`): `constants_quest.go`, `handlers_guild_info.go`, `handlers_quest.go`, `handlers_rengoku.go`, `handlers_session.go`, `model_character.go`
// handlers_cast_binary.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`
bf.WriteUint8(0x02)
bf.WriteUint16(0x00)
bf.Seek(4, io.SeekStart)
```
```go **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.
// handlers_data.go
if dataLen > 0x20000 { ... }
```
```go **Impact:** ~~New contributors can't understand what these values mean.~~ Most protocol-meaningful constants now have names and comments.
// 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`).
--- ---
@@ -318,7 +302,7 @@ Database operations use raw `database/sql` with PostgreSQL-specific syntax throu
| Severity | Anti-patterns | | Severity | Anti-patterns |
|----------|--------------| |----------|--------------|
| **High** | ~~Missing ACK responses / softlocks (#2)~~ **Fixed**, no architectural layering (#3), tight DB coupling (#13) | | **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** | | **Low** | God files (#1), ~~`init()` registration (#10)~~ **Fixed**, ~~inconsistent logging (#12)~~ **Fixed**, mutex granularity (#7), ~~panic-based flow (#11)~~ **Fixed** |
### Root Cause ### Root Cause

View File

@@ -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
)

View File

@@ -0,0 +1,7 @@
package channelserver
// Shared time duration constants (seconds)
const (
secsPerDay = 86400 // 24 hours
secsPerWeek = 604800 // 7 days
)

View File

@@ -14,25 +14,23 @@ import (
func handleMsgMhfAcquireCafeItem(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfAcquireCafeItem(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfAcquireCafeItem) pkt := p.(*mhfpacket.MsgMhfAcquireCafeItem)
var netcafePoints uint32 netcafePoints, err := adjustCharacterInt(s, "netcafe_points", -int(pkt.PointCost))
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)
if err != nil { 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 := byteframe.NewByteFrame()
resp.WriteUint32(netcafePoints) resp.WriteUint32(uint32(netcafePoints))
doAckSimpleSucceed(s, pkt.AckHandle, resp.Data()) doAckSimpleSucceed(s, pkt.AckHandle, resp.Data())
} }
func handleMsgMhfUpdateCafepoint(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfUpdateCafepoint(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfUpdateCafepoint) pkt := p.(*mhfpacket.MsgMhfUpdateCafepoint)
var netcafePoints uint32 netcafePoints, err := readCharacterInt(s, "netcafe_points")
err := s.server.db.QueryRow("SELECT COALESCE(netcafe_points, 0) FROM characters WHERE id = $1", s.charID).Scan(&netcafePoints)
if err != nil { 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 := byteframe.NewByteFrame()
resp.WriteUint32(netcafePoints) resp.WriteUint32(uint32(netcafePoints))
doAckSimpleSucceed(s, pkt.AckHandle, resp.Data()) doAckSimpleSucceed(s, pkt.AckHandle, resp.Data())
} }
@@ -93,17 +91,16 @@ func handleMsgMhfGetCafeDuration(s *Session, p mhfpacket.MHFPacket) {
} }
} }
var cafeTime uint32 cafeTime, err := readCharacterInt(s, "cafe_time")
err = s.server.db.QueryRow("SELECT cafe_time FROM characters WHERE id = $1", s.charID).Scan(&cafeTime)
if err != nil { if err != nil {
s.logger.Error("Failed to get cafe time", zap.Error(err)) s.logger.Error("Failed to get cafe time", zap.Error(err))
doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
return return
} }
if mhfcourse.CourseExists(30, s.courses) { 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 { if s.server.erupeConfig.RealClientMode >= _config.ZZ {
bf.WriteUint16(0) bf.WriteUint16(0)
ps.Uint16(bf, fmt.Sprintf(s.server.i18n.cafe.reset, int(cafeReset.Month()), cafeReset.Day()), true) 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 { func addPointNetcafe(s *Session, p int) error {
var points int points, err := readCharacterInt(s, "netcafe_points")
err := s.server.db.QueryRow("SELECT netcafe_points FROM characters WHERE id = $1", s.charID).Scan(&points)
if err != nil { if err != nil {
return err return err
} }
if points+p > s.server.erupeConfig.GameplayOptions.MaximumNP { points = min(points+p, s.server.erupeConfig.GameplayOptions.MaximumNP)
points = s.server.erupeConfig.GameplayOptions.MaximumNP
} else {
points += p
}
if _, err := s.server.db.Exec("UPDATE characters SET netcafe_points=$1 WHERE id=$2", points, s.charID); err != nil { 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)) s.logger.Error("Failed to update netcafe points", zap.Error(err))
} }

View File

@@ -34,8 +34,13 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysCastBinary) pkt := p.(*mhfpacket.MsgSysCastBinary)
tmp := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload) tmp := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload)
if pkt.BroadcastType == BroadcastTypeStage && pkt.MessageType == BinaryMessageTypeData && len(pkt.RawDataPayload) == 0x10 { const (
if tmp.ReadUint16() == 0x0002 && tmp.ReadUint8() == 0x18 { 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 var timer bool
if err := s.server.db.QueryRow(`SELECT COALESCE(timer, false) FROM users WHERE id=$1`, s.userID).Scan(&timer); err != nil { 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)) s.logger.Error("Failed to get timer setting", zap.Error(err))

View File

@@ -44,6 +44,8 @@ func sendDisabledCommandMessage(s *Session, cmd _config.Command) {
sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.disabled, cmd.Name)) 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) { func sendServerChatMessage(s *Session, message string) {
// Make the inside of the casted binary // Make the inside of the casted binary
bf := byteframe.NewByteFrame() bf := byteframe.NewByteFrame()
@@ -51,7 +53,7 @@ func sendServerChatMessage(s *Session, message string) {
msgBinChat := &binpacket.MsgBinChat{ msgBinChat := &binpacket.MsgBinChat{
Unk0: 0, Unk0: 0,
Type: 5, Type: 5,
Flags: 0x80, Flags: chatFlagServer,
Message: message, Message: message,
SenderName: "Erupe", SenderName: "Erupe",
} }

View File

@@ -15,7 +15,7 @@ import (
const ( const (
divaPhaseDuration = 601200 // 6d 23h = first song phase divaPhaseDuration = 601200 // 6d 23h = first song phase
divaInterlude = 3900 // 65 min = gap between phases 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 divaTotalLifespan = 2977200 // ~34.5 days = full event window
) )
@@ -76,7 +76,8 @@ func handleMsgMhfGetUdSchedule(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetUdSchedule) pkt := p.(*mhfpacket.MsgMhfGetUdSchedule)
bf := byteframe.NewByteFrame() 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'") 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 { if err != nil {
s.logger.Error("Failed to query diva schedule", zap.Error(err)) s.logger.Error("Failed to query diva schedule", zap.Error(err))

View File

@@ -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 { if boost.WeekCount >= boost.WeekReq {
boost.Active = true boost.Active = true

View File

@@ -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 { func generateFestaTimestamps(s *Session, start uint32, debug bool) []uint32 {
timestamps := make([]uint32, 5) timestamps := make([]uint32, 5)
midnight := TimeMidnight() midnight := TimeMidnight()
@@ -111,26 +118,26 @@ func generateFestaTimestamps(s *Session, start uint32, debug bool) []uint32 {
switch start { switch start {
case 1: case 1:
timestamps[0] = midnight timestamps[0] = midnight
timestamps[1] = timestamps[0] + 604800 timestamps[1] = timestamps[0] + secsPerWeek
timestamps[2] = timestamps[1] + 604800 timestamps[2] = timestamps[1] + secsPerWeek
timestamps[3] = timestamps[2] + 9000 timestamps[3] = timestamps[2] + festaVotingDuration
timestamps[4] = timestamps[3] + 1240200 timestamps[4] = timestamps[3] + festaRewardDuration
case 2: case 2:
timestamps[0] = midnight - 604800 timestamps[0] = midnight - secsPerWeek
timestamps[1] = midnight timestamps[1] = midnight
timestamps[2] = timestamps[1] + 604800 timestamps[2] = timestamps[1] + secsPerWeek
timestamps[3] = timestamps[2] + 9000 timestamps[3] = timestamps[2] + festaVotingDuration
timestamps[4] = timestamps[3] + 1240200 timestamps[4] = timestamps[3] + festaRewardDuration
case 3: case 3:
timestamps[0] = midnight - 1209600 timestamps[0] = midnight - 2*secsPerWeek
timestamps[1] = midnight - 604800 timestamps[1] = midnight - secsPerWeek
timestamps[2] = midnight timestamps[2] = midnight
timestamps[3] = timestamps[2] + 9000 timestamps[3] = timestamps[2] + festaVotingDuration
timestamps[4] = timestamps[3] + 1240200 timestamps[4] = timestamps[3] + festaRewardDuration
} }
return timestamps return timestamps
} }
if start == 0 || TimeAdjusted().Unix() > int64(start)+2977200 { if start == 0 || TimeAdjusted().Unix() > int64(start)+festaEventLifespan {
cleanupFesta(s) cleanupFesta(s)
// Generate a new festa, starting midnight tomorrow // Generate a new festa, starting midnight tomorrow
start = uint32(midnight.Add(24 * time.Hour).Unix()) 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[0] = start
timestamps[1] = timestamps[0] + 604800 timestamps[1] = timestamps[0] + secsPerWeek
timestamps[2] = timestamps[1] + 604800 timestamps[2] = timestamps[1] + secsPerWeek
timestamps[3] = timestamps[2] + 9000 timestamps[3] = timestamps[2] + festaVotingDuration
timestamps[4] = timestamps[3] + 1240200 timestamps[4] = timestamps[3] + festaRewardDuration
return timestamps return timestamps
} }
@@ -174,7 +181,8 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfInfoFesta) pkt := p.(*mhfpacket.MsgMhfInfoFesta)
bf := byteframe.NewByteFrame() 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'") 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 { if err != nil {
s.logger.Error("Failed to query festa schedule", zap.Error(err)) 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 guildID uint32
var guildName string var guildName string
var guildTeam = FestivalColorNone var guildTeam = FestivalColorNone
offset := 86400 * uint32(i) offset := secsPerDay * uint32(i)
if err := s.server.db.QueryRow(` if err := s.server.db.QueryRow(`
SELECT fs.guild_id, g.name, fr.team, SUM(fs.souls) as _ SELECT fs.guild_id, g.name, fr.team, SUM(fs.souls) as _
FROM festa_submissions fs 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 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 GROUP BY fs.guild_id, g.name, fr.team
ORDER BY _ DESC LIMIT 1 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)) s.logger.Error("Failed to get festa daily ranking", zap.Error(err))
} }
bf.WriteUint32(guildID) bf.WriteUint32(guildID)

View File

@@ -56,8 +56,9 @@ func handleMsgMhfGetGuildMissionList(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfGetGuildMissionRecord(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfGetGuildMissionRecord(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetGuildMissionRecord) pkt := p.(*mhfpacket.MsgMhfGetGuildMissionRecord)
// No guild mission records = 0x190 empty bytes const guildMissionRecordSize = 0x190
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 0x190)) // No guild mission records = empty buffer
doAckBufSucceed(s, pkt.AckHandle, make([]byte, guildMissionRecordSize))
} }
func handleMsgMhfAddGuildMissionCount(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfAddGuildMissionCount(s *Session, p mhfpacket.MHFPacket) {

View File

@@ -96,6 +96,22 @@ func saveCharacterData(s *Session, ackHandle uint32, column string, data []byte,
doAckSimpleSucceed(s, ackHandle, make([]byte, 4)) 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) { func updateRights(s *Session) {
rightsInt := uint32(2) rightsInt := uint32(2)
_ = s.server.db.QueryRow("SELECT rights FROM users WHERE id=$1", s.userID).Scan(&rightsInt) _ = s.server.db.QueryRow("SELECT rights FROM users WHERE id=$1", s.userID).Scan(&rightsInt)

View File

@@ -17,8 +17,7 @@ func handleMsgMhfAddKouryouPoint(s *Session, p mhfpacket.MHFPacket) {
zap.Uint32("points_to_add", pkt.KouryouPoints), zap.Uint32("points_to_add", pkt.KouryouPoints),
) )
var points int points, err := adjustCharacterInt(s, "kouryou_point", int(pkt.KouryouPoints))
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)
if err != nil { if err != nil {
s.logger.Error("Failed to update KouryouPoint in db", s.logger.Error("Failed to update KouryouPoint in db",
zap.Error(err), zap.Error(err),
@@ -42,8 +41,7 @@ func handleMsgMhfAddKouryouPoint(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfGetKouryouPoint(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfGetKouryouPoint(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetKouryouPoint) pkt := p.(*mhfpacket.MsgMhfGetKouryouPoint)
var points int points, err := readCharacterInt(s, "kouryou_point")
err := s.server.db.QueryRow("SELECT COALESCE(kouryou_point, 0) FROM characters WHERE id = $1", s.charID).Scan(&points)
if err != nil { if err != nil {
s.logger.Error("Failed to get kouryou_point from db", s.logger.Error("Failed to get kouryou_point from db",
zap.Error(err), zap.Error(err),
@@ -70,8 +68,7 @@ func handleMsgMhfExchangeKouryouPoint(s *Session, p mhfpacket.MHFPacket) {
zap.Uint32("points_to_spend", pkt.KouryouPoints), zap.Uint32("points_to_spend", pkt.KouryouPoints),
) )
var points int points, err := adjustCharacterInt(s, "kouryou_point", -int(pkt.KouryouPoints))
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)
if err != nil { if err != nil {
s.logger.Error("Failed to exchange Koryo points", s.logger.Error("Failed to exchange Koryo points",
zap.Error(err), zap.Error(err),

View File

@@ -41,11 +41,17 @@ func handleMsgMhfLoadLegendDispatch(s *Session, p mhfpacket.MHFPacket) {
doAckBufSucceed(s, pkt.AckHandle, bf.Data()) 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) { func handleMsgMhfLoadHunterNavi(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfLoadHunterNavi) pkt := p.(*mhfpacket.MsgMhfLoadHunterNavi)
naviLength := 552 naviLength := hunterNaviSizeG8
if s.server.erupeConfig.RealClientMode <= _config.G7 { if s.server.erupeConfig.RealClientMode <= _config.G7 {
naviLength = 280 naviLength = hunterNaviSizeG7
} }
loadCharacterData(s, pkt.AckHandle, "hunternavi", make([]byte, naviLength)) loadCharacterData(s, pkt.AckHandle, "hunternavi", make([]byte, naviLength))
} }
@@ -67,9 +73,9 @@ func handleMsgMhfSaveHunterNavi(s *Session, p mhfpacket.MHFPacket) {
var dataSize int var dataSize int
if pkt.IsDataDiff { if pkt.IsDataDiff {
naviLength := 552 naviLength := hunterNaviSizeG8
if s.server.erupeConfig.RealClientMode <= _config.G7 { if s.server.erupeConfig.RealClientMode <= _config.G7 {
naviLength = 280 naviLength = hunterNaviSizeG7
} }
var data []byte var data []byte
// Load existing save // Load existing save
@@ -203,13 +209,13 @@ func handleMsgMhfReadMercenaryW(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfReadMercenaryW) pkt := p.(*mhfpacket.MsgMhfReadMercenaryW)
bf := byteframe.NewByteFrame() bf := byteframe.NewByteFrame()
var pactID, cid uint32 var cid uint32
var name string 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 { if pactID > 0 {
_ = s.server.db.QueryRow("SELECT name, id FROM characters WHERE rasta_id = $1", pactID).Scan(&name, &cid) _ = s.server.db.QueryRow("SELECT name, id FROM characters WHERE rasta_id = $1", pactID).Scan(&name, &cid)
bf.WriteUint8(1) // numLends bf.WriteUint8(1) // numLends
bf.WriteUint32(pactID) bf.WriteUint32(uint32(pactID))
bf.WriteUint32(cid) bf.WriteUint32(cid)
bf.WriteBool(true) // Escort enabled bf.WriteBool(true) // Escort enabled
bf.WriteUint32(uint32(TimeAdjusted().Unix())) bf.WriteUint32(uint32(TimeAdjusted().Unix()))
@@ -232,7 +238,7 @@ func handleMsgMhfReadMercenaryW(s *Session, p mhfpacket.MHFPacket) {
continue continue
} }
loans++ loans++
temp.WriteUint32(pactID) temp.WriteUint32(uint32(pactID))
temp.WriteUint32(cid) temp.WriteUint32(cid)
temp.WriteUint32(uint32(TimeAdjusted().Unix())) temp.WriteUint32(uint32(TimeAdjusted().Unix()))
temp.WriteUint32(uint32(TimeAdjusted().Add(time.Hour * 24 * 7).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 { if pkt.Op != 1 && pkt.Op != 4 {
var data []byte 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 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 { if len(data) == 0 {
bf.WriteBool(false) bf.WriteBool(false)
@@ -254,7 +259,7 @@ func handleMsgMhfReadMercenaryW(s *Session, p mhfpacket.MHFPacket) {
bf.WriteBool(true) bf.WriteBool(true)
bf.WriteBytes(data) bf.WriteBytes(data)
} }
bf.WriteUint32(gcp) bf.WriteUint32(uint32(gcp))
} }
} }

View File

@@ -4,7 +4,6 @@ import (
"erupe-ce/common/byteframe" "erupe-ce/common/byteframe"
_config "erupe-ce/config" _config "erupe-ce/config"
"erupe-ce/network/mhfpacket" "erupe-ce/network/mhfpacket"
"fmt"
"math/bits" "math/bits"
"time" "time"
@@ -23,7 +22,9 @@ func handleMsgMhfGetEtcPoints(s *Session, p mhfpacket.MHFPacket) {
} }
var bonusQuests, dailyQuests, promoPoints uint32 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 := byteframe.NewByteFrame()
resp.WriteUint8(3) // Maybe a count of uint32(s)? resp.WriteUint8(3) // Maybe a count of uint32(s)?
resp.WriteUint32(bonusQuests) resp.WriteUint32(bonusQuests)
@@ -48,17 +49,11 @@ func handleMsgMhfUpdateEtcPoint(s *Session, p mhfpacket.MHFPacket) {
return return
} }
var value int16 value, err := readCharacterInt(s, column)
err := s.server.db.QueryRow(fmt.Sprintf(`SELECT %s FROM characters WHERE id = $1`, column), s.charID).Scan(&value)
if err == nil { if err == nil {
if value+pkt.Delta < 0 { newVal := max(value+int(pkt.Delta), 0)
if _, err := s.server.db.Exec(fmt.Sprintf(`UPDATE characters SET %s = 0 WHERE id = $1`, column), s.charID); err != nil { if _, err := s.server.db.Exec("UPDATE characters SET "+column+"=$1 WHERE id=$2", newVal, s.charID); err != nil {
s.logger.Error("Failed to reset etc point", zap.Error(err)) s.logger.Error("Failed to update 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))
}
} }
} }
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) 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) {} 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 { func equipSkinHistSize(mode _config.Mode) int {
size := 3200 size := skinHistSizeZZ
if mode <= _config.Z2 { if mode <= _config.Z2 {
size = 2560 size = skinHistSizeZ2
} }
if mode <= _config.Z1 { if mode <= _config.Z1 {
size = 1280 size = skinHistSizeZ1
} }
return size 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 // 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 // full servers to actually need to migrate people from and empty ones to
pkt := p.(*mhfpacket.MsgMhfGetLobbyCrowd) 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. // TrendWeapon represents trending weapon usage data.

View File

@@ -34,9 +34,19 @@ func handleMsgMhfLoadPlateData(s *Session, p mhfpacket.MHFPacket) {
loadCharacterData(s, pkt.AckHandle, "platedata", nil) 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) { func handleMsgMhfSavePlateData(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfSavePlateData) 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))) s.logger.Warn("PlateData payload too large", zap.Int("len", len(pkt.RawDataPayload)))
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return return
@@ -78,7 +88,7 @@ func handleMsgMhfSavePlateData(s *Session, p mhfpacket.MHFPacket) {
} }
} else { } else {
// create empty save if absent // create empty save if absent
data = make([]byte, 140000) data = make([]byte, plateDataEmptySize)
} }
// Perform diff and compress it to write back to db // 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) { func handleMsgMhfSavePlateBox(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfSavePlateBox) 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))) s.logger.Warn("PlateBox payload too large", zap.Int("len", len(pkt.RawDataPayload)))
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return return
@@ -173,7 +183,7 @@ func handleMsgMhfSavePlateBox(s *Session, p mhfpacket.MHFPacket) {
} }
} else { } else {
// create empty save if absent // create empty save if absent
data = make([]byte, 4800) data = make([]byte, plateBoxEmptySize)
} }
// Perform diff and compress it to write back to db // 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) { func handleMsgMhfLoadPlateMyset(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfLoadPlateMyset) 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) { func handleMsgMhfSavePlateMyset(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfSavePlateMyset) 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))) s.logger.Warn("PlateMyset payload too large", zap.Int("len", len(pkt.RawDataPayload)))
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return return

View File

@@ -19,6 +19,9 @@ func handleMsgMhfRegisterEvent(s *Session, p mhfpacket.MHFPacket) {
doAckSimpleSucceed(s, pkt.AckHandle, bf.Data()) 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) { func handleMsgMhfReleaseEvent(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfReleaseEvent) pkt := p.(*mhfpacket.MsgMhfReleaseEvent)
@@ -43,7 +46,7 @@ func handleMsgMhfReleaseEvent(s *Session, p mhfpacket.MHFPacket) {
s.QueueSendMHF(&mhfpacket.MsgSysAck{ s.QueueSendMHF(&mhfpacket.MsgSysAck{
AckHandle: pkt.AckHandle, AckHandle: pkt.AckHandle,
IsBufferResponse: false, IsBufferResponse: false,
ErrorCode: 0x41, ErrorCode: ackEFailed,
AckData: []byte{0x00, 0x00, 0x00, 0x00}, AckData: []byte{0x00, 0x00, 0x00, 0x00},
}) })
} }
@@ -104,11 +107,11 @@ func handleMsgSysLoadRegister(s *Session, p mhfpacket.MHFPacket) {
bf.WriteUint8(pkt.Values) bf.WriteUint8(pkt.Values)
for i := uint8(0); i < pkt.Values; i++ { for i := uint8(0); i < pkt.Values; i++ {
switch pkt.RegisterID { switch pkt.RegisterID {
case 0x40000: case raviRegisterState:
bf.WriteUint32(s.server.raviente.state[i]) bf.WriteUint32(s.server.raviente.state[i])
case 0x50000: case raviRegisterSupport:
bf.WriteUint32(s.server.raviente.support[i]) bf.WriteUint32(s.server.raviente.support[i])
case 0x60000: case raviRegisterGeneral:
bf.WriteUint32(s.server.raviente.register[i]) bf.WriteUint32(s.server.raviente.register[i])
} }
} }
@@ -122,13 +125,13 @@ func (s *Session) notifyRavi() {
} }
var temp mhfpacket.MHFPacket var temp mhfpacket.MHFPacket
raviNotif := byteframe.NewByteFrame() raviNotif := byteframe.NewByteFrame()
temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: 0x40000} temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: raviRegisterState}
raviNotif.WriteUint16(uint16(temp.Opcode())) raviNotif.WriteUint16(uint16(temp.Opcode()))
_ = temp.Build(raviNotif, s.clientContext) _ = temp.Build(raviNotif, s.clientContext)
temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: 0x50000} temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: raviRegisterSupport}
raviNotif.WriteUint16(uint16(temp.Opcode())) raviNotif.WriteUint16(uint16(temp.Opcode()))
_ = temp.Build(raviNotif, s.clientContext) _ = temp.Build(raviNotif, s.clientContext)
temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: 0x60000} temp = &mhfpacket.MsgSysNotifyRegister{RegisterID: raviRegisterGeneral}
raviNotif.WriteUint16(uint16(temp.Opcode())) raviNotif.WriteUint16(uint16(temp.Opcode()))
_ = temp.Build(raviNotif, s.clientContext) _ = temp.Build(raviNotif, s.clientContext)
raviNotif.WriteUint16(0x0010) // End it. raviNotif.WriteUint16(0x0010) // End it.

View File

@@ -12,7 +12,8 @@ func handleMsgMhfGetAdditionalBeatReward(s *Session, p mhfpacket.MHFPacket) {
// Actual response in packet captures are all just giant batches of null bytes // 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 // 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 // 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) { func handleMsgMhfGetUdRankingRewardList(s *Session, p mhfpacket.MHFPacket) {

View File

@@ -79,9 +79,9 @@ func handleMsgSysCreateAcquireSemaphore(s *Session, p mhfpacket.MHFPacket) {
suffix, _ := strconv.Atoi(pkt.SemaphoreID[len(pkt.SemaphoreID)-1:]) suffix, _ := strconv.Atoi(pkt.SemaphoreID[len(pkt.SemaphoreID)-1:])
s.server.semaphore[SemaphoreID] = &Semaphore{ s.server.semaphore[SemaphoreID] = &Semaphore{
name: pkt.SemaphoreID, name: pkt.SemaphoreID,
id: uint32((suffix + 1) * 0x10000), id: uint32((suffix + 1) * raviSemaphoreStride),
clients: make(map[*Session]uint32), clients: make(map[*Session]uint32),
maxPlayers: 127, maxPlayers: raviSemaphoreMax,
} }
} else { } else {
s.server.semaphore[SemaphoreID] = NewSemaphore(s, SemaphoreID, 1) s.server.semaphore[SemaphoreID] = NewSemaphore(s, SemaphoreID, 1)

View File

@@ -394,6 +394,8 @@ func handleMsgSysIssueLogkey(s *Session, p mhfpacket.MHFPacket) {
doAckBufSucceed(s, pkt.AckHandle, resp.Data()) doAckBufSucceed(s, pkt.AckHandle, resp.Data())
} }
const localhostAddrLE = uint32(0x0100007F) // 127.0.0.1 in little-endian
// Kill log binary layout constants // Kill log binary layout constants
const ( const (
killLogHeaderSize = 32 // bytes before monster kill count array killLogHeaderSize = 32 // bytes before monster kill count array
@@ -557,7 +559,7 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) {
if !local { if !local {
resp.WriteUint32(binary.LittleEndian.Uint32(r.ip)) resp.WriteUint32(binary.LittleEndian.Uint32(r.ip))
} else { } else {
resp.WriteUint32(0x0100007F) resp.WriteUint32(localhostAddrLE)
} }
resp.WriteUint16(r.port) resp.WriteUint16(r.port)
resp.WriteUint32(r.charID) resp.WriteUint32(r.charID)
@@ -757,7 +759,7 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) {
if !local { if !local {
resp.WriteUint32(binary.LittleEndian.Uint32(sr.ip)) resp.WriteUint32(binary.LittleEndian.Uint32(sr.ip))
} else { } else {
resp.WriteUint32(0x0100007F) resp.WriteUint32(localhostAddrLE)
} }
resp.WriteUint16(sr.port) resp.WriteUint16(sr.port)

View File

@@ -312,7 +312,7 @@ func (s *Server) BroadcastChatMessage(message string) {
msgBinChat := &binpacket.MsgBinChat{ msgBinChat := &binpacket.MsgBinChat{
Unk0: 0, Unk0: 0,
Type: 5, Type: 5,
Flags: 0x80, Flags: chatFlagServer,
Message: message, Message: message,
SenderName: s.name, SenderName: s.name,
} }
@@ -420,8 +420,15 @@ func (s *Server) HasSemaphore(ses *Session) bool {
return false 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. // Season returns the current in-game season (0-2) based on server ID and time.
func (s *Server) Season() uint8 { func (s *Server) Season() uint8 {
sid := int64(((s.ID & 0xFF00) - 4096) / 256) sid := int64(((s.ID & serverIDHighMask) - serverIDBase) / serverIDStride)
return uint8(((TimeAdjusted().Unix() / 86400) + sid) % 3) return uint8(((TimeAdjusted().Unix() / secsPerDay) + sid) % 3)
} }