6 Commits

Author SHA1 Message Date
qornwh1
a53d838e24 feat : 파티 구조 뼈대 완성 2026-03-08 21:48:34 +09:00
qornwh1
a5d3b48707 feat : 파티 요청 메시지 추가 2026-03-08 21:00:24 +09:00
qornwh1
5165b9e6dc fix : playerID == hashkey 통일 / long => int로 변경 2026-03-08 20:47:23 +09:00
qornwh1
1b7f0003fb feat : 파티 패킷 구조 1개로 통일(CRUD) 2026-03-08 20:13:07 +09:00
qornwh1
76c6f46bbe feat : UuidGenerator 여러곳에서 사용하도록 싱글톤 제거 2026-03-08 17:47:49 +09:00
qornwh1
aed5d7d0b6 feat: 싱글톤 제네릭 변경 2026-03-08 17:42:12 +09:00
16 changed files with 423 additions and 122 deletions

View File

@@ -12,7 +12,7 @@ public class DummyClients
private EventBasedNetListener listener;
private NetDataWriter? writer;
public NetPeer? peer;
public long clientId; // 일단 이게 hashKey가 됨 => 0 ~ 1000번까지는 더미용응로 뺴둠
public int clientId; // 일단 이게 hashKey가 됨 => 0 ~ 1000번까지는 더미용응로 뺴둠
// info
private Vector3 position = new Vector3();
@@ -50,7 +50,7 @@ public class DummyClients
get;
}
public DummyClients(long clientId, string ip, int port, string key)
public DummyClients(int clientId, string ip, int port, string key)
{
this.clientId = clientId;
listener = new EventBasedNetListener();
@@ -139,7 +139,7 @@ public class DummyClients
UpdateDummy();
TransformPlayerPacket transformPlayerPacket = new TransformPlayerPacket
{
PlayerId = (int)clientId,
PlayerId = clientId,
RotY = rotY,
Position = new Packet.Vector3
{

View File

@@ -125,7 +125,7 @@ public class PlayerInfo
public class DummyAccTokenPacket
{
[ProtoMember(1)]
public long Token
public int Token
{
get;
set;
@@ -523,16 +523,38 @@ public class DamagePacket
public enum PartyUpdateType
{
CREATE,
DELETE
}
public enum UserPartyUpdateType
{
DELETE,
JOIN,
LEAVE
}
// UPDATE_PARTY
// REQUEST_PARTY (클라 -> 서버) - CREATE: PartyName 사용 / JOIN·LEAVE·DELETE: PartyId 사용
[ProtoContract]
public class RequestPartyPacket
{
[ProtoMember(1)]
public PartyUpdateType Type
{
get;
set;
}
[ProtoMember(2)]
public int PartyId
{
get;
set;
} // JOIN, LEAVE, DELETE 시 사용
[ProtoMember(3)]
public string PartyName
{
get;
set;
} // CREATE 시 사용
}
// UPDATE_PARTY (서버 -> 클라) - 파티 생성/삭제: LeaderId 사용 / 파티원 추가/제거: PlayerId 사용
[ProtoContract]
public class UpdatePartyPacket
{
@@ -556,30 +578,18 @@ public class UpdatePartyPacket
get;
set;
}
}
// UPDATE_USER_PARTY
[ProtoContract]
public class UpdateUserPartyPacket
{
[ProtoMember(1)]
public int PartyId
{
get;
set;
}
[ProtoMember(2)]
[ProtoMember(4)]
public int PlayerId
{
get;
set;
}
[ProtoMember(3)]
public UserPartyUpdateType Type
[ProtoMember(5)]
public string PartyName
{
get;
set;
}
} // CREATE일 때 사용
}

View File

@@ -48,11 +48,11 @@ public enum PacketCode : ushort
// 데미지 UI 전달 (서버 -> 클라)
DAMAGE,
// 파티 (생성, 삭제)
// 파티 생성/삭제, 파티원 추가/제거 (서버 -> 클라)
UPDATE_PARTY,
// 파티 유저 업데이트(추가 삭제)
UPDATE_USER_PARTY
// 파티 참가/탈퇴/생성/해산 요청 (클라 -> 서버)
REQUEST_PARTY
}
public class PacketHeader

View File

@@ -18,7 +18,7 @@ public class StressTestClient
private EventBasedNetListener listener;
private NetDataWriter? writer;
public NetPeer? peer;
public long clientId;
public int clientId;
// 이동
private Vector3 position = new Vector3();
@@ -47,7 +47,7 @@ public class StressTestClient
public double AvgRttMs => RttCount > 0 ? TotalRttMs / RttCount : 0;
public bool IsConnected => peer != null;
public StressTestClient(long clientId, string ip, int port, string key)
public StressTestClient(int clientId, string ip, int port, string key)
{
this.clientId = clientId;
listener = new EventBasedNetListener();
@@ -150,7 +150,7 @@ public class StressTestClient
// 전송
TransformPlayerPacket pkt = new TransformPlayerPacket
{
PlayerId = (int)clientId,
PlayerId = clientId,
RotY = rotY,
Position = new Packet.Vector3 { X = position.X, Y = 0, Z = position.Z }
};

View File

@@ -107,7 +107,7 @@ public class StressTestService
for (int i = 0; i < batch; i++)
{
long id = created + 1; // 1-based (더미 범위)
int id = created + 1; // 1-based (더미 범위)
StressTestClient client = new StressTestClient(id, ip, port, key);
lock (clientsLock)
{

View File

@@ -9,10 +9,10 @@ public class Channel
private Robby robby = new Robby();
// 채널 내 유저 상태 (hashKey → Player)
private Dictionary<long, Player> connectUsers = new Dictionary<long, Player>();
private Dictionary<int, Player> connectUsers = new Dictionary<int, Player>();
// 채널 내 유저 NetPeer (hashKey → NetPeer) — BroadcastToChannel 교차 조회 제거용
private Dictionary<long, NetPeer> connectPeers = new Dictionary<long, NetPeer>();
private Dictionary<int, NetPeer> connectPeers = new Dictionary<int, NetPeer>();
public int ChannelId
{
@@ -37,14 +37,14 @@ public class Channel
ChannelId = channelId;
}
public void AddUser(long userId, Player player, NetPeer peer)
public void AddUser(int userId, Player player, NetPeer peer)
{
connectUsers[userId] = player;
connectPeers[userId] = peer;
UserCount++;
}
public void RemoveUser(long userId)
public void RemoveUser(int userId)
{
connectUsers.Remove(userId);
connectPeers.Remove(userId);
@@ -52,13 +52,13 @@ public class Channel
}
// 재연결(WiFi→LTE 등) 시 동일 유저의 peer 교체
public void UpdatePeer(long userId, NetPeer peer)
public void UpdatePeer(int userId, NetPeer peer)
{
connectPeers[userId] = peer;
}
// 채널 내 모든 유저의 hashKey 반환 (채널 입장 시 기존 플레이어 목록 조회용)
public IEnumerable<long> GetConnectUsers()
public IEnumerable<int> GetConnectUsers()
{
return connectUsers.Keys;
}
@@ -76,13 +76,13 @@ public class Channel
}
// 특정 유저의 Player 반환
public Player? GetPlayer(long userId)
public Player? GetPlayer(int userId)
{
connectUsers.TryGetValue(userId, out Player? player);
return player;
}
public int HasUser(long userId)
public int HasUser(int userId)
{
if (connectUsers.ContainsKey(userId))
{

View File

@@ -1,20 +1,15 @@
using LiteNetLib;
using LiteNetLib;
using MMOserver.Utils;
namespace MMOserver.Game.Channel;
public class ChannelManager
public class ChannelManager : Singleton<ChannelManager>
{
// 일단은 채널은 서버 켤때 고정으로간다 1개
public static ChannelManager Instance
{
get;
} = new ChannelManager();
// 채널 관리
private List<Channel> channels = new List<Channel>();
// 채널별 유저 관리 (유저 key, 채널 val)
private Dictionary<long, int> connectUsers = new Dictionary<long, int>();
private Dictionary<int, int> connectUsers = new Dictionary<int, int>();
public ChannelManager()
{
@@ -39,7 +34,7 @@ public class ChannelManager
return channels;
}
public void AddUser(int channelId, long userId, Player player, NetPeer peer)
public void AddUser(int channelId, int userId, Player player, NetPeer peer)
{
// 유저 추가
connectUsers.Add(userId, channelId);
@@ -47,7 +42,7 @@ public class ChannelManager
channels[channelId].AddUser(userId, player, peer);
}
public bool RemoveUser(long userId)
public bool RemoveUser(int userId)
{
// 채널에 없는 유저면 스킵
if (!connectUsers.TryGetValue(userId, out int channelId))
@@ -66,7 +61,7 @@ public class ChannelManager
return false;
}
public int HasUser(long userId)
public int HasUser(int userId)
{
int channelId = -1;
if (connectUsers.ContainsKey(userId))
@@ -82,7 +77,7 @@ public class ChannelManager
return channelId;
}
public Dictionary<long, int> GetConnectUsers()
public Dictionary<int, int> GetConnectUsers()
{
return connectUsers;
}

View File

@@ -2,6 +2,7 @@ using LiteNetLib;
using LiteNetLib.Utils;
using MMOserver.Api;
using MMOserver.Game.Channel;
using MMOserver.Game.Party;
using MMOserver.Packet;
using MMOserver.Utils;
using ProtoBuf;
@@ -13,18 +14,21 @@ namespace MMOserver.Game;
public class GameServer : ServerBase
{
private readonly Dictionary<ushort, Action<NetPeer, long, byte[]>> packetHandlers;
private readonly Dictionary<ushort, Action<NetPeer, int, byte[]>> packetHandlers;
private UuidGenerator userUuidGenerator;
public GameServer(int port, string connectionString) : base(port, connectionString)
{
packetHandlers = new Dictionary<ushort, Action<NetPeer, long, byte[]>>
packetHandlers = new Dictionary<ushort, Action<NetPeer, int, byte[]>>
{
[(ushort)PacketCode.INTO_CHANNEL] = OnIntoChannel,
[(ushort)PacketCode.EXIT_CHANNEL] = OnExitChannel,
[(ushort)PacketCode.TRANSFORM_PLAYER] = OnTransformPlayer,
[(ushort)PacketCode.ACTION_PLAYER] = OnActionPlayer,
[(ushort)PacketCode.STATE_PLAYER] = OnStatePlayer,
[(ushort)PacketCode.REQUEST_PARTY] = OnRequestParty,
};
userUuidGenerator = new UuidGenerator();
}
protected override void HandleEcho(NetPeer peer, byte[] payload)
@@ -49,7 +53,7 @@ public class GameServer : ServerBase
protected override void HandleAuthDummy(NetPeer peer, byte[] payload)
{
DummyAccTokenPacket accTokenPacket = Serializer.Deserialize<DummyAccTokenPacket>(new ReadOnlyMemory<byte>(payload));
long hashKey = accTokenPacket.Token;
int hashKey = accTokenPacket.Token;
if (sessions.TryGetValue(hashKey, out NetPeer? existing))
{
@@ -71,8 +75,8 @@ public class GameServer : ServerBase
Player newPlayer = new Player
{
HashKey = hashKey,
PlayerId = (int)(hashKey & 0x7FFFFFFF),
Nickname = (hashKey & 0x7FFFFFFF).ToString()
PlayerId = hashKey,
Nickname = hashKey.ToString()
};
cm.AddUser(1, hashKey, newPlayer, peer);
@@ -92,10 +96,10 @@ public class GameServer : ServerBase
{
AccTokenPacket accTokenPacket = Serializer.Deserialize<AccTokenPacket>(new ReadOnlyMemory<byte>(payload));
string token = accTokenPacket.Token;
tokenHash.TryGetValue(token, out long hashKey);
tokenHash.TryGetValue(token, out int hashKey);
if (hashKey <= 1000)
{
hashKey = UuidGeneratorManager.Instance.Create();
hashKey = userUuidGenerator.Create();
}
if (sessions.TryGetValue(hashKey, out NetPeer? existing))
@@ -113,7 +117,7 @@ public class GameServer : ServerBase
if (username == null)
{
Log.Warning("[Server] 토큰 검증 실패 - 연결 거부 PeerId={Id}", peer.Id);
UuidGeneratorManager.Instance.Release(hashKey);
userUuidGenerator.Release(hashKey);
peer.Disconnect();
return;
}
@@ -131,7 +135,7 @@ public class GameServer : ServerBase
OnSessionConnected(peer, hashKey);
}
protected override void OnSessionConnected(NetPeer peer, long hashKey)
protected override void OnSessionConnected(NetPeer peer, int hashKey)
{
Log.Information("[GameServer] 세션 연결 HashKey={Key} PeerId={Id}", hashKey, peer.Id);
@@ -152,7 +156,7 @@ public class GameServer : ServerBase
}
}
protected override void OnSessionDisconnected(NetPeer peer, long hashKey, DisconnectInfo info)
protected override void OnSessionDisconnected(NetPeer peer, int hashKey, DisconnectInfo info)
{
ChannelManager cm = ChannelManager.Instance;
@@ -171,12 +175,12 @@ public class GameServer : ServerBase
}
}
UuidGeneratorManager.Instance.Release(hashKey);
userUuidGenerator.Release(hashKey);
}
protected override void HandlePacket(NetPeer peer, long hashKey, ushort type, byte[] payload)
protected override void HandlePacket(NetPeer peer, int hashKey, ushort type, byte[] payload)
{
if (packetHandlers.TryGetValue(type, out Action<NetPeer, long, byte[]>? handler))
if (packetHandlers.TryGetValue(type, out Action<NetPeer, int, byte[]>? handler))
{
handler(peer, hashKey, payload);
}
@@ -190,7 +194,7 @@ public class GameServer : ServerBase
// 보내는 패킷
// ============================================================
private void SendLoadChannelPacket(NetPeer peer, long hashKey)
private void SendLoadChannelPacket(NetPeer peer, int hashKey)
{
LoadChannelPacket loadChannelPacket = new LoadChannelPacket();
foreach (Channel.Channel channel in ChannelManager.Instance.GetChannels())
@@ -207,7 +211,7 @@ public class GameServer : ServerBase
}
// 나간 유저를 같은 채널 유저들에게 알림 (UPDATE_CHANNEL_USER IsAdd=false)
private void SendExitChannelPacket(NetPeer peer, long hashKey, int channelId, Player player)
private void SendExitChannelPacket(NetPeer peer, int hashKey, int channelId, Player player)
{
UpdateChannelUserPacket packet = new UpdateChannelUserPacket
{
@@ -222,7 +226,7 @@ public class GameServer : ServerBase
// 채널 입장 시 패킷 전송
// - 새 유저에게 : 기존 채널 유저 목록 (INTO_CHANNEL)
// - 기존 유저들에게 : 새 유저 입장 알림 (UPDATE_CHANNEL_USER IsAdd=true)
private void SendIntoChannelPacket(NetPeer peer, long hashKey)
private void SendIntoChannelPacket(NetPeer peer, int hashKey)
{
ChannelManager cm = ChannelManager.Instance;
int channelId = cm.HasUser(hashKey);
@@ -236,7 +240,7 @@ public class GameServer : ServerBase
// 1. 새 유저에게: 자신을 제외한 기존 채널 유저 목록 전송
IntoChannelPacket response = new IntoChannelPacket { ChannelId = channelId };
foreach (long userId in channel.GetConnectUsers())
foreach (int userId in channel.GetConnectUsers())
{
if (userId == hashKey)
{
@@ -268,7 +272,7 @@ public class GameServer : ServerBase
// 채널 입장 시 패킷 전송
// - 자신 유저에게 : 내 정보 (LOAD_GAME)
private void SendLoadGame(NetPeer peer, long hashKey)
private void SendLoadGame(NetPeer peer, int hashKey)
{
ChannelManager cm = ChannelManager.Instance;
int channelId = cm.HasUser(hashKey);
@@ -338,7 +342,7 @@ public class GameServer : ServerBase
// 패킷 핸들러
// ============================================================
private void OnIntoChannel(NetPeer peer, long hashKey, byte[] payload)
private void OnIntoChannel(NetPeer peer, int hashKey, byte[] payload)
{
IntoChannelPacket packet = Serializer.Deserialize<IntoChannelPacket>(new ReadOnlyMemory<byte>(payload));
@@ -359,8 +363,8 @@ public class GameServer : ServerBase
Player newPlayer = new Player
{
HashKey = hashKey,
PlayerId = (int)(hashKey & 0x7FFFFFFF),
Nickname = (hashKey & 0x7FFFFFFF).ToString()
PlayerId = hashKey,
Nickname = hashKey.ToString()
};
cm.AddUser(packet.ChannelId, hashKey, newPlayer, peer);
@@ -373,7 +377,7 @@ public class GameServer : ServerBase
SendLoadGame(peer, hashKey);
}
private void OnExitChannel(NetPeer peer, long hashKey, byte[] payload)
private void OnExitChannel(NetPeer peer, int hashKey, byte[] payload)
{
ExitChannelPacket packet = Serializer.Deserialize<ExitChannelPacket>(new ReadOnlyMemory<byte>(payload));
@@ -393,7 +397,7 @@ public class GameServer : ServerBase
}
}
private void OnTransformPlayer(NetPeer peer, long hashKey, byte[] payload)
private void OnTransformPlayer(NetPeer peer, int hashKey, byte[] payload)
{
TransformPlayerPacket packet = Serializer.Deserialize<TransformPlayerPacket>(new ReadOnlyMemory<byte>(payload));
@@ -419,7 +423,7 @@ public class GameServer : ServerBase
BroadcastToChannel(channelId, data, peer, DeliveryMethod.Unreliable);
}
private void OnActionPlayer(NetPeer peer, long hashKey, byte[] payload)
private void OnActionPlayer(NetPeer peer, int hashKey, byte[] payload)
{
ActionPlayerPacket packet = Serializer.Deserialize<ActionPlayerPacket>(new ReadOnlyMemory<byte>(payload));
@@ -435,7 +439,7 @@ public class GameServer : ServerBase
BroadcastToChannel(channelId, data, peer);
}
private void OnStatePlayer(NetPeer peer, long hashKey, byte[] payload)
private void OnStatePlayer(NetPeer peer, int hashKey, byte[] payload)
{
StatePlayerPacket packet = Serializer.Deserialize<StatePlayerPacket>(new ReadOnlyMemory<byte>(payload));
@@ -460,4 +464,99 @@ public class GameServer : ServerBase
byte[] data = PacketSerializer.Serialize<StatePlayerPacket>((ushort)PacketCode.STATE_PLAYER, packet);
BroadcastToChannel(channelId, data, peer);
}
private void OnRequestParty(NetPeer peer, int hashKey, byte[] payload)
{
RequestPartyPacket req = Serializer.Deserialize<RequestPartyPacket>(new ReadOnlyMemory<byte>(payload));
PartyManager pm = PartyManager.Instance;
switch (req.Type)
{
case PartyUpdateType.CREATE:
{
if (!pm.CreateParty(hashKey, req.PartyName, out PartyInfo? party))
{
return;
}
UpdatePartyPacket notify = new UpdatePartyPacket
{
PartyId = party!.PartyId,
Type = PartyUpdateType.CREATE,
LeaderId = party.LeaderId,
PlayerId = hashKey,
PartyName = party.PartyName,
};
byte[] data = PacketSerializer.Serialize<UpdatePartyPacket>((ushort)PacketCode.UPDATE_PARTY, notify);
SendTo(peer, data);
break;
}
case PartyUpdateType.JOIN:
{
if (!pm.JoinParty(hashKey, req.PartyId, out PartyInfo? party))
{
return;
}
UpdatePartyPacket notify = new UpdatePartyPacket
{
PartyId = party!.PartyId,
Type = PartyUpdateType.JOIN,
LeaderId = party.LeaderId,
PlayerId = hashKey,
};
byte[] data = PacketSerializer.Serialize<UpdatePartyPacket>((ushort)PacketCode.UPDATE_PARTY, notify);
BroadcastToUsers(party.PartyMemberIds, data); // 새 멤버 포함 전원
break;
}
case PartyUpdateType.LEAVE:
{
if (!pm.LeaveParty(hashKey, out PartyInfo? party))
{
return;
}
UpdatePartyPacket notify = new UpdatePartyPacket
{
PartyId = req.PartyId,
Type = PartyUpdateType.LEAVE,
LeaderId = party?.LeaderId ?? 0,
PlayerId = hashKey,
};
byte[] data = PacketSerializer.Serialize<UpdatePartyPacket>((ushort)PacketCode.UPDATE_PARTY, notify);
if (party != null)
BroadcastToUsers(party.PartyMemberIds, data); // 남은 멤버들에게
SendTo(peer, data); // 탈퇴자 본인에게
break;
}
case PartyUpdateType.DELETE:
{
if (!pm.DeleteParty(hashKey, req.PartyId, out PartyInfo? party))
{
return;
}
UpdatePartyPacket notify = new UpdatePartyPacket
{
PartyId = party!.PartyId,
Type = PartyUpdateType.DELETE,
LeaderId = party.LeaderId,
};
byte[] data = PacketSerializer.Serialize<UpdatePartyPacket>((ushort)PacketCode.UPDATE_PARTY, notify);
BroadcastToUsers(party.PartyMemberIds, data); // 전원 (리더 포함)
break;
}
}
}
private void BroadcastToUsers(IEnumerable<int> userIds, byte[] data, DeliveryMethod method = DeliveryMethod.ReliableOrdered)
{
foreach (int userId in userIds)
{
if (sessions.TryGetValue(userId, out NetPeer? targetPeer))
{
SendTo(targetPeer, data, method);
}
}
}
}

View File

@@ -0,0 +1,35 @@
namespace MMOserver.Game.Party;
public class PartyInfo
{
public static readonly int partyMemberMax = 3;
public int PartyId
{
get;
set;
}
public int LeaderId
{
get;
set;
}
public List<int> PartyMemberIds
{
get;
set;
} = new List<int>();
public string PartyName
{
get;
set;
} = "";
public int GetPartyMemberCount()
{
return PartyMemberIds.Count;
}
}

View File

@@ -0,0 +1,152 @@
using MMOserver.Utils;
namespace MMOserver.Game.Party;
public class PartyManager : Singleton<PartyManager>
{
private readonly UuidGenerator partyUuidGenerator = new UuidGenerator();
// partyId → PartyInfo
private readonly Dictionary<int, PartyInfo> parties = new();
// playerId → partyId (한 플레이어는 하나의 파티만)
private readonly Dictionary<int, int> playerPartyMap = new();
// 파티 생성
public bool CreateParty(int leaderId, string partyName, out PartyInfo? party)
{
party = null;
if (playerPartyMap.ContainsKey(leaderId))
{
return false; // 이미 파티에 속해있음
}
int partyId = partyUuidGenerator.Create();
party = new PartyInfo
{
PartyId = partyId,
LeaderId = leaderId,
PartyName = partyName,
};
party.PartyMemberIds.Add(leaderId);
parties[partyId] = party;
playerPartyMap[leaderId] = partyId;
return true;
}
// 파티 참가
public bool JoinParty(int playerId, int partyId, out PartyInfo? party)
{
party = null;
if (playerPartyMap.ContainsKey(playerId))
{
return false; // 이미 파티에 속해있음
}
if (!parties.TryGetValue(partyId, out party))
{
return false; // 파티 없음
}
if (party.GetPartyMemberCount() >= PartyInfo.partyMemberMax)
{
party = null;
return false; // 파티 인원 초과
}
party.PartyMemberIds.Add(playerId);
playerPartyMap[playerId] = partyId;
return true;
}
// 파티 탈퇴
public bool LeaveParty(int playerId, out PartyInfo? party)
{
party = null;
if (!playerPartyMap.TryGetValue(playerId, out int partyId))
{
return false; // 파티에 속해있지 않음
}
if (!parties.TryGetValue(partyId, out party))
{
return false;
}
party.PartyMemberIds.Remove(playerId);
playerPartyMap.Remove(playerId);
// 마지막 멤버가 나가면 파티 해산
if (party.PartyMemberIds.Count == 0)
{
DeletePartyInternal(partyId, party);
party = null;
return true;
}
// 리더가 나갔으면 남은 멤버 중 첫 번째를 리더로 승계
if (party.LeaderId == playerId)
{
party.LeaderId = party.PartyMemberIds[0];
}
return true;
}
// 파티 해산 (리더만)
public bool DeleteParty(int leaderId, int partyId, out PartyInfo? party)
{
party = null;
if (!parties.TryGetValue(partyId, out party))
{
return false;
}
if (party.LeaderId != leaderId)
{
party = null;
return false; // 리더만 해산 가능
}
DeletePartyInternal(partyId, party);
return true;
}
// 조회
public PartyInfo? GetParty(int partyId)
{
parties.TryGetValue(partyId, out PartyInfo? party);
return party;
}
public PartyInfo? GetPartyByPlayer(int playerId)
{
if (!playerPartyMap.TryGetValue(playerId, out int partyId))
{
return null;
}
parties.TryGetValue(partyId, out PartyInfo? party);
return party;
}
// 삭제
private void DeletePartyInternal(int partyId, PartyInfo party)
{
// 남아 있는인원도 제거
foreach (int memberId in party.PartyMemberIds)
{
playerPartyMap.Remove(memberId);
}
parties.Remove(partyId);
partyUuidGenerator.Release(partyId);
}
}

View File

@@ -2,7 +2,7 @@ namespace MMOserver.Game;
public class Player
{
public long HashKey
public int HashKey
{
get;
set;

View File

@@ -125,7 +125,7 @@ public class PlayerInfo
public class DummyAccTokenPacket
{
[ProtoMember(1)]
public long Token
public int Token
{
get;
set;
@@ -523,16 +523,38 @@ public class DamagePacket
public enum PartyUpdateType
{
CREATE,
DELETE
}
public enum UserPartyUpdateType
{
DELETE,
JOIN,
LEAVE
}
// UPDATE_PARTY
// REQUEST_PARTY (클라 -> 서버) - CREATE: PartyName 사용 / JOIN·LEAVE·DELETE: PartyId 사용
[ProtoContract]
public class RequestPartyPacket
{
[ProtoMember(1)]
public PartyUpdateType Type
{
get;
set;
}
[ProtoMember(2)]
public int PartyId
{
get;
set;
} // JOIN, LEAVE, DELETE 시 사용
[ProtoMember(3)]
public string PartyName
{
get;
set;
} // CREATE 시 사용
}
// UPDATE_PARTY (서버 -> 클라) - 파티 생성/삭제: LeaderId 사용 / 파티원 추가/제거: PlayerId 사용
[ProtoContract]
public class UpdatePartyPacket
{
@@ -556,30 +578,18 @@ public class UpdatePartyPacket
get;
set;
}
}
// UPDATE_USER_PARTY
[ProtoContract]
public class UpdateUserPartyPacket
{
[ProtoMember(1)]
public int PartyId
{
get;
set;
}
[ProtoMember(2)]
[ProtoMember(4)]
public int PlayerId
{
get;
set;
}
[ProtoMember(3)]
public UserPartyUpdateType Type
[ProtoMember(5)]
public string PartyName
{
get;
set;
}
} // CREATE일 때 사용
}

View File

@@ -48,11 +48,11 @@ public enum PacketCode : ushort
// 데미지 UI 전달 (서버 -> 클라)
DAMAGE,
// 파티 (생성, 삭제)
// 파티 생성/삭제, 파티원 추가/제거 (서버 -> 클라)
UPDATE_PARTY,
// 파티 유저 업데이트(추가 삭제)
UPDATE_USER_PARTY
// 파티 참가/탈퇴/생성/해산 요청 (클라 -> 서버)
REQUEST_PARTY
}
public class PacketHeader

View File

@@ -1,22 +1,22 @@
namespace MMOserver.Utils;
public class UuidGeneratorManager : Singleton<UuidGeneratorManager>
public class UuidGenerator
{
// 0 ~ 1000 은 더미 클라이언트 예약 범위
private const long DUMMY_RANGE_MAX = 1000;
private const int DUMMY_RANGE_MAX = 1000;
private readonly object idLock = new();
private readonly HashSet<long> usedIds = new();
private readonly HashSet<int> usedIds = new();
// 고유 랜덤 long ID 발급 (1001번 이상, 충돌 시 재생성)
public long Create()
// 고유 랜덤 int ID 발급 (1001번 이상, 충돌 시 재생성)
public int Create()
{
lock (idLock)
{
long id;
int id;
do
{
id = Random.Shared.NextInt64(DUMMY_RANGE_MAX + 1, long.MaxValue);
id = Random.Shared.Next(DUMMY_RANGE_MAX + 1, int.MaxValue);
} while (usedIds.Contains(id));
usedIds.Add(id);
@@ -25,7 +25,7 @@ public class UuidGeneratorManager : Singleton<UuidGeneratorManager>
}
// 로그아웃 / 세션 만료 시 ID 반납
public bool Release(long id)
public bool Release(int id)
{
lock (idLock)
{

View File

@@ -15,7 +15,7 @@ namespace ServerLib.Service;
///
/// 흐름:
/// OnPeerConnected → 대기 목록 등록
/// OnNetworkReceive → Auth 패킷(type=1)이면 HashKey(8byte long) 읽어 인증
/// OnNetworkReceive → Auth 패킷(type=1)이면 HashKey(4byte int) 읽어 인증
/// → 이미 같은 HashKey 세션 있으면 이전 피어 끊고 재연결 (WiFi→LTE)
/// → 그 외 패킷은 HandlePacket() 으로 전달
/// OnPeerDisconnected → 세션/대기 목록에서 제거
@@ -34,10 +34,10 @@ public abstract class ServerBase : INetEventListener
// 인증된 세션 (hashKey → NetPeer) 재연결 조회용
// peer → hashKey 역방향은 peer.Tag as Session 으로 대체
protected readonly Dictionary<long, NetPeer> sessions = new();
protected readonly Dictionary<int, NetPeer> sessions = new();
// Token / HashKey 관리
protected readonly Dictionary<string, long> tokenHash = new();
protected readonly Dictionary<string, int> tokenHash = new();
// 재사용 NetDataWriter (단일 스레드 폴링이므로 안전)
private readonly NetDataWriter cachedWriter = new();
@@ -271,11 +271,11 @@ public abstract class ServerBase : INetEventListener
// ─── 서브클래스 구현 ─────────────────────────────────────────────────
// 인증(Auth) 완료 후 호출
protected abstract void OnSessionConnected(NetPeer peer, long hashKey);
protected abstract void OnSessionConnected(NetPeer peer, int hashKey);
// 세션 정상 해제 시 호출 (재연결 교체 시에는 호출되지 않음)
protected abstract void OnSessionDisconnected(NetPeer peer, long hashKey, DisconnectInfo info);
protected abstract void OnSessionDisconnected(NetPeer peer, int hashKey, DisconnectInfo info);
// 인증된 피어의 게임 패킷 수신 / payload는 헤더 제거된 raw bytes → 실행 프로젝트에서 protobuf 역직렬화
protected abstract void HandlePacket(NetPeer peer, long hashKey, ushort type, byte[] payload);
protected abstract void HandlePacket(NetPeer peer, int hashKey, ushort type, byte[] payload);
}

View File

@@ -10,7 +10,7 @@ public class Session
set;
}
public long HashKey
public int HashKey
{
get;
init;
@@ -63,7 +63,7 @@ public class Session
RateLimitViolations = 0;
}
public Session(long hashKey, NetPeer peer, int maxPacketsPerSecond = 60)
public Session(int hashKey, NetPeer peer, int maxPacketsPerSecond = 60)
{
HashKey = hashKey;
Peer = peer;