mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
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:
@@ -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
|
||||
|
||||
14
server/channelserver/constants_raviente.go
Normal file
14
server/channelserver/constants_raviente.go
Normal 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
|
||||
)
|
||||
7
server/channelserver/constants_time.go
Normal file
7
server/channelserver/constants_time.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package channelserver
|
||||
|
||||
// Shared time duration constants (seconds)
|
||||
const (
|
||||
secsPerDay = 86400 // 24 hours
|
||||
secsPerWeek = 604800 // 7 days
|
||||
)
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user