mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-26 17:43:21 +01:00
refactor(channelserver): migrate inline queries to helpers and define named constants
Migrate 6 character data handlers to use the existing loadCharacterData and saveCharacterData helpers, eliminating duplicate inline SQL: - LoadFavoriteQuest, SaveFavoriteQuest, LoadDecoMyset, LoadMezfesData, LoadHunterNavi, GetEquipSkinHist Define named constants replacing magic numbers across handlers: - Achievement trophy tiers, broadcast/message types, diva phase durations, RP accrual rates, kill log layout, semaphore bases, quest stage/loading screen IDs Update anti-patterns doc with accurate line counts, evidence-based softlock analysis, and revised refactoring priorities.
This commit is contained in:
@@ -23,37 +23,90 @@
|
|||||||
|
|
||||||
## 1. God Files — Massive Handler Files
|
## 1. God Files — Massive Handler Files
|
||||||
|
|
||||||
The channel server has enormous files with thousands of lines, each mixing DB queries, business logic, binary serialization, and response writing with no layering.
|
The channel server has large handler files, each mixing DB queries, business logic, binary serialization, and response writing with no layering. Actual line counts (non-test files):
|
||||||
|
|
||||||
| File | Approx. Lines | Purpose |
|
| File | Lines | Purpose |
|
||||||
|------|---------------|---------|
|
|------|-------|---------|
|
||||||
| `server/channelserver/handlers_guild.go` | ~2000+ | Guild operations |
|
| `server/channelserver/handlers_session.go` | 794 | Session setup/teardown |
|
||||||
| `server/channelserver/handlers_mail.go` | ~1200+ | Mail system |
|
| `server/channelserver/handlers_data_paper_tables.go` | 765 | Paper table data |
|
||||||
| `server/channelserver/handlers_data.go` | ~800+ | Data save/load |
|
| `server/channelserver/handlers_quest.go` | 722 | Quest lifecycle |
|
||||||
| `server/channelserver/handlers_cast_binary.go` | ~500+ | Binary relay |
|
| `server/channelserver/handlers_house.go` | 638 | Housing system |
|
||||||
| `server/channelserver/sys_session.go` | ~500+ | Session lifecycle |
|
| `server/channelserver/handlers_festa.go` | 637 | Festival events |
|
||||||
|
| `server/channelserver/handlers_data_paper.go` | 621 | Paper/data system |
|
||||||
|
| `server/channelserver/handlers_tower.go` | 529 | Tower gameplay |
|
||||||
|
| `server/channelserver/handlers_mercenary.go` | 495 | Mercenary system |
|
||||||
|
| `server/channelserver/handlers_stage.go` | 492 | Stage/lobby management |
|
||||||
|
| `server/channelserver/handlers_guild_info.go` | 473 | Guild info queries |
|
||||||
|
|
||||||
**Impact:** These files are difficult to navigate, review, and maintain. A change to guild mail logic requires working through a 2000-line file that also handles guild creation, management, and recruitment.
|
These sizes (~500-800 lines) are not extreme by Go standards, but the files mix all architectural concerns. The bigger problem is the lack of layering within each file (see [#3](#3-no-architectural-layering--handlers-do-everything)), not the file sizes themselves.
|
||||||
|
|
||||||
|
**Impact:** Each handler function is a monolith mixing data access, business logic, and protocol serialization. Testing or reusing any single concern is impossible.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Silently Swallowed Errors
|
## 2. Missing ACK Responses on Error Paths (Client Softlocks)
|
||||||
|
|
||||||
This is the most pervasive anti-pattern. The dominant error handling pattern across nearly every `handlers_*.go` file is:
|
Some handler error paths log the error and return without sending any ACK response to the client. The MHF client uses `MsgSysAck` with an `ErrorCode` field (0 = success, 1 = failure) to complete request/response cycles. When no ACK is sent at all, the client softlocks waiting for a response that never arrives.
|
||||||
|
|
||||||
|
### The three error handling patterns in the codebase
|
||||||
|
|
||||||
|
**Pattern A — Silent return (the bug):** Error logged, no ACK sent, client hangs.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
rows, err := s.Server.DB.Query(...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to get ...", zap.Error(err))
|
s.logger.Error("Failed to get ...", zap.Error(err))
|
||||||
return // client gets no response, silently fails
|
return // BUG: client gets no response, softlocks
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Errors are logged server-side but the client receives no error response. The client is left hanging or receives incomplete data with no indication of failure.
|
**Pattern B — Log and continue (acceptable):** Error logged, handler continues and sends a success ACK with default/empty data. The client proceeds with fallback behavior.
|
||||||
|
|
||||||
**Impact:** Client-side debugging is extremely difficult. Players experience mysterious failures with no feedback. Error recovery is impossible since the client doesn't know something went wrong.
|
```go
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to load mezfes data", zap.Error(err))
|
||||||
|
}
|
||||||
|
// Falls through to doAckBufSucceed with empty data
|
||||||
|
```
|
||||||
|
|
||||||
**Recommendation:** Define error response packets or at least send a generic failure response to the client before returning.
|
**Pattern C — Fail ACK (correct):** Error logged, explicit fail ACK sent. The client shows an appropriate error dialog and stays connected.
|
||||||
|
|
||||||
|
```go
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("Failed to read rengoku_data.bin", zap.Error(err))
|
||||||
|
doAckBufFail(s, pkt.AckHandle, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Evidence that fail ACKs are safe
|
||||||
|
|
||||||
|
The codebase already sends ~70 `doAckSimpleFail`/`doAckBufFail` calls in production handler code across 15 files. The client handles them gracefully in all observed cases:
|
||||||
|
|
||||||
|
| File | Fail ACKs | Client behavior |
|
||||||
|
|------|-----------|-----------------|
|
||||||
|
| `handlers_guild_scout.go` | 17 | Guild recruitment error dialogs |
|
||||||
|
| `handlers_guild_ops.go` | 10 | Permission denied, guild not found dialogs |
|
||||||
|
| `handlers_stage.go` | 8 | "Room is full", "wrong password", "stage locked" |
|
||||||
|
| `handlers_house.go` | 6 | Wrong password, invalid box index |
|
||||||
|
| `handlers_guild.go` | 9 | Guild icon update errors, unimplemented features |
|
||||||
|
| `handlers_guild_alliance.go` | 4 | Alliance permission errors |
|
||||||
|
| `handlers_data.go` | 4 | Decompression failures, oversized payloads |
|
||||||
|
| `handlers_festa.go` | 4 | Festival entry errors |
|
||||||
|
| `handlers_quest.go` | 3 | Missing quest/scenario files |
|
||||||
|
|
||||||
|
A comment in `handlers_quest.go:188` explicitly documents the mechanism:
|
||||||
|
|
||||||
|
> sends doAckBufFail, which triggers the client's error dialog (snj_questd_matching_fail → SetDialogData) instead of a softlock
|
||||||
|
|
||||||
|
The original `mhfo-hd.dll` client reads the `ErrorCode` byte from `MsgSysAck` and dispatches to per-message error UI. A fail ACK causes the client to show an error dialog and remain functional. A missing ACK causes a softlock.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
A preliminary grep for `logger.Error` followed by bare `return` (no doAck call) found instances across ~25 handler files. The worst offenders are `handlers_festa.go`, `handlers_gacha.go`, `handlers_cafe.go`, and `handlers_house.go`. However, many of these are Pattern B (log-and-continue), not Pattern A. Each instance needs individual review to determine whether an ACK is already sent further down the function.
|
||||||
|
|
||||||
|
**Impact:** Players experience softlocks on error paths that could instead show an error dialog and let them continue playing.
|
||||||
|
|
||||||
|
**Recommendation:** Audit each silent-return error path. For handlers where the packet has an `AckHandle` and no ACK is sent on the error path, add `doAckSimpleFail`/`doAckBufFail` matching the ACK type used on the success path. This matches the existing pattern used in ~70 other error paths across the codebase.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -300,9 +353,9 @@ Database operations use raw `database/sql` with PostgreSQL-specific syntax throu
|
|||||||
|
|
||||||
| Severity | Anti-patterns |
|
| Severity | Anti-patterns |
|
||||||
|----------|--------------|
|
|----------|--------------|
|
||||||
| **High** | No architectural layering (#3), silently swallowed errors (#2), god files (#1), tight DB coupling (#13) |
|
| **High** | Missing ACK responses / softlocks (#2), no architectural layering (#3), tight DB coupling (#13) |
|
||||||
| **Medium** | Magic numbers (#4), inconsistent binary I/O (#5), Session god object (#6), copy-paste handlers (#8) |
|
| **Medium** | Magic numbers (#4), inconsistent binary I/O (#5), Session god object (#6), copy-paste handlers (#8), raw SQL duplication (#9) |
|
||||||
| **Low** | `init()` registration (#10), inconsistent logging (#12), mutex granularity (#7), panic-based flow (#11) |
|
| **Low** | God files (#1), `init()` registration (#10), inconsistent logging (#12), mutex granularity (#7), panic-based flow (#11) |
|
||||||
|
|
||||||
### Root Cause
|
### Root Cause
|
||||||
|
|
||||||
@@ -310,8 +363,9 @@ Most of these anti-patterns stem from a single root cause: **the codebase grew o
|
|||||||
|
|
||||||
### Recommended Refactoring Priority
|
### Recommended Refactoring Priority
|
||||||
|
|
||||||
1. **Introduce error responses to clients** — highest user-facing impact, can be done incrementally
|
1. **Add fail ACKs to silent error paths** — prevents player softlocks, ~70 existing doAckFail calls prove safety, low risk, can be done handler-by-handler
|
||||||
2. **Extract a repository layer** — decouple SQL from handlers, enable testing
|
2. **Extract a character repository layer** — 152 queries across 26 files touch the `characters` table, highest SQL duplication
|
||||||
3. **Define protocol constants** — replace magic numbers, improve documentation
|
3. **Extract load/save helpers** — 38 handlers repeat the same ~10-15 line template, mechanical extraction
|
||||||
4. **Standardize binary I/O** — pick one approach, migrate the rest
|
4. **Extract a guild repository layer** — 32 queries across 8-15 files, second-highest SQL duplication
|
||||||
5. **Split god files** — break handlers into sub-packages by domain (guild/, mail/, quest/)
|
5. **Define protocol constants** — 1,052 hex literals with 174 unique values, improves documentation
|
||||||
|
6. **Standardize binary I/O** — pick `byteframe` (already dominant), migrate remaining `binary.Write` and raw slice code
|
||||||
|
|||||||
62
server/channelserver/constants_quest.go
Normal file
62
server/channelserver/constants_quest.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package channelserver
|
||||||
|
|
||||||
|
// Raviente quest type codes
|
||||||
|
const (
|
||||||
|
QuestTypeSpecialTool = uint8(9)
|
||||||
|
QuestTypeRegularRaviente = uint8(16)
|
||||||
|
QuestTypeViolentRaviente = uint8(22)
|
||||||
|
QuestTypeBerserkRaviente = uint8(40)
|
||||||
|
QuestTypeExtremeRaviente = uint8(50)
|
||||||
|
QuestTypeSmallBerserkRavi = uint8(51)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Event quest binary frame offsets
|
||||||
|
const (
|
||||||
|
questFrameTimeFlagOffset = 25
|
||||||
|
questFrameVariant3Offset = 175
|
||||||
|
)
|
||||||
|
|
||||||
|
// Quest body lengths per game version
|
||||||
|
const (
|
||||||
|
questBodyLenS6 = 160
|
||||||
|
questBodyLenF5 = 168
|
||||||
|
questBodyLenG101 = 192
|
||||||
|
questBodyLenZ1 = 224
|
||||||
|
questBodyLenZZ = 320
|
||||||
|
)
|
||||||
|
|
||||||
|
// BackportQuest constants
|
||||||
|
const (
|
||||||
|
questRewardTableBase = uint32(96)
|
||||||
|
questStringPointerOff = 40
|
||||||
|
questStringTablePadding = 32
|
||||||
|
questStringCount = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
// BackportQuest fill lengths per version
|
||||||
|
const (
|
||||||
|
questBackportFillS6 = uint32(44)
|
||||||
|
questBackportFillF5 = uint32(52)
|
||||||
|
questBackportFillG101 = uint32(76)
|
||||||
|
questBackportFillZZ = uint32(108)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tune value count limits per game version
|
||||||
|
const (
|
||||||
|
tuneLimitG1 = 256
|
||||||
|
tuneLimitG3 = 283
|
||||||
|
tuneLimitGG = 315
|
||||||
|
tuneLimitG61 = 332
|
||||||
|
tuneLimitG7 = 339
|
||||||
|
tuneLimitG81 = 396
|
||||||
|
tuneLimitG91 = 694
|
||||||
|
tuneLimitG101 = 704
|
||||||
|
tuneLimitZ2 = 750
|
||||||
|
tuneLimitZZ = 770
|
||||||
|
)
|
||||||
|
|
||||||
|
// Event quest data size bounds
|
||||||
|
const (
|
||||||
|
questDataMaxLen = 896
|
||||||
|
questDataMinLen = 352
|
||||||
|
)
|
||||||
@@ -9,6 +9,13 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Achievement trophy tier thresholds (bitfield values)
|
||||||
|
const (
|
||||||
|
AchievementTrophyBronze = uint8(0x40)
|
||||||
|
AchievementTrophySilver = uint8(0x60)
|
||||||
|
AchievementTrophyGold = uint8(0x7F)
|
||||||
|
)
|
||||||
|
|
||||||
var achievementCurves = [][]int32{
|
var achievementCurves = [][]int32{
|
||||||
// 0: HR weapon use, Class use, Tore dailies
|
// 0: HR weapon use, Class use, Tore dailies
|
||||||
{5, 15, 30, 50, 100, 150, 200, 300},
|
{5, 15, 30, 50, 100, 150, 200, 300},
|
||||||
@@ -61,10 +68,10 @@ func GetAchData(id uint8, score int32) Achievement {
|
|||||||
ach.NextValue = 15
|
ach.NextValue = 15
|
||||||
case 6:
|
case 6:
|
||||||
ach.NextValue = 15
|
ach.NextValue = 15
|
||||||
ach.Trophy = 0x40
|
ach.Trophy = AchievementTrophyBronze
|
||||||
case 7:
|
case 7:
|
||||||
ach.NextValue = 20
|
ach.NextValue = 20
|
||||||
ach.Trophy = 0x60
|
ach.Trophy = AchievementTrophySilver
|
||||||
}
|
}
|
||||||
return ach
|
return ach
|
||||||
} else {
|
} else {
|
||||||
@@ -83,7 +90,7 @@ func GetAchData(id uint8, score int32) Achievement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ach.Required = uint32(curve[7])
|
ach.Required = uint32(curve[7])
|
||||||
ach.Trophy = 0x7F
|
ach.Trophy = AchievementTrophyGold
|
||||||
ach.Progress = ach.Required
|
ach.Progress = ach.Required
|
||||||
return ach
|
return ach
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ 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 == 0x03 && pkt.MessageType == 0x03 && len(pkt.RawDataPayload) == 0x10 {
|
if pkt.BroadcastType == BroadcastTypeStage && pkt.MessageType == BinaryMessageTypeData && len(pkt.RawDataPayload) == 0x10 {
|
||||||
if tmp.ReadUint16() == 0x0002 && tmp.ReadUint8() == 0x18 {
|
if tmp.ReadUint16() == 0x0002 && tmp.ReadUint8() == 0x18 {
|
||||||
var timer bool
|
var timer bool
|
||||||
if err := s.server.db.QueryRow(`SELECT COALESCE(timer, false) FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, s.charID).Scan(&timer); err != nil {
|
if err := s.server.db.QueryRow(`SELECT COALESCE(timer, false) FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, s.charID).Scan(&timer); err != nil {
|
||||||
@@ -50,7 +50,7 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if s.server.erupeConfig.DebugOptions.QuestTools {
|
if s.server.erupeConfig.DebugOptions.QuestTools {
|
||||||
if pkt.BroadcastType == 0x03 && pkt.MessageType == 0x02 && len(pkt.RawDataPayload) > 32 {
|
if pkt.BroadcastType == BroadcastTypeStage && pkt.MessageType == BinaryMessageTypeQuest && len(pkt.RawDataPayload) > 32 {
|
||||||
// This is only correct most of the time
|
// This is only correct most of the time
|
||||||
tmp.ReadBytes(20)
|
tmp.ReadBytes(20)
|
||||||
tmp.SetLE()
|
tmp.SetLE()
|
||||||
@@ -131,7 +131,7 @@ func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
s.stage.BroadcastMHF(resp, s)
|
s.stage.BroadcastMHF(resp, s)
|
||||||
}
|
}
|
||||||
case BroadcastTypeServer:
|
case BroadcastTypeServer:
|
||||||
if pkt.MessageType == 1 {
|
if pkt.MessageType == BinaryMessageTypeChat {
|
||||||
raviSema := s.server.getRaviSemaphore()
|
raviSema := s.server.getRaviSemaphore()
|
||||||
if raviSema != nil {
|
if raviSema != nil {
|
||||||
raviSema.BroadcastMHF(resp, s)
|
raviSema.BroadcastMHF(resp, s)
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Diva Defense event duration constants (all values in seconds)
|
||||||
|
const (
|
||||||
|
divaPhaseDuration = 601200 // 6d 23h = first song phase
|
||||||
|
divaInterlude = 3900 // 65 min = gap between phases
|
||||||
|
divaWeekDuration = 604800 // 7 days = subsequent phase length
|
||||||
|
divaTotalLifespan = 2977200 // ~34.5 days = full event window
|
||||||
|
)
|
||||||
|
|
||||||
func cleanupDiva(s *Session) {
|
func cleanupDiva(s *Session) {
|
||||||
if _, err := s.server.db.Exec("DELETE FROM events WHERE event_type='diva'"); err != nil {
|
if _, err := s.server.db.Exec("DELETE FROM events WHERE event_type='diva'"); err != nil {
|
||||||
s.logger.Error("Failed to delete diva events", zap.Error(err))
|
s.logger.Error("Failed to delete diva events", zap.Error(err))
|
||||||
@@ -25,29 +33,29 @@ func generateDivaTimestamps(s *Session, start uint32, debug bool) []uint32 {
|
|||||||
switch start {
|
switch start {
|
||||||
case 1:
|
case 1:
|
||||||
timestamps[0] = midnight
|
timestamps[0] = midnight
|
||||||
timestamps[1] = timestamps[0] + 601200
|
timestamps[1] = timestamps[0] + divaPhaseDuration
|
||||||
timestamps[2] = timestamps[1] + 3900
|
timestamps[2] = timestamps[1] + divaInterlude
|
||||||
timestamps[3] = timestamps[1] + 604800
|
timestamps[3] = timestamps[1] + divaWeekDuration
|
||||||
timestamps[4] = timestamps[3] + 3900
|
timestamps[4] = timestamps[3] + divaInterlude
|
||||||
timestamps[5] = timestamps[3] + 604800
|
timestamps[5] = timestamps[3] + divaWeekDuration
|
||||||
case 2:
|
case 2:
|
||||||
timestamps[0] = midnight - 605100
|
timestamps[0] = midnight - (divaPhaseDuration + divaInterlude)
|
||||||
timestamps[1] = midnight - 3900
|
timestamps[1] = midnight - divaInterlude
|
||||||
timestamps[2] = midnight
|
timestamps[2] = midnight
|
||||||
timestamps[3] = timestamps[1] + 604800
|
timestamps[3] = timestamps[1] + divaWeekDuration
|
||||||
timestamps[4] = timestamps[3] + 3900
|
timestamps[4] = timestamps[3] + divaInterlude
|
||||||
timestamps[5] = timestamps[3] + 604800
|
timestamps[5] = timestamps[3] + divaWeekDuration
|
||||||
case 3:
|
case 3:
|
||||||
timestamps[0] = midnight - 1213800
|
timestamps[0] = midnight - (divaPhaseDuration + divaInterlude + divaWeekDuration + divaInterlude)
|
||||||
timestamps[1] = midnight - 608700
|
timestamps[1] = midnight - (divaWeekDuration + divaInterlude)
|
||||||
timestamps[2] = midnight - 604800
|
timestamps[2] = midnight - divaWeekDuration
|
||||||
timestamps[3] = midnight - 3900
|
timestamps[3] = midnight - divaInterlude
|
||||||
timestamps[4] = midnight
|
timestamps[4] = midnight
|
||||||
timestamps[5] = timestamps[3] + 604800
|
timestamps[5] = timestamps[3] + divaWeekDuration
|
||||||
}
|
}
|
||||||
return timestamps
|
return timestamps
|
||||||
}
|
}
|
||||||
if start == 0 || TimeAdjusted().Unix() > int64(start)+2977200 {
|
if start == 0 || TimeAdjusted().Unix() > int64(start)+divaTotalLifespan {
|
||||||
cleanupDiva(s)
|
cleanupDiva(s)
|
||||||
// Generate a new diva defense, starting midnight tomorrow
|
// Generate a new diva defense, starting midnight tomorrow
|
||||||
start = uint32(midnight.Add(24 * time.Hour).Unix())
|
start = uint32(midnight.Add(24 * time.Hour).Unix())
|
||||||
@@ -56,11 +64,11 @@ func generateDivaTimestamps(s *Session, start uint32, debug bool) []uint32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
timestamps[0] = start
|
timestamps[0] = start
|
||||||
timestamps[1] = timestamps[0] + 601200
|
timestamps[1] = timestamps[0] + divaPhaseDuration
|
||||||
timestamps[2] = timestamps[1] + 3900
|
timestamps[2] = timestamps[1] + divaInterlude
|
||||||
timestamps[3] = timestamps[1] + 604800
|
timestamps[3] = timestamps[1] + divaWeekDuration
|
||||||
timestamps[4] = timestamps[3] + 3900
|
timestamps[4] = timestamps[3] + divaInterlude
|
||||||
timestamps[5] = timestamps[3] + 604800
|
timestamps[5] = timestamps[3] + divaWeekDuration
|
||||||
return timestamps
|
return timestamps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,21 +22,8 @@ func handleMsgMhfSaveMezfesData(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
|
|
||||||
func handleMsgMhfLoadMezfesData(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfLoadMezfesData(s *Session, p mhfpacket.MHFPacket) {
|
||||||
pkt := p.(*mhfpacket.MsgMhfLoadMezfesData)
|
pkt := p.(*mhfpacket.MsgMhfLoadMezfesData)
|
||||||
var data []byte
|
loadCharacterData(s, pkt.AckHandle, "mezfes",
|
||||||
if err := s.server.db.QueryRow(`SELECT mezfes FROM characters WHERE id=$1`, s.charID).Scan(&data); err != nil && !errors.Is(err, sql.ErrNoRows) {
|
[]byte{0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
|
||||||
s.logger.Error("Failed to load mezfes data", zap.Error(err))
|
|
||||||
}
|
|
||||||
bf := byteframe.NewByteFrame()
|
|
||||||
if len(data) > 0 {
|
|
||||||
bf.WriteBytes(data)
|
|
||||||
} else {
|
|
||||||
bf.WriteUint32(0)
|
|
||||||
bf.WriteUint8(2)
|
|
||||||
bf.WriteUint32(0)
|
|
||||||
bf.WriteUint32(0)
|
|
||||||
bf.WriteUint32(0)
|
|
||||||
}
|
|
||||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMsgMhfEnumerateRanking(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfEnumerateRanking(s *Session, p mhfpacket.MHFPacket) {
|
||||||
|
|||||||
@@ -271,18 +271,11 @@ func handleMsgMhfUpdateMyhouseInfo(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
|
|
||||||
func handleMsgMhfLoadDecoMyset(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfLoadDecoMyset(s *Session, p mhfpacket.MHFPacket) {
|
||||||
pkt := p.(*mhfpacket.MsgMhfLoadDecoMyset)
|
pkt := p.(*mhfpacket.MsgMhfLoadDecoMyset)
|
||||||
var data []byte
|
defaultData := []byte{0x01, 0x00}
|
||||||
err := s.server.db.QueryRow("SELECT decomyset FROM characters WHERE id = $1", s.charID).Scan(&data)
|
if s.server.erupeConfig.RealClientMode < _config.G10 {
|
||||||
if err != nil {
|
defaultData = []byte{0x00, 0x00}
|
||||||
s.logger.Error("Failed to load decomyset", zap.Error(err))
|
|
||||||
}
|
}
|
||||||
if len(data) == 0 {
|
loadCharacterData(s, pkt.AckHandle, "decomyset", defaultData)
|
||||||
data = []byte{0x01, 0x00}
|
|
||||||
if s.server.erupeConfig.RealClientMode < _config.G10 {
|
|
||||||
data = []byte{0x00, 0x00}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
doAckBufSucceed(s, pkt.AckHandle, data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMsgMhfSaveDecoMyset(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfSaveDecoMyset(s *Session, p mhfpacket.MHFPacket) {
|
||||||
|
|||||||
@@ -47,13 +47,7 @@ func handleMsgMhfLoadHunterNavi(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
if s.server.erupeConfig.RealClientMode <= _config.G7 {
|
if s.server.erupeConfig.RealClientMode <= _config.G7 {
|
||||||
naviLength = 280
|
naviLength = 280
|
||||||
}
|
}
|
||||||
var data []byte
|
loadCharacterData(s, pkt.AckHandle, "hunternavi", make([]byte, naviLength))
|
||||||
err := s.server.db.QueryRow("SELECT hunternavi FROM characters WHERE id = $1", s.charID).Scan(&data)
|
|
||||||
if len(data) == 0 {
|
|
||||||
s.logger.Error("Failed to load hunternavi", zap.Error(err))
|
|
||||||
data = make([]byte, naviLength)
|
|
||||||
}
|
|
||||||
doAckBufSucceed(s, pkt.AckHandle, data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMsgMhfSaveHunterNavi(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfSaveHunterNavi(s *Session, p mhfpacket.MHFPacket) {
|
||||||
|
|||||||
@@ -170,13 +170,7 @@ func equipSkinHistSize(mode _config.Mode) int {
|
|||||||
func handleMsgMhfGetEquipSkinHist(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfGetEquipSkinHist(s *Session, p mhfpacket.MHFPacket) {
|
||||||
pkt := p.(*mhfpacket.MsgMhfGetEquipSkinHist)
|
pkt := p.(*mhfpacket.MsgMhfGetEquipSkinHist)
|
||||||
size := equipSkinHistSize(s.server.erupeConfig.RealClientMode)
|
size := equipSkinHistSize(s.server.erupeConfig.RealClientMode)
|
||||||
var data []byte
|
loadCharacterData(s, pkt.AckHandle, "skin_hist", make([]byte, size))
|
||||||
err := s.server.db.QueryRow("SELECT COALESCE(skin_hist::bytea, $2::bytea) FROM characters WHERE id = $1", s.charID, make([]byte, size)).Scan(&data)
|
|
||||||
if err != nil {
|
|
||||||
s.logger.Error("Failed to load skin_hist", zap.Error(err))
|
|
||||||
data = make([]byte, size)
|
|
||||||
}
|
|
||||||
doAckBufSucceed(s, pkt.AckHandle, data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMsgMhfUpdateEquipSkinHist(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfUpdateEquipSkinHist(s *Session, p mhfpacket.MHFPacket) {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func equal(a, b []byte) bool {
|
|||||||
|
|
||||||
// BackportQuest converts a quest binary to an older format.
|
// BackportQuest converts a quest binary to an older format.
|
||||||
func BackportQuest(data []byte, mode _config.Mode) []byte {
|
func BackportQuest(data []byte, mode _config.Mode) []byte {
|
||||||
wp := binary.LittleEndian.Uint32(data[0:4]) + 96
|
wp := binary.LittleEndian.Uint32(data[0:4]) + questRewardTableBase
|
||||||
rp := wp + 4
|
rp := wp + 4
|
||||||
for i := uint32(0); i < 6; i++ {
|
for i := uint32(0); i < 6; i++ {
|
||||||
if i != 0 {
|
if i != 0 {
|
||||||
@@ -60,13 +60,13 @@ func BackportQuest(data []byte, mode _config.Mode) []byte {
|
|||||||
copy(data[wp:wp+4], data[rp:rp+4])
|
copy(data[wp:wp+4], data[rp:rp+4])
|
||||||
}
|
}
|
||||||
|
|
||||||
fillLength := uint32(108)
|
fillLength := questBackportFillZZ
|
||||||
if mode <= _config.S6 {
|
if mode <= _config.S6 {
|
||||||
fillLength = 44
|
fillLength = questBackportFillS6
|
||||||
} else if mode <= _config.F5 {
|
} else if mode <= _config.F5 {
|
||||||
fillLength = 52
|
fillLength = questBackportFillF5
|
||||||
} else if mode <= _config.G101 {
|
} else if mode <= _config.G101 {
|
||||||
fillLength = 76
|
fillLength = questBackportFillG101
|
||||||
}
|
}
|
||||||
|
|
||||||
copy(data[wp:wp+fillLength], data[rp:rp+fillLength])
|
copy(data[wp:wp+fillLength], data[rp:rp+fillLength])
|
||||||
@@ -195,27 +195,13 @@ func seasonConversion(s *Session, questFile string) string {
|
|||||||
|
|
||||||
func handleMsgMhfLoadFavoriteQuest(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfLoadFavoriteQuest(s *Session, p mhfpacket.MHFPacket) {
|
||||||
pkt := p.(*mhfpacket.MsgMhfLoadFavoriteQuest)
|
pkt := p.(*mhfpacket.MsgMhfLoadFavoriteQuest)
|
||||||
var data []byte
|
loadCharacterData(s, pkt.AckHandle, "savefavoritequest",
|
||||||
err := s.server.db.QueryRow("SELECT savefavoritequest FROM characters WHERE id = $1", s.charID).Scan(&data)
|
[]byte{0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
|
||||||
if err == nil && len(data) > 0 {
|
|
||||||
doAckBufSucceed(s, pkt.AckHandle, data)
|
|
||||||
} else {
|
|
||||||
doAckBufSucceed(s, pkt.AckHandle, []byte{0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMsgMhfSaveFavoriteQuest(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfSaveFavoriteQuest(s *Session, p mhfpacket.MHFPacket) {
|
||||||
pkt := p.(*mhfpacket.MsgMhfSaveFavoriteQuest)
|
pkt := p.(*mhfpacket.MsgMhfSaveFavoriteQuest)
|
||||||
if len(pkt.Data) > 65536 {
|
saveCharacterData(s, pkt.AckHandle, "savefavoritequest", pkt.Data, 65536)
|
||||||
s.logger.Warn("FavoriteQuest payload too large", zap.Int("len", len(pkt.Data)))
|
|
||||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dumpSaveData(s, pkt.Data, "favquest")
|
|
||||||
if _, err := s.server.db.Exec("UPDATE characters SET savefavoritequest=$1 WHERE id=$2", pkt.Data, s.charID); err != nil {
|
|
||||||
s.logger.Error("Failed to save favorite quest", zap.Error(err))
|
|
||||||
}
|
|
||||||
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadQuestFile(s *Session, questId int) []byte {
|
func loadQuestFile(s *Session, questId int) []byte {
|
||||||
|
|||||||
@@ -237,14 +237,14 @@ func logoutPlayer(s *Session) {
|
|||||||
timePlayed += sessionTime
|
timePlayed += sessionTime
|
||||||
|
|
||||||
if mhfcourse.CourseExists(30, s.courses) {
|
if mhfcourse.CourseExists(30, s.courses) {
|
||||||
rpGained = timePlayed / 900
|
rpGained = timePlayed / rpAccrualCafe
|
||||||
timePlayed = timePlayed % 900
|
timePlayed = timePlayed % rpAccrualCafe
|
||||||
if _, err := s.server.db.Exec("UPDATE characters SET cafe_time=cafe_time+$1 WHERE id=$2", sessionTime, s.charID); err != nil {
|
if _, err := s.server.db.Exec("UPDATE characters SET cafe_time=cafe_time+$1 WHERE id=$2", sessionTime, s.charID); err != nil {
|
||||||
s.logger.Error("Failed to update cafe time", zap.Error(err))
|
s.logger.Error("Failed to update cafe time", zap.Error(err))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
rpGained = timePlayed / 1800
|
rpGained = timePlayed / rpAccrualNormal
|
||||||
timePlayed = timePlayed % 1800
|
timePlayed = timePlayed % rpAccrualNormal
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Debug("Session metrics calculated",
|
s.logger.Debug("Session metrics calculated",
|
||||||
@@ -386,13 +386,25 @@ func handleMsgSysIssueLogkey(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
doAckBufSucceed(s, pkt.AckHandle, resp.Data())
|
doAckBufSucceed(s, pkt.AckHandle, resp.Data())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kill log binary layout constants
|
||||||
|
const (
|
||||||
|
killLogHeaderSize = 32 // bytes before monster kill count array
|
||||||
|
killLogMonsterCount = 176 // monster table entries
|
||||||
|
)
|
||||||
|
|
||||||
|
// RP accrual rate constants (seconds per RP point)
|
||||||
|
const (
|
||||||
|
rpAccrualNormal = 1800 // 30 min per RP without cafe
|
||||||
|
rpAccrualCafe = 900 // 15 min per RP with cafe course
|
||||||
|
)
|
||||||
|
|
||||||
func handleMsgSysRecordLog(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgSysRecordLog(s *Session, p mhfpacket.MHFPacket) {
|
||||||
pkt := p.(*mhfpacket.MsgSysRecordLog)
|
pkt := p.(*mhfpacket.MsgSysRecordLog)
|
||||||
if s.server.erupeConfig.RealClientMode == _config.ZZ {
|
if s.server.erupeConfig.RealClientMode == _config.ZZ {
|
||||||
bf := byteframe.NewByteFrameFromBytes(pkt.Data)
|
bf := byteframe.NewByteFrameFromBytes(pkt.Data)
|
||||||
_, _ = bf.Seek(32, 0)
|
_, _ = bf.Seek(killLogHeaderSize, 0)
|
||||||
var val uint8
|
var val uint8
|
||||||
for i := 0; i < 176; i++ {
|
for i := 0; i < killLogMonsterCount; i++ {
|
||||||
val = bf.ReadUint8()
|
val = bf.ReadUint8()
|
||||||
if val > 0 && mhfmon.Monsters[i].Large {
|
if val > 0 && mhfmon.Monsters[i].Large {
|
||||||
if _, err := s.server.db.Exec(`INSERT INTO kill_logs (character_id, monster, quantity, timestamp) VALUES ($1, $2, $3, $4)`, s.charID, i, val, TimeAdjusted()); err != nil {
|
if _, err := s.server.db.Exec(`INSERT INTO kill_logs (character_id, monster, quantity, timestamp) VALUES ($1, $2, $3, $4)`, s.charID, i, val, TimeAdjusted()); err != nil {
|
||||||
|
|||||||
@@ -327,12 +327,18 @@ func (s *Session) getObjectId() uint32 {
|
|||||||
return uint32(s.objectID)<<16 | uint32(s.objectIndex)
|
return uint32(s.objectID)<<16 | uint32(s.objectIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Semaphore ID base values
|
||||||
|
const (
|
||||||
|
semaphoreBaseDefault = uint32(0x000F0000)
|
||||||
|
semaphoreBaseAlt = uint32(0x000E0000)
|
||||||
|
)
|
||||||
|
|
||||||
// GetSemaphoreID returns the semaphore ID held by the session, varying by semaphore mode.
|
// GetSemaphoreID returns the semaphore ID held by the session, varying by semaphore mode.
|
||||||
func (s *Session) GetSemaphoreID() uint32 {
|
func (s *Session) GetSemaphoreID() uint32 {
|
||||||
if s.semaphoreMode {
|
if s.semaphoreMode {
|
||||||
return 0x000E0000 + uint32(s.semaphoreID[1])
|
return semaphoreBaseAlt + uint32(s.semaphoreID[1])
|
||||||
} else {
|
} else {
|
||||||
return 0x000F0000 + uint32(s.semaphoreID[0])
|
return semaphoreBaseDefault + uint32(s.semaphoreID[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user