mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
Silently ignored DB errors in handlers could cause data loss (frontier point transactions completing without DB writes), reward duplication (stamp exchange granting items on failed UPDATE), and crashes (tower mission page=0 causing index-out-of-bounds). House access state defaulting to 0 on DB failure also bypassed all access controls. HIGH risk fixes: - frontier point buy/sell now fails with ACK on DB error - stamp exchange/stampcard abort on failed UPDATE - guild meal INSERT returns fail ACK instead of orphaned ID 0 - mercenary/airou creation aborts on failed sequence nextval MEDIUM risk fixes: - tower mission page clamped to >= 1 preventing array underflow - tower RP donation returns early on failed guild state read - house state defaults to 2 (password-protected) on DB failure - playtime read failure logged instead of silently resetting RP Also cache userID on Session at login time, eliminating ~25 redundant subqueries of the form WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1) across shop, gacha, command, and distitem handlers.
825 lines
25 KiB
Go
825 lines
25 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()
|
|
|
|
if err := s.server.db.QueryRow("SELECT user_id FROM characters WHERE id=$1", s.charID).Scan(&s.userID); 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
|
|
}
|
|
|
|
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 SET last_character=$1 WHERE id=$2", s.charID, s.userID)
|
|
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 {
|
|
if err := s.server.db.QueryRow("SELECT time_played FROM characters WHERE id = $1", s.charID).Scan(&timePlayed); err != nil {
|
|
s.logger.Error("Failed to read time_played, RP accrual may be inaccurate", zap.Error(err))
|
|
}
|
|
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) {}
|