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:
Houmgaor
2026-02-27 17:29:32 +01:00
parent 649eebe67c
commit 21f9a79b62
5 changed files with 62 additions and 26 deletions

View File

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