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
// 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

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) {
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))
}

View File

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

View File

@@ -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",
}

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
// 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) {

View File

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

View File

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

View File

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