Files
Erupe/server/channelserver/handlers_session.go
Houmgaor 7ea2660335 docs(stubs): annotate empty handlers and add unimplemented reference doc
Add // stub: unimplemented to 70 empty game-feature handlers and
// stub: reserved to 56 protocol-reserved slots in handlers_reserve.go,
making them discoverable via grep. Add docs/unimplemented.md listing all
unimplemented handlers grouped by subsystem with descriptions.
2026-03-19 10:57:09 +01:00

782 lines
24 KiB
Go

package channelserver
import (
"crypto/rand"
"encoding/binary"
"erupe-ce/common/byteframe"
"erupe-ce/common/mhfcourse"
"erupe-ce/common/mhfmon"
ps "erupe-ce/common/pascalstring"
"erupe-ce/common/stringsupport"
cfg "erupe-ce/config"
"erupe-ce/network/mhfpacket"
"fmt"
"io"
"strings"
"time"
"go.uber.org/zap"
)
func handleMsgHead(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgSysExtendThreshold(s *Session, p mhfpacket.MHFPacket) {
// No data aside from header, no resp required.
}
func handleMsgSysEnd(s *Session, p mhfpacket.MHFPacket) {
// No data aside from header, no resp required.
}
func handleMsgSysNop(s *Session, p mhfpacket.MHFPacket) {
// No data aside from header, no resp required.
}
func handleMsgSysAck(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgSysTerminalLog(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysTerminalLog)
for i := range pkt.Entries {
s.server.logger.Info("SysTerminalLog",
zap.Uint8("Type1", pkt.Entries[i].Type1),
zap.Uint8("Type2", pkt.Entries[i].Type2),
zap.Int16("Unk0", pkt.Entries[i].Unk0),
zap.Int32("Unk1", pkt.Entries[i].Unk1),
zap.Int32("Unk2", pkt.Entries[i].Unk2),
zap.Int32("Unk3", pkt.Entries[i].Unk3),
zap.Int32s("Unk4", pkt.Entries[i].Unk4),
)
}
resp := byteframe.NewByteFrame()
resp.WriteUint32(pkt.LogID + 1) // LogID to use for requests after this.
doAckSimpleSucceed(s, pkt.AckHandle, resp.Data())
}
func handleMsgSysLogin(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysLogin)
if !s.server.erupeConfig.DebugOptions.DisableTokenCheck {
if err := s.server.sessionRepo.ValidateLoginToken(pkt.LoginTokenString, pkt.LoginTokenNumber, pkt.CharID0); err != nil {
_ = s.rawConn.Close()
s.logger.Warn("Invalid login token", zap.Uint32("charID", pkt.CharID0))
return
}
}
s.Lock()
s.charID = pkt.CharID0
s.token = pkt.LoginTokenString
s.Unlock()
userID, err := s.server.charRepo.GetUserID(s.charID)
if err != nil {
s.logger.Error("Failed to resolve user ID for character", zap.Error(err), zap.Uint32("charID", s.charID))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
s.userID = userID
if s.captureConn != nil {
s.captureConn.SetSessionInfo(s.charID, s.userID)
}
bf := byteframe.NewByteFrame()
bf.WriteUint32(uint32(TimeAdjusted().Unix())) // Unix timestamp
err = s.server.sessionRepo.UpdatePlayerCount(s.server.ID, len(s.server.sessions))
if err != nil {
s.logger.Error("Failed to update current players", zap.Error(err))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
err = s.server.sessionRepo.BindSession(s.token, s.server.ID, s.charID)
if err != nil {
s.logger.Error("Failed to update sign session", zap.Error(err))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
if err = s.server.charRepo.UpdateLastLogin(s.charID, TimeAdjusted().Unix()); err != nil {
s.logger.Error("Failed to update last login", zap.Error(err))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
err = s.server.userRepo.SetLastCharacter(s.userID, s.charID)
if err != nil {
s.logger.Error("Failed to update last character", zap.Error(err))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
doAckSimpleSucceed(s, pkt.AckHandle, bf.Data())
updateRights(s)
s.server.BroadcastMHF(&mhfpacket.MsgSysInsertUser{CharID: s.charID}, s)
}
func handleMsgSysLogout(s *Session, p mhfpacket.MHFPacket) {
logoutPlayer(s)
}
// saveAllCharacterData saves all character data to the database with proper error handling.
// This function ensures data persistence even if the client disconnects unexpectedly.
// It handles:
// - Main savedata blob (compressed)
// - User binary data (house, gallery, etc.)
// - Plate data (transmog appearance, storage, equipment sets)
// - Playtime updates
// - RP updates
// - Name corruption prevention
func saveAllCharacterData(s *Session, rpToAdd int) error {
saveStart := time.Now()
// Get current savedata from database
characterSaveData, err := GetCharacterSaveData(s, s.charID)
if err != nil {
s.logger.Error("Failed to retrieve character save data",
zap.Error(err),
zap.Uint32("charID", s.charID),
zap.String("name", s.Name),
)
return err
}
if characterSaveData == nil {
s.logger.Warn("Character save data is nil, skipping save",
zap.Uint32("charID", s.charID),
zap.String("name", s.Name),
)
return nil
}
// Force name to match to prevent corruption detection issues
// This handles SJIS/UTF-8 encoding differences across game versions
if characterSaveData.Name != s.Name {
s.logger.Debug("Correcting name mismatch before save",
zap.String("savedata_name", characterSaveData.Name),
zap.String("session_name", s.Name),
zap.Uint32("charID", s.charID),
)
characterSaveData.Name = s.Name
characterSaveData.updateSaveDataWithStruct()
}
// Update playtime from session
if !s.playtimeTime.IsZero() {
sessionPlaytime := uint32(time.Since(s.playtimeTime).Seconds())
s.playtime += sessionPlaytime
s.logger.Debug("Updated playtime",
zap.Uint32("session_playtime_seconds", sessionPlaytime),
zap.Uint32("total_playtime", s.playtime),
zap.Uint32("charID", s.charID),
)
}
characterSaveData.Playtime = s.playtime
// Update RP if any gained during session
if rpToAdd > 0 {
characterSaveData.RP += uint16(rpToAdd)
if characterSaveData.RP >= s.server.erupeConfig.GameplayOptions.MaximumRP {
characterSaveData.RP = s.server.erupeConfig.GameplayOptions.MaximumRP
s.logger.Debug("RP capped at maximum",
zap.Uint16("max_rp", s.server.erupeConfig.GameplayOptions.MaximumRP),
zap.Uint32("charID", s.charID),
)
}
s.logger.Debug("Added RP",
zap.Int("rp_gained", rpToAdd),
zap.Uint16("new_rp", characterSaveData.RP),
zap.Uint32("charID", s.charID),
)
}
// Save to database (main savedata + user_binary)
if err := characterSaveData.Save(s); err != nil {
s.logger.Error("Failed to save character data",
zap.Error(err),
zap.Uint32("charID", s.charID),
zap.String("name", s.Name),
)
return err
}
// Save auxiliary data types
// Note: Plate data saves immediately when client sends save packets,
// so this is primarily a safety net for monitoring and consistency
if err := savePlateDataToDatabase(s); err != nil {
s.logger.Error("Failed to save plate data during logout",
zap.Error(err),
zap.Uint32("charID", s.charID),
)
// Don't return error - continue with logout even if plate save fails
}
saveDuration := time.Since(saveStart)
s.logger.Info("Saved character data successfully",
zap.Uint32("charID", s.charID),
zap.String("name", s.Name),
zap.Duration("duration", saveDuration),
zap.Int("rp_added", rpToAdd),
zap.Uint32("playtime", s.playtime),
)
return nil
}
func logoutPlayer(s *Session) {
logoutStart := time.Now()
// Log logout initiation with session details
sessionDuration := time.Duration(0)
if s.sessionStart > 0 {
sessionDuration = time.Since(time.Unix(s.sessionStart, 0))
}
s.logger.Info("Player logout initiated",
zap.Uint32("charID", s.charID),
zap.String("name", s.Name),
zap.Duration("session_duration", sessionDuration),
)
// Calculate session metrics FIRST (before cleanup)
var timePlayed int
var sessionTime int
var rpGained int
if s.charID != 0 {
if val, err := s.server.charRepo.ReadInt(s.charID, "time_played"); err != nil {
s.logger.Error("Failed to read time_played, RP accrual may be inaccurate", zap.Error(err))
} else {
timePlayed = val
}
sessionTime = int(TimeAdjusted().Unix()) - int(s.sessionStart)
timePlayed += sessionTime
if mhfcourse.CourseExists(30, s.courses) {
rpGained = timePlayed / rpAccrualCafe
timePlayed = timePlayed % rpAccrualCafe
if _, err := s.server.charRepo.AdjustInt(s.charID, "cafe_time", sessionTime); err != nil {
s.logger.Error("Failed to update cafe time", zap.Error(err))
}
} else {
rpGained = timePlayed / rpAccrualNormal
timePlayed = timePlayed % rpAccrualNormal
}
s.logger.Debug("Session metrics calculated",
zap.Uint32("charID", s.charID),
zap.Int("session_time_seconds", sessionTime),
zap.Int("rp_gained", rpGained),
zap.Int("time_played_remainder", timePlayed),
)
// Save all character data ONCE with all updates
// This is the safety net that ensures data persistence even if client
// didn't send save packets before disconnecting
if err := saveAllCharacterData(s, rpGained); err != nil {
s.logger.Error("Failed to save character data during logout",
zap.Error(err),
zap.Uint32("charID", s.charID),
zap.String("name", s.Name),
)
// Continue with logout even if save fails
}
// Update time_played and guild treasure hunt
if err := s.server.charRepo.UpdateTimePlayed(s.charID, timePlayed); err != nil {
s.logger.Error("Failed to update time played", zap.Error(err))
}
if err := s.server.guildRepo.ClearTreasureHunt(s.charID); err != nil {
s.logger.Error("Failed to clear treasure hunt", zap.Error(err))
}
}
// Flush and close capture file before closing the connection.
if s.captureCleanup != nil {
s.captureCleanup()
}
// NOW do cleanup (after save is complete)
s.server.Lock()
delete(s.server.sessions, s.rawConn)
_ = s.rawConn.Close()
s.server.Unlock()
// Stage cleanup — snapshot sessions first under server mutex, then iterate stages
s.server.Lock()
sessionSnapshot := make([]*Session, 0, len(s.server.sessions))
for _, sess := range s.server.sessions {
sessionSnapshot = append(sessionSnapshot, sess)
}
s.server.Unlock()
s.server.stages.Range(func(_ string, stage *Stage) bool {
stage.Lock()
// Tell sessions registered to disconnecting player's quest to unregister
if stage.host != nil && stage.host.charID == s.charID {
for _, sess := range sessionSnapshot {
for rSlot := range stage.reservedClientSlots {
if sess.charID == rSlot && sess.stage != nil && sess.stage.id[3:5] != "Qs" {
sess.QueueSendMHFNonBlocking(&mhfpacket.MsgSysStageDestruct{})
}
}
}
}
for session := range stage.clients {
if session.charID == s.charID {
delete(stage.clients, session)
}
}
stage.Unlock()
return true
})
// Update sign sessions and server player count
if s.server.db != nil {
if err := s.server.sessionRepo.ClearSession(s.token); err != nil {
s.logger.Error("Failed to clear sign session", zap.Error(err))
}
if err := s.server.sessionRepo.UpdatePlayerCount(s.server.ID, len(s.server.sessions)); err != nil {
s.logger.Error("Failed to update player count", zap.Error(err))
}
}
if s.stage == nil {
logoutDuration := time.Since(logoutStart)
s.logger.Info("Player logout completed",
zap.Uint32("charID", s.charID),
zap.String("name", s.Name),
zap.Duration("logout_duration", logoutDuration),
)
return
}
// Broadcast user deletion and final cleanup
s.server.BroadcastMHF(&mhfpacket.MsgSysDeleteUser{
CharID: s.charID,
}, s)
s.server.stages.Range(func(_ string, stage *Stage) bool {
stage.Lock()
delete(stage.reservedClientSlots, s.charID)
stage.Unlock()
return true
})
removeSessionFromSemaphore(s)
removeSessionFromStage(s)
logoutDuration := time.Since(logoutStart)
s.logger.Info("Player logout completed",
zap.Uint32("charID", s.charID),
zap.String("name", s.Name),
zap.Duration("logout_duration", logoutDuration),
zap.Int("rp_gained", rpGained),
)
}
func handleMsgSysSetStatus(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgSysPing(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysPing)
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
}
func handleMsgSysTime(s *Session, p mhfpacket.MHFPacket) {
resp := &mhfpacket.MsgSysTime{
GetRemoteTime: false,
Timestamp: uint32(TimeAdjusted().Unix()), // JP timezone
}
s.QueueSendMHF(resp)
s.notifyRavi()
}
func handleMsgSysIssueLogkey(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysIssueLogkey)
// Make a random log key for this session.
logKey := make([]byte, 16)
_, err := rand.Read(logKey)
if err != nil {
s.logger.Error("Failed to generate log key", zap.Error(err))
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
return
}
// 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()
// Issue it.
resp := byteframe.NewByteFrame()
resp.WriteBytes(logKey)
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
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) {
pkt := p.(*mhfpacket.MsgSysRecordLog)
if s.server.erupeConfig.RealClientMode == cfg.ZZ {
bf := byteframe.NewByteFrameFromBytes(pkt.Data)
_, _ = bf.Seek(killLogHeaderSize, 0)
var val uint8
for i := 0; i < killLogMonsterCount; i++ {
val = bf.ReadUint8()
if val > 0 && mhfmon.Monsters[i].Large {
if err := s.server.guildRepo.InsertKillLog(s.charID, i, val, TimeAdjusted()); err != nil {
s.logger.Error("Failed to insert kill log", zap.Error(err))
}
}
}
}
// remove a client returning to town from reserved slots to make sure the stage is hidden from board
delete(s.stage.reservedClientSlots, s.charID)
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}
func handleMsgSysEcho(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgSysLockGlobalSema(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysLockGlobalSema)
sgid := s.server.Registry.FindChannelForStage(pkt.UserIDString)
bf := byteframe.NewByteFrame()
if len(sgid) > 0 && sgid != s.server.GlobalID {
bf.WriteUint8(0)
bf.WriteUint8(0)
ps.Uint16(bf, sgid, false)
} else {
bf.WriteUint8(2)
bf.WriteUint8(0)
ps.Uint16(bf, pkt.ServerChannelIDString, false)
}
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}
func handleMsgSysUnlockGlobalSema(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysUnlockGlobalSema)
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}
func handleMsgSysUpdateRight(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgSysAuthQuery(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgSysAuthTerminal(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgSysRightsReload(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysRightsReload)
updateRights(s)
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
}
func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfTransitMessage)
local := strings.Split(s.rawConn.RemoteAddr().String(), ":")[0] == "127.0.0.1"
var maxResults, port, count uint16
var cid uint32
var term, ip string
bf := byteframe.NewByteFrameFromBytes(pkt.MessageData)
switch pkt.SearchType {
case 1:
maxResults = 1
cid = bf.ReadUint32()
case 2:
bf.ReadUint16() // term length
maxResults = bf.ReadUint16()
bf.ReadUint8() // Unk
term = stringsupport.SJISToUTF8Lossy(bf.ReadNullTerminatedBytes())
case 3:
_ip := bf.ReadBytes(4)
ip = fmt.Sprintf("%d.%d.%d.%d", _ip[3], _ip[2], _ip[1], _ip[0])
port = bf.ReadUint16()
bf.ReadUint16() // term length
maxResults = bf.ReadUint16()
bf.ReadUint8()
term = string(bf.ReadNullTerminatedBytes())
}
resp := byteframe.NewByteFrame()
resp.WriteUint16(0)
switch pkt.SearchType {
case 1, 2, 3: // usersearchidx, usersearchname, lobbysearchname
predicate := func(snap SessionSnapshot) bool {
switch pkt.SearchType {
case 1:
return snap.CharID == cid
case 2:
return strings.Contains(snap.Name, term)
case 3:
return snap.ServerIP.String() == ip && snap.ServerPort == port && snap.StageID == term
}
return false
}
snapshots := s.server.Registry.SearchSessions(predicate, int(maxResults))
count = uint16(len(snapshots))
for _, snap := range snapshots {
if !local {
resp.WriteUint32(binary.LittleEndian.Uint32(snap.ServerIP))
} else {
resp.WriteUint32(localhostAddrLE)
}
resp.WriteUint16(snap.ServerPort)
resp.WriteUint32(snap.CharID)
sjisStageID := stringsupport.UTF8ToSJIS(snap.StageID)
sjisName := stringsupport.UTF8ToSJIS(snap.Name)
resp.WriteUint8(uint8(len(sjisStageID) + 1))
resp.WriteUint8(uint8(len(sjisName) + 1))
resp.WriteUint16(uint16(len(snap.UserBinary3)))
// 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 {
resp.WriteBytes(make([]byte, 40))
}
resp.WriteBytes(make([]byte, 8))
resp.WriteNullTerminatedBytes(sjisStageID)
resp.WriteNullTerminatedBytes(sjisName)
resp.WriteBytes(snap.UserBinary3)
}
case 4: // lobbysearch
type FindPartyParams struct {
StagePrefix string
RankRestriction int16
Targets []int16
Unk0 []int16
Unk1 []int16
QuestID []int16
}
findPartyParams := FindPartyParams{
StagePrefix: "sl2Ls210",
}
numParams := bf.ReadUint8()
maxResults = bf.ReadUint16()
for i := uint8(0); i < numParams; i++ {
switch bf.ReadUint8() {
case 0:
values := bf.ReadUint8()
for i := uint8(0); i < values; i++ {
if s.server.erupeConfig.RealClientMode >= cfg.Z1 {
findPartyParams.RankRestriction = bf.ReadInt16()
} else {
findPartyParams.RankRestriction = int16(bf.ReadInt8())
}
}
case 1:
values := bf.ReadUint8()
for i := uint8(0); i < values; i++ {
if s.server.erupeConfig.RealClientMode >= cfg.Z1 {
findPartyParams.Targets = append(findPartyParams.Targets, bf.ReadInt16())
} else {
findPartyParams.Targets = append(findPartyParams.Targets, int16(bf.ReadInt8()))
}
}
case 2:
values := bf.ReadUint8()
for i := uint8(0); i < values; i++ {
var value int16
if s.server.erupeConfig.RealClientMode >= cfg.Z1 {
value = bf.ReadInt16()
} else {
value = int16(bf.ReadInt8())
}
switch value {
case 0: // Public Bar
findPartyParams.StagePrefix = "sl2Ls210"
case 1: // Tokotoko Partnya
findPartyParams.StagePrefix = "sl2Ls463"
case 2: // Hunting Prowess Match
findPartyParams.StagePrefix = "sl2Ls286"
case 3: // Volpakkun Together
findPartyParams.StagePrefix = "sl2Ls465"
case 5: // Quick Party
// Unk
}
}
case 3: // Unknown
values := bf.ReadUint8()
for i := uint8(0); i < values; i++ {
if s.server.erupeConfig.RealClientMode >= cfg.Z1 {
findPartyParams.Unk0 = append(findPartyParams.Unk0, bf.ReadInt16())
} else {
findPartyParams.Unk0 = append(findPartyParams.Unk0, int16(bf.ReadInt8()))
}
}
case 4: // Looking for n or already have n
values := bf.ReadUint8()
for i := uint8(0); i < values; i++ {
if s.server.erupeConfig.RealClientMode >= cfg.Z1 {
findPartyParams.Unk1 = append(findPartyParams.Unk1, bf.ReadInt16())
} else {
findPartyParams.Unk1 = append(findPartyParams.Unk1, int16(bf.ReadInt8()))
}
}
case 5:
values := bf.ReadUint8()
for i := uint8(0); i < values; i++ {
if s.server.erupeConfig.RealClientMode >= cfg.Z1 {
findPartyParams.QuestID = append(findPartyParams.QuestID, bf.ReadInt16())
} else {
findPartyParams.QuestID = append(findPartyParams.QuestID, int16(bf.ReadInt8()))
}
}
}
}
allStages := s.server.Registry.SearchStages(findPartyParams.StagePrefix, int(maxResults))
// Post-fetch filtering on snapshots (rank restriction, targets)
type filteredStage struct {
StageSnapshot
stageData []int16
}
var stageResults []filteredStage
for _, snap := range allStages {
sb3 := byteframe.NewByteFrameFromBytes(snap.RawBinData3)
_, _ = sb3.Seek(4, 0)
stageDataParams := 7
if s.server.erupeConfig.RealClientMode <= cfg.G10 {
stageDataParams = 4
} else if s.server.erupeConfig.RealClientMode <= cfg.Z1 {
stageDataParams = 6
}
var stageData []int16
for i := 0; i < stageDataParams; i++ {
if s.server.erupeConfig.RealClientMode >= cfg.Z1 {
stageData = append(stageData, sb3.ReadInt16())
} else {
stageData = append(stageData, int16(sb3.ReadInt8()))
}
}
if findPartyParams.RankRestriction >= 0 {
if stageData[0] > findPartyParams.RankRestriction {
continue
}
}
if len(findPartyParams.Targets) > 0 {
var hasTarget bool
for _, target := range findPartyParams.Targets {
if target == stageData[1] {
hasTarget = true
break
}
}
if !hasTarget {
continue
}
}
stageResults = append(stageResults, filteredStage{
StageSnapshot: snap,
stageData: stageData,
})
}
count = uint16(len(stageResults))
for _, sr := range stageResults {
if !local {
resp.WriteUint32(binary.LittleEndian.Uint32(sr.ServerIP))
} else {
resp.WriteUint32(localhostAddrLE)
}
resp.WriteUint16(sr.ServerPort)
resp.WriteUint16(0) // Static?
resp.WriteUint16(0) // Unk, [0 1 2]
resp.WriteUint16(uint16(sr.ClientCount))
resp.WriteUint16(sr.MaxPlayers)
// 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))
resp.WriteUint8(1) // Static?
resp.WriteUint8(uint8(len(sr.StageID) + 1))
resp.WriteUint8(uint8(len(sr.RawBinData0)))
resp.WriteUint8(uint8(len(sr.RawBinData1)))
for i := range sr.stageData {
if s.server.erupeConfig.RealClientMode >= cfg.Z1 {
resp.WriteInt16(sr.stageData[i])
} else {
resp.WriteInt8(int8(sr.stageData[i]))
}
}
resp.WriteUint8(0) // Unk
resp.WriteUint8(0) // Unk
resp.WriteNullTerminatedBytes([]byte(sr.StageID))
resp.WriteBytes(sr.RawBinData0)
resp.WriteBytes(sr.RawBinData1)
}
}
_, _ = resp.Seek(0, io.SeekStart)
resp.WriteUint16(count)
doAckBufSucceed(s, pkt.AckHandle, resp.Data())
}
func handleMsgCaExchangeItem(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgMhfServerCommand(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgMhfAnnounce(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfAnnounce)
s.server.BroadcastRaviente(pkt.IPAddress, pkt.Port, pkt.StageID, pkt.Data.ReadUint8())
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}
func handleMsgMhfSetLoginwindow(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgSysTransBinary(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgSysCollectBinary(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgSysGetState(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgSysSerialize(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgSysEnumlobby(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgSysEnumuser(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgSysInfokyserver(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented
func handleMsgMhfGetCaUniqueID(s *Session, p mhfpacket.MHFPacket) {} // stub: unimplemented