mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-21 23:22:34 +01:00
feat(channelserver): decouple channel servers for independent operation (#33)
Enable multiple Erupe instances to share a single PostgreSQL database without destroying each other's state, fix existing data races in cross-channel access, and lay groundwork for future distributed channel server deployments. Phase 1 — DB safety: - Scope DELETE FROM servers/sign_sessions to this instance's server IDs - Fix ci++ bug where failed channel start shifted subsequent IDs Phase 2 — Fix data races in cross-channel access: - Lock sessions map in FindSessionByCharID and DisconnectUser - Lock stagesLock in handleMsgSysLockGlobalSema - Snapshot sessions/stages under lock in TransitMessage types 1-4 - Lock channel when finding mail notification targets Phase 3 — ChannelRegistry interface: - Define ChannelRegistry interface with 7 cross-channel operations - Implement LocalChannelRegistry with proper locking - Add SessionSnapshot/StageSnapshot immutable copy types - Delegate WorldcastMHF, FindSessionByCharID, DisconnectUser to Registry - Migrate LockGlobalSema and guild mail handlers to use Registry - Add comprehensive tests including concurrent access Phase 4 — Per-channel enable/disable: - Add Enabled *bool to EntranceChannelInfo (nil defaults to true) - Skip disabled channels in startup loop, preserving ID stability - Add IsEnabled() helper with backward-compatible default - Update config.example.json with Enabled field
This commit is contained in:
58
server/channelserver/channel_registry.go
Normal file
58
server/channelserver/channel_registry.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package channelserver
|
||||
|
||||
import (
|
||||
"erupe-ce/network/mhfpacket"
|
||||
"net"
|
||||
)
|
||||
|
||||
// ChannelRegistry abstracts cross-channel operations behind an interface.
|
||||
// The default LocalChannelRegistry wraps the in-process []*Server slice.
|
||||
// Future implementations may use DB/Redis/NATS for multi-process deployments.
|
||||
type ChannelRegistry interface {
|
||||
// Worldcast broadcasts a packet to all sessions across all channels.
|
||||
Worldcast(pkt mhfpacket.MHFPacket, ignoredSession *Session, ignoredChannel *Server)
|
||||
|
||||
// FindSessionByCharID looks up a session by character ID across all channels.
|
||||
FindSessionByCharID(charID uint32) *Session
|
||||
|
||||
// DisconnectUser disconnects all sessions belonging to the given character IDs.
|
||||
DisconnectUser(cids []uint32)
|
||||
|
||||
// FindChannelForStage searches all channels for a stage whose ID has the
|
||||
// given suffix and returns the owning channel's GlobalID, or "" if not found.
|
||||
FindChannelForStage(stageSuffix string) string
|
||||
|
||||
// SearchSessions searches sessions across all channels using a predicate,
|
||||
// returning up to max snapshot results.
|
||||
SearchSessions(predicate func(SessionSnapshot) bool, max int) []SessionSnapshot
|
||||
|
||||
// SearchStages searches stages across all channels with a prefix filter,
|
||||
// returning up to max snapshot results.
|
||||
SearchStages(stagePrefix string, max int) []StageSnapshot
|
||||
|
||||
// NotifyMailToCharID finds the session for charID and sends a mail notification.
|
||||
NotifyMailToCharID(charID uint32, sender *Session, mail *Mail)
|
||||
}
|
||||
|
||||
// SessionSnapshot is an immutable copy of session data taken under lock.
|
||||
type SessionSnapshot struct {
|
||||
CharID uint32
|
||||
Name string
|
||||
StageID string
|
||||
ServerIP net.IP
|
||||
ServerPort uint16
|
||||
UserBinary3 []byte // Copy of userBinaryParts index 3
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
156
server/channelserver/channel_registry_local.go
Normal file
156
server/channelserver/channel_registry_local.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package channelserver
|
||||
|
||||
import (
|
||||
"erupe-ce/network/mhfpacket"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LocalChannelRegistry is the in-process ChannelRegistry backed by []*Server.
|
||||
type LocalChannelRegistry struct {
|
||||
channels []*Server
|
||||
}
|
||||
|
||||
// NewLocalChannelRegistry creates a LocalChannelRegistry wrapping the given channels.
|
||||
func NewLocalChannelRegistry(channels []*Server) *LocalChannelRegistry {
|
||||
return &LocalChannelRegistry{channels: channels}
|
||||
}
|
||||
|
||||
func (r *LocalChannelRegistry) Worldcast(pkt mhfpacket.MHFPacket, ignoredSession *Session, ignoredChannel *Server) {
|
||||
for _, c := range r.channels {
|
||||
if c == ignoredChannel {
|
||||
continue
|
||||
}
|
||||
c.BroadcastMHF(pkt, ignoredSession)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LocalChannelRegistry) FindSessionByCharID(charID uint32) *Session {
|
||||
for _, c := range r.channels {
|
||||
c.Lock()
|
||||
for _, session := range c.sessions {
|
||||
if session.charID == charID {
|
||||
c.Unlock()
|
||||
return session
|
||||
}
|
||||
}
|
||||
c.Unlock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *LocalChannelRegistry) DisconnectUser(cids []uint32) {
|
||||
for _, c := range r.channels {
|
||||
c.Lock()
|
||||
for _, session := range c.sessions {
|
||||
for _, cid := range cids {
|
||||
if session.charID == cid {
|
||||
_ = session.rawConn.Close()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LocalChannelRegistry) FindChannelForStage(stageSuffix string) string {
|
||||
for _, channel := range r.channels {
|
||||
channel.stagesLock.RLock()
|
||||
for id := range channel.stages {
|
||||
if strings.HasSuffix(id, stageSuffix) {
|
||||
gid := channel.GlobalID
|
||||
channel.stagesLock.RUnlock()
|
||||
return gid
|
||||
}
|
||||
}
|
||||
channel.stagesLock.RUnlock()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *LocalChannelRegistry) SearchSessions(predicate func(SessionSnapshot) bool, max int) []SessionSnapshot {
|
||||
var results []SessionSnapshot
|
||||
for _, c := range r.channels {
|
||||
if len(results) >= max {
|
||||
break
|
||||
}
|
||||
c.Lock()
|
||||
c.userBinaryPartsLock.RLock()
|
||||
for _, session := range c.sessions {
|
||||
if len(results) >= max {
|
||||
break
|
||||
}
|
||||
snap := SessionSnapshot{
|
||||
CharID: session.charID,
|
||||
Name: session.Name,
|
||||
ServerIP: net.ParseIP(c.IP).To4(),
|
||||
ServerPort: c.Port,
|
||||
}
|
||||
if session.stage != nil {
|
||||
snap.StageID = session.stage.id
|
||||
}
|
||||
ub3 := c.userBinaryParts[userBinaryPartID{charID: session.charID, index: 3}]
|
||||
if len(ub3) > 0 {
|
||||
snap.UserBinary3 = make([]byte, len(ub3))
|
||||
copy(snap.UserBinary3, ub3)
|
||||
}
|
||||
if predicate(snap) {
|
||||
results = append(results, snap)
|
||||
}
|
||||
}
|
||||
c.userBinaryPartsLock.RUnlock()
|
||||
c.Unlock()
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func (r *LocalChannelRegistry) SearchStages(stagePrefix string, max int) []StageSnapshot {
|
||||
var results []StageSnapshot
|
||||
for _, c := range r.channels {
|
||||
if len(results) >= max {
|
||||
break
|
||||
}
|
||||
c.stagesLock.RLock()
|
||||
for _, stage := range c.stages {
|
||||
if len(results) >= max {
|
||||
break
|
||||
}
|
||||
if !strings.HasPrefix(stage.id, stagePrefix) {
|
||||
continue
|
||||
}
|
||||
stage.RLock()
|
||||
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)
|
||||
bin3 := stage.rawBinaryData[stageBinaryKey{1, 3}]
|
||||
bin3Copy := make([]byte, len(bin3))
|
||||
copy(bin3Copy, bin3)
|
||||
|
||||
results = append(results, StageSnapshot{
|
||||
ServerIP: net.ParseIP(c.IP).To4(),
|
||||
ServerPort: c.Port,
|
||||
StageID: stage.id,
|
||||
ClientCount: len(stage.clients) + len(stage.reservedClientSlots),
|
||||
Reserved: len(stage.reservedClientSlots),
|
||||
MaxPlayers: stage.maxPlayers,
|
||||
RawBinData0: bin0Copy,
|
||||
RawBinData1: bin1Copy,
|
||||
RawBinData3: bin3Copy,
|
||||
})
|
||||
stage.RUnlock()
|
||||
}
|
||||
c.stagesLock.RUnlock()
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func (r *LocalChannelRegistry) NotifyMailToCharID(charID uint32, sender *Session, mail *Mail) {
|
||||
session := r.FindSessionByCharID(charID)
|
||||
if session != nil {
|
||||
SendMailNotification(sender, mail, session)
|
||||
}
|
||||
}
|
||||
190
server/channelserver/channel_registry_test.go
Normal file
190
server/channelserver/channel_registry_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package channelserver
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func createTestChannels(count int) []*Server {
|
||||
channels := make([]*Server, count)
|
||||
for i := 0; i < count; i++ {
|
||||
s := createTestServer()
|
||||
s.ID = uint16(0x1010 + i)
|
||||
s.IP = "10.0.0.1"
|
||||
s.Port = uint16(54001 + i)
|
||||
s.GlobalID = "0101"
|
||||
s.userBinaryParts = make(map[userBinaryPartID][]byte)
|
||||
channels[i] = s
|
||||
}
|
||||
return channels
|
||||
}
|
||||
|
||||
func TestLocalRegistryFindSessionByCharID(t *testing.T) {
|
||||
channels := createTestChannels(2)
|
||||
reg := NewLocalChannelRegistry(channels)
|
||||
|
||||
conn1 := &mockConn{}
|
||||
sess1 := createTestSessionForServer(channels[0], conn1, 100, "Alice")
|
||||
channels[0].Lock()
|
||||
channels[0].sessions[conn1] = sess1
|
||||
channels[0].Unlock()
|
||||
|
||||
conn2 := &mockConn{}
|
||||
sess2 := createTestSessionForServer(channels[1], conn2, 200, "Bob")
|
||||
channels[1].Lock()
|
||||
channels[1].sessions[conn2] = sess2
|
||||
channels[1].Unlock()
|
||||
|
||||
// Find on first channel
|
||||
found := reg.FindSessionByCharID(100)
|
||||
if found == nil || found.charID != 100 {
|
||||
t.Errorf("FindSessionByCharID(100) = %v, want session with charID 100", found)
|
||||
}
|
||||
|
||||
// Find on second channel
|
||||
found = reg.FindSessionByCharID(200)
|
||||
if found == nil || found.charID != 200 {
|
||||
t.Errorf("FindSessionByCharID(200) = %v, want session with charID 200", found)
|
||||
}
|
||||
|
||||
// Not found
|
||||
found = reg.FindSessionByCharID(999)
|
||||
if found != nil {
|
||||
t.Errorf("FindSessionByCharID(999) = %v, want nil", found)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalRegistryFindChannelForStage(t *testing.T) {
|
||||
channels := createTestChannels(2)
|
||||
channels[0].GlobalID = "0101"
|
||||
channels[1].GlobalID = "0102"
|
||||
reg := NewLocalChannelRegistry(channels)
|
||||
|
||||
channels[1].stagesLock.Lock()
|
||||
channels[1].stages["sl2Qs123p0a0u42"] = NewStage("sl2Qs123p0a0u42")
|
||||
channels[1].stagesLock.Unlock()
|
||||
|
||||
gid := reg.FindChannelForStage("u42")
|
||||
if gid != "0102" {
|
||||
t.Errorf("FindChannelForStage(u42) = %q, want %q", gid, "0102")
|
||||
}
|
||||
|
||||
gid = reg.FindChannelForStage("u999")
|
||||
if gid != "" {
|
||||
t.Errorf("FindChannelForStage(u999) = %q, want empty", gid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalRegistryDisconnectUser(t *testing.T) {
|
||||
channels := createTestChannels(1)
|
||||
reg := NewLocalChannelRegistry(channels)
|
||||
|
||||
conn := &mockConn{}
|
||||
sess := createTestSessionForServer(channels[0], conn, 42, "Target")
|
||||
channels[0].Lock()
|
||||
channels[0].sessions[conn] = sess
|
||||
channels[0].Unlock()
|
||||
|
||||
reg.DisconnectUser([]uint32{42})
|
||||
|
||||
if !conn.WasClosed() {
|
||||
t.Error("DisconnectUser should have closed the connection for charID 42")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalRegistrySearchSessions(t *testing.T) {
|
||||
channels := createTestChannels(2)
|
||||
reg := NewLocalChannelRegistry(channels)
|
||||
|
||||
// Add 3 sessions across 2 channels
|
||||
for i, ch := range channels {
|
||||
conn := &mockConn{}
|
||||
sess := createTestSessionForServer(ch, conn, uint32(i+1), "Player")
|
||||
sess.stage = NewStage("sl1Ns200p0a0u0")
|
||||
ch.Lock()
|
||||
ch.sessions[conn] = sess
|
||||
ch.Unlock()
|
||||
}
|
||||
conn3 := &mockConn{}
|
||||
sess3 := createTestSessionForServer(channels[0], conn3, 3, "Player")
|
||||
sess3.stage = NewStage("sl1Ns200p0a0u0")
|
||||
channels[0].Lock()
|
||||
channels[0].sessions[conn3] = sess3
|
||||
channels[0].Unlock()
|
||||
|
||||
// Search all
|
||||
results := reg.SearchSessions(func(s SessionSnapshot) bool { return true }, 10)
|
||||
if len(results) != 3 {
|
||||
t.Errorf("SearchSessions(all) returned %d results, want 3", len(results))
|
||||
}
|
||||
|
||||
// Search with max
|
||||
results = reg.SearchSessions(func(s SessionSnapshot) bool { return true }, 2)
|
||||
if len(results) != 2 {
|
||||
t.Errorf("SearchSessions(max=2) returned %d results, want 2", len(results))
|
||||
}
|
||||
|
||||
// Search with predicate
|
||||
results = reg.SearchSessions(func(s SessionSnapshot) bool { return s.CharID == 1 }, 10)
|
||||
if len(results) != 1 {
|
||||
t.Errorf("SearchSessions(charID==1) returned %d results, want 1", len(results))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalRegistrySearchStages(t *testing.T) {
|
||||
channels := createTestChannels(1)
|
||||
reg := NewLocalChannelRegistry(channels)
|
||||
|
||||
channels[0].stagesLock.Lock()
|
||||
channels[0].stages["sl2Ls210test1"] = NewStage("sl2Ls210test1")
|
||||
channels[0].stages["sl2Ls210test2"] = NewStage("sl2Ls210test2")
|
||||
channels[0].stages["sl1Ns200other"] = NewStage("sl1Ns200other")
|
||||
channels[0].stagesLock.Unlock()
|
||||
|
||||
results := reg.SearchStages("sl2Ls210", 10)
|
||||
if len(results) != 2 {
|
||||
t.Errorf("SearchStages(sl2Ls210) returned %d results, want 2", len(results))
|
||||
}
|
||||
|
||||
results = reg.SearchStages("sl2Ls210", 1)
|
||||
if len(results) != 1 {
|
||||
t.Errorf("SearchStages(sl2Ls210, max=1) returned %d results, want 1", len(results))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalRegistryConcurrentAccess(t *testing.T) {
|
||||
channels := createTestChannels(2)
|
||||
reg := NewLocalChannelRegistry(channels)
|
||||
|
||||
// Populate some sessions
|
||||
for _, ch := range channels {
|
||||
for i := 0; i < 10; i++ {
|
||||
conn := &mockConn{remoteAddr: &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 50000 + i}}
|
||||
sess := createTestSessionForServer(ch, conn, uint32(i+1), "Player")
|
||||
sess.stage = NewStage("sl1Ns200p0a0u0")
|
||||
ch.Lock()
|
||||
ch.sessions[conn] = sess
|
||||
ch.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Run concurrent operations
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(3)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
_ = reg.FindSessionByCharID(uint32(id%10 + 1))
|
||||
}(i)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = reg.FindChannelForStage("u0")
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = reg.SearchSessions(func(s SessionSnapshot) bool { return true }, 5)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -304,11 +304,26 @@ func handleMsgMhfOperateGuildMember(s *Session, p mhfpacket.MHFPacket) {
|
||||
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
|
||||
} else {
|
||||
_ = mail.Send(s, nil)
|
||||
for _, channel := range s.server.Channels {
|
||||
for _, session := range channel.sessions {
|
||||
if session.charID == pkt.CharID {
|
||||
SendMailNotification(s, &mail, session)
|
||||
if s.server.Registry != nil {
|
||||
s.server.Registry.NotifyMailToCharID(pkt.CharID, s, &mail)
|
||||
} else {
|
||||
// Fallback: find the target session under lock, then notify outside the lock.
|
||||
var targetSession *Session
|
||||
for _, channel := range s.server.Channels {
|
||||
channel.Lock()
|
||||
for _, session := range channel.sessions {
|
||||
if session.charID == pkt.CharID {
|
||||
targetSession = session
|
||||
break
|
||||
}
|
||||
}
|
||||
channel.Unlock()
|
||||
if targetSession != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if targetSession != nil {
|
||||
SendMailNotification(s, &mail, targetSession)
|
||||
}
|
||||
}
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
|
||||
@@ -399,11 +399,17 @@ func handleMsgSysEcho(s *Session, p mhfpacket.MHFPacket) {}
|
||||
func handleMsgSysLockGlobalSema(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgSysLockGlobalSema)
|
||||
var sgid string
|
||||
for _, channel := range s.server.Channels {
|
||||
for id := range channel.stages {
|
||||
if strings.HasSuffix(id, pkt.UserIDString) {
|
||||
sgid = channel.GlobalID
|
||||
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()
|
||||
@@ -468,7 +474,23 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) {
|
||||
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
|
||||
@@ -483,31 +505,45 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
sessionName := stringsupport.UTF8ToSJIS(session.Name)
|
||||
sessionStage := stringsupport.UTF8ToSJIS(session.stage.id)
|
||||
if !local {
|
||||
resp.WriteUint32(binary.LittleEndian.Uint32(net.ParseIP(c.IP).To4()))
|
||||
} else {
|
||||
resp.WriteUint32(0x0100007F)
|
||||
}
|
||||
resp.WriteUint16(c.Port)
|
||||
resp.WriteUint32(session.charID)
|
||||
resp.WriteUint8(uint8(len(sessionStage) + 1))
|
||||
resp.WriteUint8(uint8(len(sessionName) + 1))
|
||||
resp.WriteUint16(uint16(len(c.userBinaryParts[userBinaryPartID{charID: session.charID, index: 3}])))
|
||||
|
||||
// TODO: This case might be <=G2
|
||||
if _config.ErupeConfig.RealClientMode <= _config.G1 {
|
||||
resp.WriteBytes(make([]byte, 8))
|
||||
} else {
|
||||
resp.WriteBytes(make([]byte, 40))
|
||||
}
|
||||
resp.WriteBytes(make([]byte, 8))
|
||||
|
||||
resp.WriteNullTerminatedBytes(sessionStage)
|
||||
resp.WriteNullTerminatedBytes(sessionName)
|
||||
resp.WriteBytes(c.userBinaryParts[userBinaryPartID{session.charID, 3}])
|
||||
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 _config.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 {
|
||||
@@ -594,12 +630,31 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
|
||||
@@ -621,6 +676,7 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) {
|
||||
|
||||
if findPartyParams.RankRestriction >= 0 {
|
||||
if stageData[0] > findPartyParams.RankRestriction {
|
||||
stage.RUnlock()
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -634,47 +690,72 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) {
|
||||
}
|
||||
}
|
||||
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++
|
||||
if !local {
|
||||
resp.WriteUint32(binary.LittleEndian.Uint32(net.ParseIP(c.IP).To4()))
|
||||
} else {
|
||||
resp.WriteUint32(0x0100007F)
|
||||
}
|
||||
resp.WriteUint16(c.Port)
|
||||
|
||||
resp.WriteUint16(0) // Static?
|
||||
resp.WriteUint16(0) // Unk, [0 1 2]
|
||||
resp.WriteUint16(uint16(len(stage.clients) + len(stage.reservedClientSlots)))
|
||||
resp.WriteUint16(stage.maxPlayers)
|
||||
// TODO: Retail returned the number of clients in quests, not workshop/my series
|
||||
resp.WriteUint16(uint16(len(stage.reservedClientSlots)))
|
||||
|
||||
resp.WriteUint8(0) // Static?
|
||||
resp.WriteUint8(uint8(stage.maxPlayers))
|
||||
resp.WriteUint8(1) // Static?
|
||||
resp.WriteUint8(uint8(len(stage.id) + 1))
|
||||
resp.WriteUint8(uint8(len(stage.rawBinaryData[stageBinaryKey{1, 0}])))
|
||||
resp.WriteUint8(uint8(len(stage.rawBinaryData[stageBinaryKey{1, 1}])))
|
||||
|
||||
for i := range stageData {
|
||||
if _config.ErupeConfig.RealClientMode >= _config.Z1 {
|
||||
resp.WriteInt16(stageData[i])
|
||||
} else {
|
||||
resp.WriteInt8(int8(stageData[i]))
|
||||
}
|
||||
}
|
||||
resp.WriteUint8(0) // Unk
|
||||
resp.WriteUint8(0) // Unk
|
||||
|
||||
resp.WriteNullTerminatedBytes([]byte(stage.id))
|
||||
resp.WriteBytes(stage.rawBinaryData[stageBinaryKey{1, 0}])
|
||||
resp.WriteBytes(stage.rawBinaryData[stageBinaryKey{1, 1}])
|
||||
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 _config.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)
|
||||
|
||||
@@ -37,6 +37,7 @@ type userBinaryPartID struct {
|
||||
type Server struct {
|
||||
sync.Mutex
|
||||
Channels []*Server
|
||||
Registry ChannelRegistry
|
||||
ID uint16
|
||||
GlobalID string
|
||||
IP string
|
||||
@@ -271,6 +272,10 @@ func (s *Server) BroadcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session)
|
||||
|
||||
// WorldcastMHF broadcasts a packet to all sessions across all channel servers.
|
||||
func (s *Server) WorldcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session, ignoredChannel *Server) {
|
||||
if s.Registry != nil {
|
||||
s.Registry.Worldcast(pkt, ignoredSession, ignoredChannel)
|
||||
return
|
||||
}
|
||||
for _, c := range s.Channels {
|
||||
if c == ignoredChannel {
|
||||
continue
|
||||
@@ -317,12 +322,18 @@ func (s *Server) DiscordScreenShotSend(charName string, title string, descriptio
|
||||
|
||||
// FindSessionByCharID looks up a session by character ID across all channels.
|
||||
func (s *Server) FindSessionByCharID(charID uint32) *Session {
|
||||
if s.Registry != nil {
|
||||
return s.Registry.FindSessionByCharID(charID)
|
||||
}
|
||||
for _, c := range s.Channels {
|
||||
c.Lock()
|
||||
for _, session := range c.sessions {
|
||||
if session.charID == charID {
|
||||
c.Unlock()
|
||||
return session
|
||||
}
|
||||
}
|
||||
c.Unlock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -341,7 +352,12 @@ func (s *Server) DisconnectUser(uid uint32) {
|
||||
cids = append(cids, cid)
|
||||
}
|
||||
}
|
||||
if s.Registry != nil {
|
||||
s.Registry.DisconnectUser(cids)
|
||||
return
|
||||
}
|
||||
for _, c := range s.Channels {
|
||||
c.Lock()
|
||||
for _, session := range c.sessions {
|
||||
for _, cid := range cids {
|
||||
if session.charID == cid {
|
||||
@@ -350,6 +366,7 @@ func (s *Server) DisconnectUser(uid uint32) {
|
||||
}
|
||||
}
|
||||
}
|
||||
c.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user