Fix: 플레이어 프로필 DB 연동, 파티 초대/추방 프로토콜 구현
- 채널 입장 시 API 서버에서 플레이어 프로필 로드 (레벨/스탯/위치) - 채널 퇴장 시 위치/플레이타임 DB 저장 (SaveGameDataAsync) - Player.cs에 AttackPower/AttackRange/SprintMultiplier/Experience 필드 추가 - ToPlayerInfo에서 전투 스탯 매핑 추가 - Session에 ChannelJoinedAt 추가 (플레이타임 계산용) - PartyUpdateType에 INVITE/KICK 추가 - RequestPartyPacket에 TargetPlayerId 필드 추가 - GameServer에 INVITE/KICK 핸들러 구현 - Channel에 GetPeer() 메서드 추가 - RestApi에 GetPlayerProfileAsync/SaveGameDataAsync 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -663,7 +663,9 @@ public enum PartyUpdateType
|
||||
DELETE,
|
||||
JOIN,
|
||||
LEAVE,
|
||||
UPDATE
|
||||
UPDATE,
|
||||
INVITE,
|
||||
KICK
|
||||
}
|
||||
|
||||
// REQUEST_PARTY (클라 -> 서버) - CREATE: PartyName 사용 / JOIN·LEAVE·DELETE: PartyId 사용
|
||||
|
||||
@@ -129,6 +129,18 @@ public class RestApi : Singleton<RestApi>
|
||||
|
||||
[JsonPropertyName("sprintMultiplier")]
|
||||
public double SprintMultiplier { get; set; }
|
||||
|
||||
[JsonPropertyName("lastPosX")]
|
||||
public double LastPosX { get; set; }
|
||||
|
||||
[JsonPropertyName("lastPosY")]
|
||||
public double LastPosY { get; set; }
|
||||
|
||||
[JsonPropertyName("lastPosZ")]
|
||||
public double LastPosZ { get; set; }
|
||||
|
||||
[JsonPropertyName("lastRotY")]
|
||||
public double LastRotY { get; set; }
|
||||
}
|
||||
|
||||
// 레이드 채널 접속 여부 체크
|
||||
@@ -192,6 +204,39 @@ public class RestApi : Singleton<RestApi>
|
||||
return null;
|
||||
}
|
||||
|
||||
// 게임 데이터 저장 (채널 퇴장 시 위치/플레이타임 저장)
|
||||
public async Task<bool> SaveGameDataAsync(string username, float? posX, float? posY, float? posZ, float? rotY, long? playTimeDelta)
|
||||
{
|
||||
string url = AppConfig.RestApi.BaseUrl + "/api/internal/player/save?username=" + Uri.EscapeDataString(username);
|
||||
|
||||
var body = new Dictionary<string, object?>();
|
||||
if (posX.HasValue) body["lastPosX"] = posX.Value;
|
||||
if (posY.HasValue) body["lastPosY"] = posY.Value;
|
||||
if (posZ.HasValue) body["lastPosZ"] = posZ.Value;
|
||||
if (rotY.HasValue) body["lastRotY"] = rotY.Value;
|
||||
if (playTimeDelta.HasValue) body["playTimeDelta"] = playTimeDelta.Value;
|
||||
|
||||
for (int attempt = 1; attempt <= MAX_RETRY; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response = await httpClient.PostAsJsonAsync(url, body);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (attempt < MAX_RETRY)
|
||||
{
|
||||
Log.Warning("[RestApi] 게임 데이터 저장 실패 (시도 {Attempt}/{Max}): {Message}", attempt, MAX_RETRY, ex.Message);
|
||||
await Task.Delay(RETRY_DELAY);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("[RestApi] 게임 데이터 저장 최종 실패 ({Max}회 시도): {Message}", MAX_RETRY, ex.Message);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private sealed class BossRaidAccessResponse
|
||||
{
|
||||
[JsonPropertyName("roomId")]
|
||||
|
||||
@@ -137,6 +137,13 @@ public class Channel
|
||||
return player;
|
||||
}
|
||||
|
||||
// 특정 유저의 NetPeer 반환
|
||||
public NetPeer? GetPeer(int userId)
|
||||
{
|
||||
connectPeers.TryGetValue(userId, out NetPeer? peer);
|
||||
return peer;
|
||||
}
|
||||
|
||||
public int HasUser(int userId)
|
||||
{
|
||||
if (connectUsers.ContainsKey(userId))
|
||||
|
||||
@@ -207,6 +207,21 @@ public class GameServer : ServerBase
|
||||
int channelId = cm.HasUser(hashKey);
|
||||
Player? player = channelId >= 0 ? cm.GetChannel(channelId).GetPlayer(hashKey) : null;
|
||||
|
||||
// 퇴장 시 위치/플레이타임 DB 저장 (fire-and-forget)
|
||||
if (player != null && peer.Tag is Session session && session.Username != null)
|
||||
{
|
||||
long playTimeDelta = 0;
|
||||
if (session.ChannelJoinedAt != default)
|
||||
{
|
||||
playTimeDelta = (long)(DateTime.UtcNow - session.ChannelJoinedAt).TotalSeconds;
|
||||
}
|
||||
_ = RestApi.Instance.SaveGameDataAsync(
|
||||
session.Username,
|
||||
player.PosX, player.PosY, player.PosZ, player.RotY,
|
||||
playTimeDelta > 0 ? playTimeDelta : null
|
||||
);
|
||||
}
|
||||
|
||||
if (cm.RemoveUser(hashKey))
|
||||
{
|
||||
Log.Information("[GameServer] 세션 해제 HashKey={Key} Reason={Reason}", hashKey, info.Reason);
|
||||
@@ -524,6 +539,10 @@ public class GameServer : ServerBase
|
||||
newPlayer.AttackPower = (float)profile.AttackPower;
|
||||
newPlayer.AttackRange = (float)profile.AttackRange;
|
||||
newPlayer.SprintMultiplier = (float)profile.SprintMultiplier;
|
||||
newPlayer.PosX = (float)profile.LastPosX;
|
||||
newPlayer.PosY = (float)profile.LastPosY;
|
||||
newPlayer.PosZ = (float)profile.LastPosZ;
|
||||
newPlayer.RotY = (float)profile.LastRotY;
|
||||
Log.Information("[GameServer] 프로필 로드 완료 Username={Username} Level={Level} MaxHp={MaxHp}",
|
||||
username, profile.Level, profile.MaxHp);
|
||||
}
|
||||
@@ -540,6 +559,10 @@ public class GameServer : ServerBase
|
||||
}
|
||||
}
|
||||
|
||||
// 채널 입장 시각 기록 (플레이타임 계산용)
|
||||
((Session)peer.Tag).ChannelJoinedAt = DateTime.UtcNow;
|
||||
|
||||
// 채널에 추가
|
||||
cm.AddUser(packet.ChannelId, hashKey, newPlayer, peer);
|
||||
Log.Debug("[GameServer] INTO_CHANNEL HashKey={Key} ChannelId={ChannelId}", hashKey, packet.ChannelId);
|
||||
|
||||
@@ -921,6 +944,84 @@ public class GameServer : ServerBase
|
||||
BroadcastToChannel(channelId, data); // 채널 전체 파티 목록 갱신
|
||||
break;
|
||||
}
|
||||
case PartyUpdateType.INVITE:
|
||||
{
|
||||
// 리더만 초대 가능
|
||||
PartyInfo? myParty = pm.GetPartyByPlayer(hashKey);
|
||||
if (myParty == null || myParty.LeaderId != hashKey)
|
||||
{
|
||||
SendError(peer, ErrorCode.PARTY_JOIN_FAILED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (myParty.GetPartyMemberCount() >= PartyInfo.partyMemberMax)
|
||||
{
|
||||
SendError(peer, ErrorCode.PARTY_JOIN_FAILED);
|
||||
return;
|
||||
}
|
||||
|
||||
// 대상 플레이어가 같은 채널에 있는지 확인
|
||||
int targetId = req.TargetPlayerId;
|
||||
Channel.Channel? ch = cm.GetChannel(channelId);
|
||||
if (ch == null || ch.GetPlayer(targetId) == null)
|
||||
{
|
||||
SendError(peer, ErrorCode.PARTY_JOIN_FAILED);
|
||||
return;
|
||||
}
|
||||
|
||||
// 대상에게 초대 알림 전송
|
||||
NetPeer? targetPeer = ch.GetPeer(targetId);
|
||||
if (targetPeer == null)
|
||||
{
|
||||
SendError(peer, ErrorCode.PARTY_JOIN_FAILED);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdatePartyPacket inviteNotify = new()
|
||||
{
|
||||
PartyId = myParty.PartyId,
|
||||
Type = PartyUpdateType.INVITE,
|
||||
LeaderId = hashKey,
|
||||
PlayerId = targetId,
|
||||
PartyName = myParty.PartyName
|
||||
};
|
||||
byte[] inviteData = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, inviteNotify);
|
||||
SendTo(targetPeer, inviteData);
|
||||
break;
|
||||
}
|
||||
case PartyUpdateType.KICK:
|
||||
{
|
||||
// 리더만 추방 가능
|
||||
PartyInfo? myParty2 = pm.GetPartyByPlayer(hashKey);
|
||||
if (myParty2 == null || myParty2.LeaderId != hashKey)
|
||||
{
|
||||
SendError(peer, ErrorCode.PARTY_DELETE_FAILED);
|
||||
return;
|
||||
}
|
||||
|
||||
int kickTarget = req.TargetPlayerId;
|
||||
if (kickTarget == hashKey)
|
||||
{
|
||||
return; // 자기 자신은 추방 불가
|
||||
}
|
||||
|
||||
if (!pm.LeaveParty(kickTarget, out PartyInfo? kickedParty) || kickedParty == null)
|
||||
{
|
||||
SendError(peer, ErrorCode.PARTY_NOT_IN_PARTY);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdatePartyPacket kickNotify = new()
|
||||
{
|
||||
PartyId = kickedParty.PartyId,
|
||||
Type = PartyUpdateType.KICK,
|
||||
LeaderId = kickedParty.LeaderId,
|
||||
PlayerId = kickTarget
|
||||
};
|
||||
byte[] kickData = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, kickNotify);
|
||||
BroadcastToChannel(channelId, kickData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,4 +118,5 @@ public class Player
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -661,7 +661,9 @@ public enum PartyUpdateType
|
||||
DELETE,
|
||||
JOIN,
|
||||
LEAVE,
|
||||
UPDATE
|
||||
UPDATE,
|
||||
INVITE, // 리더가 대상 플레이어에게 초대 전송
|
||||
KICK // 리더가 파티원을 추방
|
||||
}
|
||||
|
||||
// REQUEST_PARTY (클라 -> 서버) - CREATE: PartyName 사용 / JOIN·LEAVE·DELETE: PartyId 사용
|
||||
@@ -688,6 +690,13 @@ public class RequestPartyPacket
|
||||
get;
|
||||
set;
|
||||
} // CREATE 시 사용
|
||||
|
||||
[ProtoMember(4)]
|
||||
public int TargetPlayerId
|
||||
{
|
||||
get;
|
||||
set;
|
||||
} // INVITE, KICK 시 사용
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -67,6 +67,9 @@ public class Session
|
||||
RateLimitViolations = 0;
|
||||
}
|
||||
|
||||
// 채널 입장 시각 (플레이타임 계산용)
|
||||
public DateTime ChannelJoinedAt { get; set; }
|
||||
|
||||
public Session(int hashKey, NetPeer peer, int maxPacketsPerSecond = 60)
|
||||
{
|
||||
HashKey = hashKey;
|
||||
|
||||
Reference in New Issue
Block a user