mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
fix(channelserver): correct session handler retail mismatches (#167)
Lobby search now returns only quest-bound players (QuestReserved) instead of all reserved slots, matching retail behavior. The new field is pre-collected under server lock before stage iteration to respect Server.Mutex → Stage.RWMutex lock ordering. Replaced three TODOs with RE documentation from Ghidra decompilation of mhfo-hd.dll ZZ: - Log key off-by-one: putRecord_log/putTerminal_log pass size 0 for the key field in ZZ, so the stored key is unused beyond issuance - User search padding: ZZ per-entry parser confirms 40-byte block via memcpy(dst, src+8, 0x28); G2 DLL analysis inconclusive (stripped) - Player count: field at entry offset 0x08 maps to struct param_1[0xe]
This commit is contained in:
@@ -46,13 +46,14 @@ type SessionSnapshot struct {
|
||||
|
||||
// StageSnapshot is an immutable copy of stage data taken under lock.
|
||||
type StageSnapshot struct {
|
||||
ServerIP net.IP
|
||||
ServerPort uint16
|
||||
StageID string
|
||||
ClientCount int
|
||||
Reserved int
|
||||
MaxPlayers uint16
|
||||
RawBinData0 []byte
|
||||
RawBinData1 []byte
|
||||
RawBinData3 []byte
|
||||
ServerIP net.IP
|
||||
ServerPort uint16
|
||||
StageID string
|
||||
ClientCount int
|
||||
Reserved int
|
||||
QuestReserved int // Players who left to enter quest stages ("Qs" prefix)
|
||||
MaxPlayers uint16
|
||||
RawBinData0 []byte
|
||||
RawBinData1 []byte
|
||||
RawBinData3 []byte
|
||||
}
|
||||
|
||||
@@ -107,6 +107,19 @@ func (r *LocalChannelRegistry) SearchStages(stagePrefix string, max int) []Stage
|
||||
if len(results) >= max {
|
||||
break
|
||||
}
|
||||
|
||||
// Pre-collect which charIDs are in quest stages under server lock,
|
||||
// so we can count quest-reserved players without lock ordering issues
|
||||
// (Server.Mutex must be acquired before Stage.RWMutex).
|
||||
c.Lock()
|
||||
inQuest := make(map[uint32]bool)
|
||||
for _, sess := range c.sessions {
|
||||
if sess.stage != nil && len(sess.stage.id) > 4 && sess.stage.id[3:5] == "Qs" {
|
||||
inQuest[sess.charID] = true
|
||||
}
|
||||
}
|
||||
c.Unlock()
|
||||
|
||||
cIP := net.ParseIP(c.IP).To4()
|
||||
cPort := c.Port
|
||||
c.stages.Range(func(_ string, stage *Stage) bool {
|
||||
@@ -127,16 +140,24 @@ func (r *LocalChannelRegistry) SearchStages(stagePrefix string, max int) []Stage
|
||||
bin3Copy := make([]byte, len(bin3))
|
||||
copy(bin3Copy, bin3)
|
||||
|
||||
questReserved := 0
|
||||
for charID := range stage.reservedClientSlots {
|
||||
if inQuest[charID] {
|
||||
questReserved++
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, StageSnapshot{
|
||||
ServerIP: cIP,
|
||||
ServerPort: cPort,
|
||||
StageID: stage.id,
|
||||
ClientCount: len(stage.clients) + len(stage.reservedClientSlots),
|
||||
Reserved: len(stage.reservedClientSlots),
|
||||
MaxPlayers: stage.maxPlayers,
|
||||
RawBinData0: bin0Copy,
|
||||
RawBinData1: bin1Copy,
|
||||
RawBinData3: bin3Copy,
|
||||
ServerIP: cIP,
|
||||
ServerPort: cPort,
|
||||
StageID: stage.id,
|
||||
ClientCount: len(stage.clients) + len(stage.reservedClientSlots),
|
||||
Reserved: len(stage.reservedClientSlots),
|
||||
QuestReserved: questReserved,
|
||||
MaxPlayers: stage.maxPlayers,
|
||||
RawBinData0: bin0Copy,
|
||||
RawBinData1: bin1Copy,
|
||||
RawBinData3: bin3Copy,
|
||||
})
|
||||
stage.RUnlock()
|
||||
return true
|
||||
|
||||
@@ -407,8 +407,13 @@ func handleMsgSysIssueLogkey(s *Session, p mhfpacket.MHFPacket) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(Andoryuuta): In the official client, the log key index is off by one,
|
||||
// cutting off the last byte in _most uses_. Find and document these accordingly.
|
||||
// Client log key off-by-one (RE'd from mhfo-hd.dll ZZ):
|
||||
// putIssue_logkey (0x1D) requests and stores all 16 bytes correctly.
|
||||
// putRecord_log (0x1E) and putTerminal_log (0x13) do NOT embed the log key
|
||||
// in their packets — they pass size 0 to the packet builder for the key field.
|
||||
// The original off-by-one note (Andoryuuta) may apply to pre-ZZ clients where
|
||||
// these functions did use the key. In ZZ the key is stored but never sent back,
|
||||
// so the server value is effectively unused beyond issuance.
|
||||
s.Lock()
|
||||
s.logKey = logKey
|
||||
s.Unlock()
|
||||
@@ -548,7 +553,11 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) {
|
||||
resp.WriteUint8(uint8(len(sjisName) + 1))
|
||||
resp.WriteUint16(uint16(len(snap.UserBinary3)))
|
||||
|
||||
// TODO: This case might be <=G2
|
||||
// User search response padding block (RE'd from mhfo-hd.dll ZZ):
|
||||
// ZZ per-entry parser (FUN_115868a0) reads 0x28 (40) bytes at offset +8
|
||||
// via memcpy into the result struct. G1 and earlier use 8 bytes.
|
||||
// G2 DLL analysis was inconclusive (stripped binary, no shared struct
|
||||
// sizes with ZZ) — the boundary may be <=G2 rather than <=G1.
|
||||
if s.server.erupeConfig.RealClientMode <= cfg.G1 {
|
||||
resp.WriteBytes(make([]byte, 8))
|
||||
} else {
|
||||
@@ -711,8 +720,10 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) {
|
||||
resp.WriteUint16(0) // Unk, [0 1 2]
|
||||
resp.WriteUint16(uint16(sr.ClientCount))
|
||||
resp.WriteUint16(sr.MaxPlayers)
|
||||
// TODO: Retail returned the number of clients in quests, not workshop/my series
|
||||
resp.WriteUint16(uint16(sr.Reserved))
|
||||
// Retail returned only clients in quest stages ("Qs" prefix),
|
||||
// not workshop/my series. RE'd from FUN_11586690 in mhfo-hd.dll ZZ:
|
||||
// field at entry offset 0x08-0x09 → struct offset 0x1C (param_1[0xe]).
|
||||
resp.WriteUint16(uint16(sr.QuestReserved))
|
||||
|
||||
resp.WriteUint8(0) // Static?
|
||||
resp.WriteUint8(uint8(sr.MaxPlayers))
|
||||
|
||||
Reference in New Issue
Block a user