Files
Erupe/server/channelserver/handlers_session.go
Houmgaor 7c444b023b refactor(channelserver): replace magic numbers with named protocol constants
Extract numeric literals into named constants across quest handling,
save data parsing, rengoku skill layout, diva event timing, guild info,
achievement trophies, RP accrual rates, and semaphore IDs. Adds
constants_quest.go for quest-related constants shared across functions.

Pure rename/extract with zero behavior change.
2026-02-20 19:50:28 +01:00

817 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"
_config "erupe-ce/config"
"erupe-ce/network/mhfpacket"
"fmt"
"io"
"net"
"strings"
"time"
"go.uber.org/zap"
)
func handleMsgHead(s *Session, p mhfpacket.MHFPacket) {}
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) {}
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 {
var token string
err := s.server.db.QueryRow("SELECT token FROM sign_sessions ss INNER JOIN public.users u on ss.user_id = u.id WHERE token=$1 AND ss.id=$2 AND u.id=(SELECT c.user_id FROM characters c WHERE c.id=$3)", pkt.LoginTokenString, pkt.LoginTokenNumber, pkt.CharID0).Scan(&token)
if err != nil {
_ = s.rawConn.Close()
s.logger.Warn(fmt.Sprintf("Invalid login token, offending CID: (%d)", pkt.CharID0))
return
}
}
s.Lock()
s.charID = pkt.CharID0
s.token = pkt.LoginTokenString
s.Unlock()
bf := byteframe.NewByteFrame()
bf.WriteUint32(uint32(TimeAdjusted().Unix())) // Unix timestamp
_, err := s.server.db.Exec("UPDATE servers SET current_players=$1 WHERE server_id=$2", len(s.server.sessions), s.server.ID)
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.db.Exec("UPDATE sign_sessions SET server_id=$1, char_id=$2 WHERE token=$3", s.server.ID, s.charID, s.token)
if err != nil {
s.logger.Error("Failed to update sign session", zap.Error(err))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
_, err = s.server.db.Exec("UPDATE characters SET last_login=$1 WHERE id=$2", TimeAdjusted().Unix(), s.charID)
if err != nil {
s.logger.Error("Failed to update last login", zap.Error(err))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
_, err = s.server.db.Exec("UPDATE users u SET last_character=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)", 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)
characterSaveData.Save(s)
// 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 {
_ = s.server.db.QueryRow("SELECT time_played FROM characters WHERE id = $1", s.charID).Scan(&timePlayed)
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.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))
}
} 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.db.Exec("UPDATE characters SET time_played = $1 WHERE id = $2", timePlayed, s.charID); err != nil {
s.logger.Error("Failed to update time played", zap.Error(err))
}
if _, err := s.server.db.Exec(`UPDATE guild_characters SET treasure_hunt=NULL WHERE character_id=$1`, s.charID); err != nil {
s.logger.Error("Failed to clear treasure hunt", zap.Error(err))
}
}
// NOW do cleanup (after save is complete)
s.server.Lock()
delete(s.server.sessions, s.rawConn)
_ = s.rawConn.Close()
s.server.Unlock()
// Stage cleanup
for _, stage := range s.server.stages {
// Tell sessions registered to disconnecting players quest to unregister
if stage.host != nil && stage.host.charID == s.charID {
for _, sess := range s.server.sessions {
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)
}
}
}
// Update sign sessions and server player count
if s.server.db != nil {
_, err := s.server.db.Exec("UPDATE sign_sessions SET server_id=NULL, char_id=NULL WHERE token=$1", s.token)
if err != nil {
s.logger.Error("Failed to clear sign session", zap.Error(err))
}
_, err = s.server.db.Exec("UPDATE servers SET current_players=$1 WHERE server_id=$2", len(s.server.sessions), s.server.ID)
if 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.Lock()
for _, stage := range s.server.stages {
delete(stage.reservedClientSlots, s.charID)
}
s.server.Unlock()
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) {}
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
}
// TODO(Andoryuuta): In the offical client, the log key index is off by one,
// cutting off the last byte in _most uses_. Find and document these accordingly.
s.Lock()
s.logKey = logKey
s.Unlock()
// Issue it.
resp := byteframe.NewByteFrame()
resp.WriteBytes(logKey)
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) {
pkt := p.(*mhfpacket.MsgSysRecordLog)
if s.server.erupeConfig.RealClientMode == _config.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.db.Exec(`INSERT INTO kill_logs (character_id, monster, quantity, timestamp) VALUES ($1, $2, $3, $4)`, 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) {}
func handleMsgSysLockGlobalSema(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysLockGlobalSema)
var sgid string
if s.server.Registry != nil {
sgid = s.server.Registry.FindChannelForStage(pkt.UserIDString)
} else {
for _, channel := range s.server.Channels {
channel.stagesLock.RLock()
for id := range channel.stages {
if strings.HasSuffix(id, pkt.UserIDString) {
sgid = channel.GlobalID
}
}
channel.stagesLock.RUnlock()
}
}
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) {}
func handleMsgSysAuthQuery(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgSysAuthTerminal(s *Session, p mhfpacket.MHFPacket) {}
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.SJISToUTF8(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
// Snapshot matching sessions under lock, then build response outside locks.
type sessionResult struct {
charID uint32
name []byte
stageID []byte
ip net.IP
port uint16
userBin3 []byte
}
var results []sessionResult
for _, c := range s.server.Channels {
if count == maxResults {
break
}
c.Lock()
c.userBinaryPartsLock.RLock()
for _, session := range c.sessions {
if count == maxResults {
break
}
if pkt.SearchType == 1 && session.charID != cid {
continue
}
if pkt.SearchType == 2 && !strings.Contains(session.Name, term) {
continue
}
if pkt.SearchType == 3 && session.server.IP != ip && session.server.Port != port && session.stage.id != term {
continue
}
count++
ub3 := c.userBinaryParts[userBinaryPartID{charID: session.charID, index: 3}]
ub3Copy := make([]byte, len(ub3))
copy(ub3Copy, ub3)
results = append(results, sessionResult{
charID: session.charID,
name: stringsupport.UTF8ToSJIS(session.Name),
stageID: stringsupport.UTF8ToSJIS(session.stage.id),
ip: net.ParseIP(c.IP).To4(),
port: c.Port,
userBin3: ub3Copy,
})
}
c.userBinaryPartsLock.RUnlock()
c.Unlock()
}
for _, r := range results {
if !local {
resp.WriteUint32(binary.LittleEndian.Uint32(r.ip))
} else {
resp.WriteUint32(0x0100007F)
}
resp.WriteUint16(r.port)
resp.WriteUint32(r.charID)
resp.WriteUint8(uint8(len(r.stageID) + 1))
resp.WriteUint8(uint8(len(r.name) + 1))
resp.WriteUint16(uint16(len(r.userBin3)))
// TODO: This case might be <=G2
if s.server.erupeConfig.RealClientMode <= _config.G1 {
resp.WriteBytes(make([]byte, 8))
} else {
resp.WriteBytes(make([]byte, 40))
}
resp.WriteBytes(make([]byte, 8))
resp.WriteNullTerminatedBytes(r.stageID)
resp.WriteNullTerminatedBytes(r.name)
resp.WriteBytes(r.userBin3)
}
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 >= _config.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 >= _config.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 >= _config.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 >= _config.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 >= _config.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 >= _config.Z1 {
findPartyParams.QuestID = append(findPartyParams.QuestID, bf.ReadInt16())
} else {
findPartyParams.QuestID = append(findPartyParams.QuestID, int16(bf.ReadInt8()))
}
}
}
}
// Snapshot matching stages under lock, then build response outside locks.
type stageResult struct {
ip net.IP
port uint16
clientCount int
reserved int
maxPlayers uint16
stageID string
stageData []int16
rawBinData0 []byte
rawBinData1 []byte
}
var stageResults []stageResult
for _, c := range s.server.Channels {
if count == maxResults {
break
}
c.stagesLock.RLock()
for _, stage := range c.stages {
if count == maxResults {
break
}
if strings.HasPrefix(stage.id, findPartyParams.StagePrefix) {
stage.RLock()
sb3 := byteframe.NewByteFrameFromBytes(stage.rawBinaryData[stageBinaryKey{1, 3}])
_, _ = sb3.Seek(4, 0)
stageDataParams := 7
if s.server.erupeConfig.RealClientMode <= _config.G10 {
stageDataParams = 4
} else if s.server.erupeConfig.RealClientMode <= _config.Z1 {
stageDataParams = 6
}
var stageData []int16
for i := 0; i < stageDataParams; i++ {
if s.server.erupeConfig.RealClientMode >= _config.Z1 {
stageData = append(stageData, sb3.ReadInt16())
} else {
stageData = append(stageData, int16(sb3.ReadInt8()))
}
}
if findPartyParams.RankRestriction >= 0 {
if stageData[0] > findPartyParams.RankRestriction {
stage.RUnlock()
continue
}
}
var hasTarget bool
if len(findPartyParams.Targets) > 0 {
for _, target := range findPartyParams.Targets {
if target == stageData[1] {
hasTarget = true
break
}
}
if !hasTarget {
stage.RUnlock()
continue
}
}
// Copy binary data under lock
bin0 := stage.rawBinaryData[stageBinaryKey{1, 0}]
bin0Copy := make([]byte, len(bin0))
copy(bin0Copy, bin0)
bin1 := stage.rawBinaryData[stageBinaryKey{1, 1}]
bin1Copy := make([]byte, len(bin1))
copy(bin1Copy, bin1)
count++
stageResults = append(stageResults, stageResult{
ip: net.ParseIP(c.IP).To4(),
port: c.Port,
clientCount: len(stage.clients) + len(stage.reservedClientSlots),
reserved: len(stage.reservedClientSlots),
maxPlayers: stage.maxPlayers,
stageID: stage.id,
stageData: stageData,
rawBinData0: bin0Copy,
rawBinData1: bin1Copy,
})
stage.RUnlock()
}
}
c.stagesLock.RUnlock()
}
for _, sr := range stageResults {
if !local {
resp.WriteUint32(binary.LittleEndian.Uint32(sr.ip))
} else {
resp.WriteUint32(0x0100007F)
}
resp.WriteUint16(sr.port)
resp.WriteUint16(0) // Static?
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))
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 >= _config.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) {}
func handleMsgMhfServerCommand(s *Session, p mhfpacket.MHFPacket) {}
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) {}
func handleMsgSysTransBinary(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgSysCollectBinary(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgSysGetState(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgSysSerialize(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgSysEnumlobby(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgSysEnumuser(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgSysInfokyserver(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgMhfGetCaUniqueID(s *Session, p mhfpacket.MHFPacket) {}