feat: marble full game

This commit is contained in:
Somebody
2025-05-10 23:08:38 +08:00
parent c95b957da3
commit 182b679cd7
24 changed files with 858 additions and 183 deletions

View File

@@ -8,5 +8,6 @@ public enum MarblePlayerPhaseEnum
PerformanceFinish = 3,
Gaming = 4,
Launching = 5,
UseTech = 6
SimulateFinish = 6,
UseTech = 7
}

View File

@@ -18,7 +18,7 @@ public class DanhengConnection
public static readonly ConcurrentBag<int> IgnoreLog =
[
CmdIds.PlayerHeartBeatCsReq, CmdIds.PlayerHeartBeatScRsp, CmdIds.SceneEntityMoveCsReq,
CmdIds.SceneEntityMoveScRsp, CmdIds.GetShopListCsReq, CmdIds.GetShopListScRsp
CmdIds.SceneEntityMoveScRsp, CmdIds.GetShopListCsReq, CmdIds.GetShopListScRsp, CmdIds.FightHeartBeatCsReq, CmdIds.FightHeartBeatScRsp
];
protected readonly CancellationTokenSource CancelToken;

View File

@@ -26,7 +26,7 @@ public class LobbyRoomInstance(PlayerInstance owner, long roomId, FightGameMode
public async ValueTask AddPlayer(PlayerInstance player, List<int> sealList, LobbyCharacterType characterType)
{
await AddPlayer(new LobbyPlayerInstance(player, characterType)
await AddPlayer(new LobbyPlayerInstance(player, characterType, this)
{
EquippedSealList = sealList
});
@@ -105,6 +105,15 @@ public class LobbyRoomInstance(PlayerInstance owner, long roomId, FightGameMode
return Retcode.RetSucc;
}
public async ValueTask<Retcode> EndFight(LobbyPlayerInstance player)
{
// alrdy check status in lobby start fight
IsInGame = false;
player.CharacterStatus = LobbyCharacterStatus.Idle;
await BroadCastToRoom(new PacketLobbySyncInfoScNotify(player.Player.Uid, this, LobbyModifyType.FightEnd));
return Retcode.RetSucc;
}
public LobbyPlayerInstance? GetPlayerByUid(int uid)
{
var player = Players.FirstOrDefault(x => x.Player.Uid == uid);

View File

@@ -3,12 +3,13 @@ using EggLink.DanhengServer.Proto;
namespace EggLink.DanhengServer.GameServer.Game.Lobby.Player;
public class LobbyPlayerInstance(PlayerInstance player, LobbyCharacterType characterType)
public class LobbyPlayerInstance(PlayerInstance player, LobbyCharacterType characterType, LobbyRoomInstance lobby)
{
public PlayerInstance Player { get; } = player;
public List<int> EquippedSealList { get; set; } = [];
public LobbyCharacterType CharacterType { get; set; } = characterType;
public LobbyCharacterStatus CharacterStatus { get; set; } = LobbyCharacterStatus.Idle;
public LobbyRoomInstance LobbyRoom { get; set; } = lobby;
public LobbyBasicInfo ToProto()
{

View File

@@ -8,6 +8,7 @@ public abstract class BaseGamePlayerInstance(LobbyPlayerInstance lobby)
{
public LobbyPlayerInstance LobbyPlayer { get; } = lobby;
public bool EnterGame { get; set; }
public bool LeaveGame { get; set; }
public Connection? Connection { get; set; }
public async ValueTask SendPacket(BasePacket packet)

View File

@@ -27,7 +27,7 @@ public class MarbleGamePlayerInstance : BaseGamePlayerInstance
if (!GameData.MarbleSealData.TryGetValue(seal, out var marbleSeal)) continue;
var posY = (index - 1) * 1.5f;
var posX = posXBaseValue * (Math.Abs(index - 1) * 0.5f + 2);
var posX = posXBaseValue * (Math.Abs(index - 1) * 1 + 3);
var rotX = posXBaseValue * -1f;
AllowMoveSealList.Add(CurItemId);

View File

@@ -1,25 +1,32 @@
using EggLink.DanhengServer.Data;
using System.Numerics;
using EggLink.DanhengServer.Data;
using EggLink.DanhengServer.Enums.Fight;
using EggLink.DanhengServer.GameServer.Game.Lobby;
using EggLink.DanhengServer.GameServer.Game.MultiPlayer.MarbleGame.Physics;
using EggLink.DanhengServer.GameServer.Game.MultiPlayer.MarbleGame.Seal;
using EggLink.DanhengServer.GameServer.Game.MultiPlayer.MarbleGame.Sync;
using EggLink.DanhengServer.GameServer.Server;
using EggLink.DanhengServer.GameServer.Server.Packet.Send.Fight;
using EggLink.DanhengServer.GameServer.Server.Packet.Send.Multiplayer;
using EggLink.DanhengServer.Kcp;
using EggLink.DanhengServer.Proto;
using Microsoft.Xna.Framework;
using EggLink.DanhengServer.Util;
namespace EggLink.DanhengServer.GameServer.Game.MultiPlayer.MarbleGame;
public class MarbleGameRoomInstance : BaseMultiPlayerGameRoomInstance
{
public MarbleTeamType CurMoveTeamType { get; set; }
public MarbleTeamType FirstMoveTeamType { get; set; }
public int CurRound { get; set; }
public int TurnCount { get; set; }
public long WaitingOperationEndTime { get; set; }
public MarbleGameRoomInstance(long roomId, LobbyRoomInstance parentLobby) : base(roomId, parentLobby)
{
// random move team type
CurMoveTeamType = (MarbleTeamType)Random.Shared.Next(1, 3);
FirstMoveTeamType = CurMoveTeamType;
// set player
foreach (var player in parentLobby.Players)
{
@@ -28,6 +35,9 @@ public class MarbleGameRoomInstance : BaseMultiPlayerGameRoomInstance
}
}
#region Player
public async ValueTask BroadCastToRoom(BasePacket packet)
{
foreach (var player in Players)
@@ -36,6 +46,14 @@ public class MarbleGameRoomInstance : BaseMultiPlayerGameRoomInstance
}
}
public async ValueTask BroadCastToRoomPlayer(BasePacket packet)
{
foreach (var player in Players.Where(x => !x.LeaveGame))
{
await player.LobbyPlayer.Player.SendPacket(packet);
}
}
public async ValueTask EnterGame(int uid)
{
var player = GetPlayerById(uid);
@@ -55,6 +73,48 @@ public class MarbleGameRoomInstance : BaseMultiPlayerGameRoomInstance
}
}
public async ValueTask OnPlayerHeartBeat()
{
var curTime = Extensions.GetUnixMs();
if (WaitingOperationEndTime > 0 && curTime >= WaitingOperationEndTime)
{
// timeout
await SwitchTurn();
}
}
public List<MarbleGameBaseSyncData> CheckPlayerWin()
{
var winPlayer = Players.OfType<MarbleGamePlayerInstance>().FirstOrDefault(x => x.Score >= 6);
if (winPlayer == null) return [];
List<MarbleGameBaseSyncData> syncData = [];
// win
foreach (var player in Players.OfType<MarbleGamePlayerInstance>())
{
syncData.Add(new MarbleGameFinishSyncData(player, player == winPlayer));
}
return syncData;
}
public async ValueTask EndGame()
{
// end game
await BroadCastToRoom(new PacketFightSessionStopScNotify(this));
await BroadCastToRoom(new PacketMultiplayerFightGameFinishScNotify(this));
foreach (var marblePlayer in Players.OfType<MarbleGamePlayerInstance>())
{
await marblePlayer.LobbyPlayer.LobbyRoom.EndFight(marblePlayer.LobbyPlayer);
marblePlayer.Connection?.Stop();
}
// remove room
ServerUtils.MultiPlayerGameServerManager.RemoveRoom(RoomId);
}
#endregion
#region Handler
public async ValueTask HandleGeneralRequest(MarbleGamePlayerInstance player, uint msgType, byte[] reqData)
@@ -79,7 +139,8 @@ public class MarbleGameRoomInstance : BaseMultiPlayerGameRoomInstance
await BroadCastToRoom(new PacketFightGeneralScNotify(MarbleNetWorkMsgEnum.SyncBatch,
MarbleNetWorkMsgEnum.Operation, operationReq));
break;
default:
case MarbleNetWorkMsgEnum.SimulateFinish:
await HandleSimulateFinish(player);
break;
}
}
@@ -87,7 +148,7 @@ public class MarbleGameRoomInstance : BaseMultiPlayerGameRoomInstance
public async ValueTask LoadFinish(MarbleGamePlayerInstance player)
{
player.Phase = MarblePlayerPhaseEnum.LoadFinish;
if (Players.OfType<MarbleGamePlayerInstance>().ToList().All(x => x.Phase == MarblePlayerPhaseEnum.LoadFinish))
if (Players.OfType<MarbleGamePlayerInstance>().ToList().Where(x => !x.LeaveGame).All(x => x.Phase == MarblePlayerPhaseEnum.LoadFinish))
{
// next phase (performance)
await BroadCastToRoom(new PacketFightGeneralScNotify(MarbleNetWorkMsgEnum.SyncBatch,
@@ -98,13 +159,23 @@ public class MarbleGameRoomInstance : BaseMultiPlayerGameRoomInstance
public async ValueTask PerformanceFinish(MarbleGamePlayerInstance player)
{
player.Phase = MarblePlayerPhaseEnum.PerformanceFinish;
if (Players.OfType<MarbleGamePlayerInstance>().ToList().All(x => x.Phase == MarblePlayerPhaseEnum.PerformanceFinish))
if (Players.OfType<MarbleGamePlayerInstance>().ToList().Where(x => !x.LeaveGame).All(x => x.Phase == MarblePlayerPhaseEnum.PerformanceFinish))
{
// next phase (round start)
await RoundStart();
}
}
public async ValueTask HandleSimulateFinish(MarbleGamePlayerInstance player)
{
player.Phase = MarblePlayerPhaseEnum.SimulateFinish;
if (Players.OfType<MarbleGamePlayerInstance>().ToList().Where(x => !x.LeaveGame).All(x => x.Phase == MarblePlayerPhaseEnum.SimulateFinish))
{
// switch turn
await SwitchTurn();
}
}
#endregion
#region Round
@@ -112,6 +183,8 @@ public class MarbleGameRoomInstance : BaseMultiPlayerGameRoomInstance
public async ValueTask RoundStart()
{
CurRound++;
TurnCount = 0;
CurMoveTeamType = FirstMoveTeamType;
foreach (var player in Players.OfType<MarbleGamePlayerInstance>())
{
player.ChangeRound();
@@ -123,6 +196,59 @@ public class MarbleGameRoomInstance : BaseMultiPlayerGameRoomInstance
Players.OfType<MarbleGamePlayerInstance>().SelectMany(x => x.SealList.Values)
.Select(x => new MarbleGameSealSyncData(x, MarbleFrameType.RoundStart)).ToList())
]));
WaitingOperationEndTime = Extensions.GetUnixMs() + 1000 * 30;
TurnCount++;
}
public async ValueTask SwitchTurn()
{
var moveCount = Players.OfType<MarbleGamePlayerInstance>()
.SelectMany(x => x.SealList.Values.Where(j => j.OnStage)).Count();
if (moveCount == TurnCount)
{
await RoundEnd();
return;
}
CurMoveTeamType = CurMoveTeamType == MarbleTeamType.TeamA ? MarbleTeamType.TeamB : MarbleTeamType.TeamA;
foreach (var player in Players.OfType<MarbleGamePlayerInstance>())
{
player.Phase = MarblePlayerPhaseEnum.Gaming;
}
await BroadCastToRoom(new PacketFightGeneralScNotify(MarbleNetWorkMsgEnum.SyncBatch,
[
new MarbleGameInfoSyncData(MarbleNetWorkMsgEnum.SyncNotify, MarbleSyncType.SwitchRound, this,
Players.OfType<MarbleGamePlayerInstance>().SelectMany(x => x.SealList.Values)
.Select(x => new MarbleGameSealSyncData(x, MarbleFrameType.RoundStart)).ToList())
]));
WaitingOperationEndTime = Extensions.GetUnixMs() + 1000 * 30;
TurnCount++;
}
public async ValueTask RoundEnd()
{
FirstMoveTeamType = FirstMoveTeamType == MarbleTeamType.TeamA ? MarbleTeamType.TeamB : MarbleTeamType.TeamA;
foreach (var player in Players.OfType<MarbleGamePlayerInstance>())
{
player.Phase = MarblePlayerPhaseEnum.Gaming;
}
await BroadCastToRoom(new PacketFightGeneralScNotify(MarbleNetWorkMsgEnum.SyncBatch,
[
new MarbleGameInfoSyncData(MarbleNetWorkMsgEnum.SyncNotify, MarbleSyncType.RoundEnd, this,
Players.OfType<MarbleGamePlayerInstance>().SelectMany(x => x.SealList.Values)
.Select(x => new MarbleGameSealSyncData(x, MarbleFrameType.RoundEnd)).ToList())
]));
WaitingOperationEndTime = 0;
await RoundStart();
}
#endregion
@@ -131,19 +257,25 @@ public class MarbleGameRoomInstance : BaseMultiPlayerGameRoomInstance
public async ValueTask HandleLaunch(int itemId, Vector2 rotation)
{
WaitingOperationEndTime = 0;
var player = Players.OfType<MarbleGamePlayerInstance>().FirstOrDefault(x => x.SealList.ContainsKey(itemId));
if (player == null) return;
var seal = player.SealList[itemId];
if (!GameData.MarbleSealData.TryGetValue(seal.SealId, out var sealExcel)) return;
player.AllowMoveSealList.Remove(seal.Id);
var speed = sealExcel.MaxSpeed * rotation;
var simulator = new PhysicsSimulator(
gravity: new Vector2(0, 0),
var simulator = new CollisionSimulator(
leftBound: -5.25f,
rightBound: 5.25f,
topBound: 3f,
bottomBound: -3f
);
)
{
LaunchTeam = itemId / 100
};
seal.Velocity = new MarbleSealVector
{
@@ -151,55 +283,297 @@ public class MarbleGameRoomInstance : BaseMultiPlayerGameRoomInstance
Y = speed.Y
};
List<MarbleGameSealSyncData> syncData = [];
List<BaseMarbleGameSyncData> syncData = [];
foreach (var sealInst in Players.OfType<MarbleGamePlayerInstance>().SelectMany(x => x.SealList.Values)
.Where(x => x.OnStage))
{
simulator.AddBall(sealInst.Id, new Vector2(sealInst.Position.X, sealInst.Position.Y), sealInst.Mass,
sealInst.Size, new Vector2(sealInst.Velocity.X, sealInst.Velocity.Y));
sealInst.Size, new Vector2(sealInst.Velocity.X, sealInst.Velocity.Y), hp:sealInst.CurHp, atk:sealInst.Attack);
}
syncData.AddRange(Players.OfType<MarbleGamePlayerInstance>().SelectMany(x => x.SealList.Values)
.Select(sealInst => new MarbleGameSealActionSyncData(sealInst, MarbleFrameType.ActionStart)));
simulator.Simulate(maxDuration: 20f);
syncData.Add(new MarbleGameSealLaunchStopSyncData(seal, MarbleFrameType.Launch));
simulator.Simulate();
foreach (var record in simulator.CollisionRecords)
foreach (var recordRaw in simulator.Records) // process record
{
syncData.Add(new MarbleGameSealCollisionSyncData(seal, record.ObjectAId, record.ObjectBId, record.Time, record.Position));
}
foreach (var body in simulator.World.BodyList)
{
if (body?.UserData is int id)
switch (recordRaw)
{
var sealInst = Players.OfType<MarbleGamePlayerInstance>().SelectMany(x => x.SealList.Values)
.FirstOrDefault(x => x.Id == id);
if (sealInst == null) continue;
case CollisionRecord record:
{
var sealInst = Players.OfType<MarbleGamePlayerInstance>().SelectMany(x => x.SealList.Values)
.FirstOrDefault(x => x.Id == record.BallA.Id);
if (sealInst == null) continue;
var sealInstB = Players.OfType<MarbleGamePlayerInstance>().SelectMany(x => x.SealList.Values)
.FirstOrDefault(x => x.Id == (record.BallB?.Id ?? 1));
sealInst.Position = new MarbleSealVector
sealInst.Velocity = new MarbleSealVector
{
X = record.BallA.Velocity.X,
Y = record.BallA.Velocity.Y
};
sealInst.Position = new MarbleSealVector
{
X = record.BallA.Position.X,
Y = record.BallA.Position.Y
};
if (record.BallA.Velocity != Vector2.Zero) // avoid zero vector
{
var velocityANormal = Vector2.Normalize(record.BallA.Velocity);
sealInst.Rotation = new MarbleSealVector
{
X = velocityANormal.X,
Y = velocityANormal.Y
};
}
if (sealInstB != null && record.BallB != null)
{
var velocityBNormal = Vector2.Normalize(record.BallB.Velocity);
if (record.BallB.Velocity != Vector2.Zero)
{
sealInstB.Rotation = new MarbleSealVector
{
X = velocityBNormal.X,
Y = velocityBNormal.Y
};
}
sealInstB.Velocity = new MarbleSealVector
{
X = record.BallB.Velocity.X,
Y = record.BallB.Velocity.Y
};
sealInstB.Position = new MarbleSealVector
{
X = record.BallB.Position.X,
Y = record.BallB.Position.Y
};
if (sealInstB.Id / 100 != sealInst.Id / 100)
{
// different teams
// do damage to b
syncData.AddRange(sealInst.Id / 100 == itemId / 100
? DoDamage(sealInst, sealInstB, record.Time)
// do damage to a
: DoDamage(sealInstB, sealInst, record.Time));
}
}
syncData.Add(new MarbleGameSealCollisionSyncData(sealInst, record.BallA.Id, record.BallB?.Id ?? 1,
record.Time, record.CollisionPos, sealInstB?.Velocity));
if (sealInstB != null)
{
syncData.Add(new MarbleGameSealCollisionSyncData(sealInstB, record.BallA.Id,
record.BallB?.Id ?? 1,
record.Time, record.CollisionPos, sealInst.Velocity));
}
break;
}
case StopRecord stopRecord:
{
X = body.Position.X,
Y = body.Position.Y
};
sealInst.Rotation = new MarbleSealVector
var sealInst = Players.OfType<MarbleGamePlayerInstance>().SelectMany(x => x.SealList.Values)
.FirstOrDefault(x => x.Id == stopRecord.Ball.Id);
if (sealInst == null) continue;
sealInst.Velocity = new MarbleSealVector
{
X = 0,
Y = 0
};
sealInst.Position = new MarbleSealVector
{
X = stopRecord.Ball.Position.X,
Y = stopRecord.Ball.Position.Y
};
syncData.Add(new MarbleGameSealLaunchStopSyncData(sealInst, MarbleFrameType.Stop, stopRecord.Time));
break;
}
case ChangeSpeedRecord changeSpeedRecord:
{
X = body.Rotation
};
sealInst.Velocity = new MarbleSealVector
{
X = body.LinearVelocity.X,
Y = body.LinearVelocity.Y
};
var sealInst = Players.OfType<MarbleGamePlayerInstance>().SelectMany(x => x.SealList.Values)
.FirstOrDefault(x => x.Id == changeSpeedRecord.Ball.Id);
if (sealInst == null) continue;
sealInst.Velocity = new MarbleSealVector
{
X = changeSpeedRecord.Ball.Velocity.X,
Y = changeSpeedRecord.Ball.Velocity.Y
};
sealInst.Position = new MarbleSealVector
{
X = changeSpeedRecord.Ball.Position.X,
Y = changeSpeedRecord.Ball.Position.Y
};
if (changeSpeedRecord.Ball.Velocity != Vector2.Zero)
{
var normal = Vector2.Normalize(changeSpeedRecord.Ball.Velocity);
sealInst.Rotation = new MarbleSealVector
{
X = normal.X,
Y = normal.Y
};
}
syncData.Add(new MarbleGameSealLaunchStopSyncData(sealInst, MarbleFrameType.ChangeSpeed,
changeSpeedRecord.Time));
break;
}
}
}
foreach (var ball in simulator.Balls)
{
var sealInst = Players.OfType<MarbleGamePlayerInstance>().SelectMany(x => x.SealList.Values)
.FirstOrDefault(x => x.Id == ball.Id);
if (sealInst == null) continue;
sealInst.Position = new MarbleSealVector
{
X = ball.Position.X,
Y = ball.Position.Y
};
sealInst.Velocity = new MarbleSealVector
{
X = ball.Velocity.X,
Y = ball.Velocity.Y
};
}
syncData.AddRange(Players.OfType<MarbleGamePlayerInstance>().SelectMany(x => x.SealList.Values)
.Select(sealInst => new MarbleGameSealActionSyncData(sealInst, MarbleFrameType.ActionEnd, simulator.CurrentTime)));
.Select(sealInst =>
new MarbleGameSealActionSyncData(sealInst, MarbleFrameType.ActionEnd, simulator.CurTime)));
syncData.AddRange(ReviveAllDeadSeals(simulator.CurTime));
var winData = CheckPlayerWin();
await BroadCastToRoom(new PacketFightGeneralScNotify(MarbleNetWorkMsgEnum.SyncBatch,
[new MarbleGameInfoLaunchingSyncData(MarbleNetWorkMsgEnum.SyncNotify, MarbleSyncType.SimulateStart, simulator.CurrentTime, itemId, syncData)]));
[
new MarbleGameInfoLaunchingSyncData(MarbleNetWorkMsgEnum.SyncNotify, MarbleSyncType.SimulateStart,
simulator.CurTime, itemId, syncData), ..winData
]));
foreach (var p in Players.OfType<MarbleGamePlayerInstance>())
{
p.Phase = MarblePlayerPhaseEnum.Launching;
}
if (winData.Count > 0)
{
await EndGame();
}
}
public List<BaseMarbleGameSyncData> DoDamage(MarbleGameSealInstance attacker, MarbleGameSealInstance target, float time)
{
List<BaseMarbleGameSyncData> syncData = [];
var attackerPlayer = Players.OfType<MarbleGamePlayerInstance>().FirstOrDefault(x =>
x.SealList.ContainsKey(attacker.Id));
var targetPlayer = Players.OfType<MarbleGamePlayerInstance>().FirstOrDefault(x =>
x.SealList.ContainsKey(target.Id));
if (attackerPlayer == null || targetPlayer == null) return syncData;
var damage = attacker.Attack;
target.CurHp -= damage;
syncData.Add(new MarbleGameHpChangeSyncData(target, MarbleFrameType.HpChange, -damage, time));
if (target.CurHp <= 0)
{
// die
// score
if (Players.OfType<MarbleGamePlayerInstance>().All(x => x.Score < 6))
{
attackerPlayer.Score++;
syncData.Add(new MarbleGameScoreSyncData((Players[0] as MarbleGamePlayerInstance)!.Score,
(Players[1] as MarbleGamePlayerInstance)!.Score, MarbleFrameType.TeamScore));
}
target.CurHp = 0;
}
return syncData;
}
public List<BaseMarbleGameSyncData> ReviveAllDeadSeals(float time)
{
List<BaseMarbleGameSyncData> syncData = [];
var seals = Players.OfType<MarbleGamePlayerInstance>()
.SelectMany(x => x.SealList.Values).ToList();
var deadSeals = seals.Where(j => j.CurHp <= 0).ToList();
if (deadSeals.Count == 0) return syncData;
foreach (var seal in deadSeals)
{
var player = Players.OfType<MarbleGamePlayerInstance>()
.FirstOrDefault(x => x.SealList.ContainsKey(seal.Id));
if (player == null) continue;
var posXBaseValue = player.TeamType == MarbleTeamType.TeamA ? -1 : 1;
var index = seal.Id % 100;
var posY = (index - 1) * 1.5f;
var posX = posXBaseValue * (Math.Abs(index - 1) * 1 + 3);
seal.CurHp = seal.MaxHp;
seal.OnStage = true;
seal.Velocity = new MarbleSealVector();
seal.Position = new MarbleSealVector
{
X = posX,
Y = posY
};
seal.Rotation = new MarbleSealVector
{
X = posXBaseValue * -1f,
};
syncData.Add(new MarbleGameSealActionSyncData(seal, MarbleFrameType.Revive, time));
}
bool anyMove;
do
{
// detect collision with seals
anyMove = false;
for (var i = 0; i < seals.Count; i++)
{
for (var j = i + 1; j < seals.Count; j++)
{
var sealA = seals[i];
var sealB = seals[j];
var sealAPos = new Vector2(sealA.Position.X, sealA.Position.Y);
var sealBPos = new Vector2(sealB.Position.X, sealB.Position.Y);
if (!(Vector2.Distance(sealBPos, sealAPos) <= sealA.Size + sealB.Size)) continue;
anyMove = true;
// move sealB away
var normalVec = Vector2.Normalize(sealBPos - sealAPos);
var moveDistance = sealA.Size + sealB.Size - Vector2.Distance(sealBPos, sealAPos) + 0.1f;
var moveVec = normalVec * moveDistance;
sealB.Position = new MarbleSealVector
{
X = sealB.Position.X + moveVec.X,
Y = sealB.Position.Y + moveVec.Y
};
syncData.Add(new MarbleGameSealActionSyncData(sealB, MarbleFrameType.Revive, time));
}
}
} while (anyMove);
return syncData;
}
#endregion
@@ -210,7 +584,7 @@ public class MarbleGameRoomInstance : BaseMultiPlayerGameRoomInstance
{
LobbyBasicInfo = { ParentLobby.Players.Select(x => x.ToProto()) },
CurActionTeamType = CurMoveTeamType,
LevelId = (uint)Random.Shared.Next(100, 103),
LevelId = 101,
TeamAPlayer = (uint)Players[0].LobbyPlayer.Player.Uid,
TeamBPlayer = (uint)Players[1].LobbyPlayer.Player.Uid,
TeamARank = 1,

View File

@@ -0,0 +1,26 @@
using System.Numerics;
namespace EggLink.DanhengServer.GameServer.Game.MultiPlayer.MarbleGame.Physics;
public class Ball(int id, Vector2 position, float mass, float radius, Vector2? velocity = null, bool isStatic = false, int hp = 100, int atk = 0)
{
public int Id { get; } = id;
public Vector2 Position { get; set; } = position;
public Vector2 Velocity { get; set; } = velocity ?? Vector2.Zero;
public Vector2 StageInitialVelocity { get; set; } = velocity ?? Vector2.Zero;
public float Radius { get; } = radius;
public float Mass { get; } = mass;
public bool IsStatic { get; set; } = isStatic;
public int Hp { get; set; } = hp;
public int Atk { get; set; } = atk;
public BallSnapshot GetSnapshot()
{
return new BallSnapshot
{
Id = Id,
Position = Position,
Velocity = Velocity
};
}
}

View File

@@ -0,0 +1,10 @@
using System.Numerics;
namespace EggLink.DanhengServer.GameServer.Game.MultiPlayer.MarbleGame.Physics;
public class BallSnapshot
{
public int Id { get; set; }
public Vector2 Position { get; set; }
public Vector2 Velocity { get; set; }
}

View File

@@ -0,0 +1,189 @@
using System.Numerics;
namespace EggLink.DanhengServer.GameServer.Game.MultiPlayer.MarbleGame.Physics;
public class CollisionSimulator(float leftBound, float rightBound, float topBound, float bottomBound, float deceleration = 15f)
{
public float Deceleration = deceleration;
public const float StepTime = 0.001f;
public List<Ball> Balls { get; } = [];
public int LaunchTeam { get; set; } = 0;
public List<object> Records { get; } = [];
public float CurTime { get; set; }
private float LeftBound { get; } = leftBound;
private float RightBound { get; } = rightBound;
private float TopBound { get; } = topBound;
private float BottomBound { get; } = bottomBound;
public void AddBall(int id, Vector2 position, float mass, float radius, Vector2? velocity = null, bool isStatic = false, int hp = 100, int atk = 0)
{
Balls.Add(new Ball(id, position, mass, radius, velocity, isStatic, hp, atk));
}
public void AdvanceTime(float time)
{
CurTime += time;
foreach (var ball in Balls.Where(ball => !ball.IsStatic))
{
if (ball.Velocity.Length() < 0.01f)
{
if (ball.Velocity != Vector2.Zero)
{
ball.Velocity = Vector2.Zero;
Records.Add(new StopRecord(CurTime, ball.GetSnapshot()));
}
continue;
}
ball.Position += ball.Velocity * time;
ball.Velocity -= Vector2.Normalize(ball.Velocity) * Deceleration * time;
}
CheckWallCollision();
CheckBallCollision();
}
public void CheckWallCollision()
{
foreach (var ball in Balls.Where(ball => !ball.IsStatic))
{
if (ball.Velocity.Length() < 0.001f) continue;
var collide = false;
var collisionPosition = Vector2.Zero;
if (ball.Position.X + ball.Radius >= RightBound || ball.Position.X - ball.Radius <= LeftBound)
{
collisionPosition = ball.Position with { X = MathF.Sign(ball.Velocity.X) * RightBound };
ball.Velocity = ball.Velocity with { X = -ball.Velocity.X };
collide = true;
}
if (ball.Position.Y + ball.Radius >= TopBound || ball.Position.Y - ball.Radius <= BottomBound)
{
collisionPosition = ball.Position with { Y = MathF.Sign(ball.Velocity.Y) * TopBound };
ball.Velocity = ball.Velocity with { Y = -ball.Velocity.Y };
collide = true;
}
if (collide)
{
ball.StageInitialVelocity = ball.Velocity;
Records.Add(new CollisionRecord(CurTime, ball.GetSnapshot(), null, collisionPosition));
}
}
}
public void CheckBallCollision()
{
for (var i = 0; i < Balls.Count; i++)
{
for (var j = i + 1; j < Balls.Count; j++)
{
var ballA = Balls[i];
var ballB = Balls[j];
if (ballA.IsStatic || ballB.IsStatic) continue; // skip static balls
var distance = Vector2.Distance(ballA.Position, ballB.Position);
if (distance <= ballA.Radius + ballB.Radius)
{
HandleBallCollision(ballA, ballB);
}
}
}
}
public void HandleBallCollision(Ball ballA, Ball ballB)
{
var deltaPos = ballB.Position - ballA.Position;
if (deltaPos == Vector2.Zero) return;
// get normal and tangent vectors
var normal = Vector2.Normalize(deltaPos);
var tangent = new Vector2(-normal.Y, normal.X);
// split velocity into normal and tangent components
var v1n = Vector2.Dot(ballA.Velocity, normal);
var v1t = Vector2.Dot(ballA.Velocity, tangent);
var v2n = Vector2.Dot(ballB.Velocity, normal);
var v2t = Vector2.Dot(ballB.Velocity, tangent);
var massA = ballA.Mass;
var massB = ballB.Mass;
var totalMass = massA + massB;
// switch to normal velocity
var newV1n = (v1n * (massA - massB) + 2 * massB * v2n) / totalMass;
var newV2n = (v2n * (massB - massA) + 2 * massA * v1n) / totalMass;
// combine velocity
ballA.Velocity = newV1n * normal + v1t * tangent;
ballB.Velocity = newV2n * normal + v2t * tangent;
// fix pos
var overlap = ballA.Radius + ballB.Radius - deltaPos.Length();
if (overlap > 0)
{
var correction = normal * overlap;
if (!ballA.IsStatic) ballA.Position -= correction * massB / totalMass;
if (!ballB.IsStatic) ballB.Position += correction * massA / totalMass;
}
ballA.StageInitialVelocity = ballA.Velocity;
ballB.StageInitialVelocity = ballB.Velocity;
// record
var collisionPos = ballA.Position + normal * ballA.Radius;
Records.Add(new CollisionRecord(CurTime, ballA.GetSnapshot(), ballB.GetSnapshot(), collisionPos));
// check if ball is dead
if (ballA.Id / 100 != ballB.Id / 100)
{
// different teams
if (ballA.Id / 100 == LaunchTeam)
{
ballB.Hp -= ballA.Atk;
if (ballB.Hp <= 0)
{
ballB.Velocity = Vector2.Zero;
ballB.StageInitialVelocity = Vector2.Zero;
ballB.IsStatic = true;
}
}
else
{
ballA.Hp -= ballB.Atk;
if (ballA.Hp <= 0)
{
ballA.Velocity = Vector2.Zero;
ballA.StageInitialVelocity = Vector2.Zero;
ballA.IsStatic = true;
}
}
}
}
public void Simulate()
{
while (!AllObjectStopped())
{
AdvanceTime(StepTime);
foreach (var ball in Balls.Where(x => x.StageInitialVelocity != Vector2.Zero && x.Velocity.Length() > 0.01f))
{
var speed = ball.Velocity.Length();
if (ball.StageInitialVelocity.Length() / 2 > speed)
{
ball.StageInitialVelocity = Vector2.Zero; // avoid infinite loop
Records.Add(new ChangeSpeedRecord(CurTime, ball.GetSnapshot()));
}
}
}
}
public bool AllObjectStopped()
{
return Balls.All(ball => ball.Velocity.Length() == 0);
}
}

View File

@@ -0,0 +1,7 @@
using System.Numerics;
namespace EggLink.DanhengServer.GameServer.Game.MultiPlayer.MarbleGame.Physics;
public record CollisionRecord(float Time, BallSnapshot BallA, BallSnapshot? BallB, Vector2 CollisionPos);
public record StopRecord(float Time, BallSnapshot Ball);
public record ChangeSpeedRecord(float Time, BallSnapshot Ball);

View File

@@ -1,102 +0,0 @@
using Genbox.VelcroPhysics.Dynamics;
using Genbox.VelcroPhysics.Factories;
using Microsoft.Xna.Framework;
namespace EggLink.DanhengServer.GameServer.Game.MultiPlayer.MarbleGame.Physics;
public class PhysicsSimulator
{
public World World { get; }
public List<CollisionRecord> CollisionRecords { get; } = [];
public float CurrentTime;
public PhysicsSimulator(Vector2 gravity, float leftBound, float rightBound, float topBound, float bottomBound)
{
World = new World(gravity);
CreateWalls(leftBound, rightBound, topBound, bottomBound);
World.ContactManager.OnBroadphaseCollision += OnCollisionDetected;
}
private void CreateWalls(float left, float right, float top, float bottom)
{
// 创建四个静态墙体
BodyFactory.CreateEdge(World, new Vector2(left, bottom), new Vector2(left, top)); // 左墙
BodyFactory.CreateEdge(World, new Vector2(right, bottom), new Vector2(right, top)); // 右墙
BodyFactory.CreateEdge(World, new Vector2(left, top), new Vector2(right, top)); // 上墙
BodyFactory.CreateEdge(World, new Vector2(left, bottom), new Vector2(right, bottom));// 下墙
}
private void OnCollisionDetected(ref FixtureProxy proxyA, ref FixtureProxy proxyB)
{
Fixture fixtureA = proxyA.Fixture;
Fixture fixtureB = proxyB.Fixture;
// 过滤墙体碰撞记录(可选)
if (fixtureA.Body.BodyType == BodyType.Static ||
fixtureB.Body.BodyType == BodyType.Static)
return;
if (fixtureA.Body.UserData is int idA && fixtureB.Body.UserData is int idB)
{
CollisionRecords.Add(new CollisionRecord
{
Time = CurrentTime,
ObjectAId = idA,
ObjectBId = idB,
Position = (fixtureA.Body.Position + fixtureB.Body.Position) / 2, // 使用实际接触点
VelocityA = fixtureA.Body.LinearVelocity,
VelocityB = fixtureB.Body.LinearVelocity
});
}
}
public void AddBall(int id, Vector2 position, float mass, float radius, Vector2? velocity = null)
{
float density = mass / (MathF.PI * radius * radius);
Body body = BodyFactory.CreateCircle(World, radius, density, position);
body.BodyType = BodyType.Dynamic;
body.Restitution = 0.8f; // 统一恢复系数
body.Friction = 0.3f;
body.LinearDamping = 1.5f; // 使用阻尼代替手动衰减
body.UserData = id;
body.IsBullet = true; // 启用CCD
if (velocity.HasValue)
body.LinearVelocity = velocity.Value;
}
public void Simulate(float maxDuration, float timeStep = 0.001f)
{
World.Step(0);
while (CurrentTime < maxDuration && !AllObjectsStopped())
{
World.Step(timeStep);
CurrentTime += timeStep;
}
}
public IEnumerable<string> GetFinalPositions()
{
return World.BodyList.Select(body =>
$"ID {(int)(body?.UserData ?? -1)}: ({body.Position.X:F2}, {body.Position.Y:F2})");
}
private bool AllObjectsStopped() =>
World.BodyList.All(b => b.LinearVelocity.Length() < 0.1f);
}
public class CollisionRecord
{
public float Time { get; set; }
public int ObjectAId { get; set; }
public int ObjectBId { get; set; }
public Vector2 Position { get; set; }
public Vector2 VelocityA { get; set; }
public Vector2 VelocityB { get; set; }
}

View File

@@ -1,13 +1,49 @@
using EggLink.DanhengServer.Proto;
using Microsoft.Xna.Framework;
using System.Numerics;
using EggLink.DanhengServer.Proto;
namespace EggLink.DanhengServer.GameServer.Game.MultiPlayer.MarbleGame.Seal;
public class MarbleGameSealSyncData(MarbleGameSealInstance inst, MarbleFrameType frameType)
public abstract class BaseMarbleGameSyncData
{
public abstract MarbleGameSyncData ToProto();
}
public class MarbleGameScoreSyncData(int playerAScore, int playerBScore, MarbleFrameType frameType) : BaseMarbleGameSyncData
{
public override MarbleGameSyncData ToProto()
{
return new MarbleGameSyncData
{
FrameType = frameType,
PlayerAScore = (uint)playerAScore,
PlayerBScore = (uint)playerBScore
};
}
}
public class MarbleGameHpChangeSyncData(MarbleGameSealInstance inst, MarbleFrameType frameType, int changeValue, float time = 0f) : MarbleGameSealSyncData(inst, frameType)
{
public override MarbleGameSyncData ToProto()
{
return new MarbleGameSyncData
{
FrameType = FrameType,
Id = (uint)Instance.Id,
Time = time,
Hp = Instance.CurHp,
MaxHp = Instance.MaxHp,
HpChangeValue = changeValue,
Attack = Instance.Attack
};
}
}
public class MarbleGameSealSyncData(MarbleGameSealInstance inst, MarbleFrameType frameType) : BaseMarbleGameSyncData
{
public MarbleGameSealInstance Instance { get; set; } = inst.Clone();
public MarbleFrameType FrameType { get; set; } = frameType;
public virtual MarbleGameSyncData ToProto()
public override MarbleGameSyncData ToProto()
{
return new MarbleGameSyncData
{
@@ -19,7 +55,7 @@ public class MarbleGameSealSyncData(MarbleGameSealInstance inst, MarbleFrameType
SealRotation = Instance.Rotation,
SealOnStage = Instance.OnStage,
SealSize = Instance.Size,
FrameType = frameType
FrameType = FrameType
};
}
}
@@ -38,14 +74,33 @@ public class MarbleGameSealActionSyncData(MarbleGameSealInstance inst, MarbleFra
SealRotation = Instance.Rotation,
SealOnStage = Instance.OnStage,
SealSize = Instance.Size,
SealVelocity = Instance.Velocity,
FrameType = frameType,
FrameType = FrameType,
Time = time
};
}
}
public class MarbleGameSealCollisionSyncData(MarbleGameSealInstance inst, int collideOwnerId, int collideTargetId, float time, Vector2 collidePos) : MarbleGameSealSyncData(inst, MarbleFrameType.Collide)
public class MarbleGameSealLaunchStopSyncData(MarbleGameSealInstance inst, MarbleFrameType frameType, float time = 0) : MarbleGameSealSyncData(inst, frameType)
{
public override MarbleGameSyncData ToProto()
{
return new MarbleGameSyncData
{
Attack = Instance.Attack,
Id = (uint)Instance.Id,
Hp = Instance.CurHp,
MaxHp = Instance.MaxHp,
SealPosition = Instance.Position,
SealRotation = Instance.Rotation,
FrameType = FrameType,
Time = time,
SealVelocity = Instance.Velocity
};
}
}
public class MarbleGameSealCollisionSyncData(MarbleGameSealInstance inst, int collideOwnerId, int collideTargetId, float time, Vector2 collidePos, MarbleSealVector? targetVelocity) : MarbleGameSealSyncData(inst, MarbleFrameType.Collide)
{
public override MarbleGameSyncData ToProto()
{
@@ -57,8 +112,6 @@ public class MarbleGameSealCollisionSyncData(MarbleGameSealInstance inst, int co
MaxHp = Instance.MaxHp,
SealPosition = Instance.Position,
SealRotation = Instance.Rotation,
SealOnStage = Instance.OnStage,
SealSize = Instance.Size,
FrameType = MarbleFrameType.Collide,
CollideType = collideTargetId == 1 ? MarbleFactionType.Field : collideTargetId / 100 == collideOwnerId / 100 ? MarbleFactionType.Ally : MarbleFactionType.Enemy,
CollideOwnerId = (uint)collideOwnerId,
@@ -68,7 +121,7 @@ public class MarbleGameSealCollisionSyncData(MarbleGameSealInstance inst, int co
X = collidePos.X,
Y = collidePos.Y
},
CollisionTargetVelocity = new MarbleSealVector(),
CollisionTargetVelocity = targetVelocity ?? new MarbleSealVector(),
SealVelocity = Instance.Velocity,
Time = time
};

View File

@@ -6,5 +6,5 @@ namespace EggLink.DanhengServer.GameServer.Game.MultiPlayer.MarbleGame.Sync;
public abstract class MarbleGameBaseSyncData(MarbleNetWorkMsgEnum type)
{
public MarbleNetWorkMsgEnum MessageType { get; set; } = type;
public abstract MarbleGameSyncInfo ToProto();
public abstract FightGameInfo ToProto();
}

View File

@@ -0,0 +1,28 @@
using EggLink.DanhengServer.Enums.Fight;
using EggLink.DanhengServer.Proto;
namespace EggLink.DanhengServer.GameServer.Game.MultiPlayer.MarbleGame.Sync;
public class MarbleGameFinishSyncData(MarbleGamePlayerInstance player, bool isWin) : MarbleGameBaseSyncData(MarbleNetWorkMsgEnum.GameFinish)
{
public override FightGameInfo ToProto()
{
return new FightGameInfo
{
GameMessageType = (uint)MessageType,
RogueFinishInfo = new MarbleGameFinishInfo
{
IsWin = isWin,
SealOwnerUid = (uint)player.LobbyPlayer.Player.Uid,
SealFinishInfoList =
{
player.SealList.Keys.Select(x => new MarbleSealFinishInfo
{
ItemId = (uint)x,
MatchTitleId = 2
})
}
}
};
}
}

View File

@@ -10,19 +10,24 @@ public class MarbleGameInfoSyncData(
MarbleGameRoomInstance room,
List<MarbleGameSealSyncData> syncDatas) : MarbleGameBaseSyncData(type)
{
public override MarbleGameSyncInfo ToProto()
public override FightGameInfo ToProto()
{
return new MarbleGameSyncInfo
return new FightGameInfo
{
MarbleSyncType = syncType,
CurRound = (uint)room.CurRound,
AllowedMoveSealList =
GameMessageType = (uint)MessageType,
MarbleGameSyncInfo = new MarbleGameSyncInfo
{
(room.Players[(int)room.CurMoveTeamType - 1] as MarbleGamePlayerInstance)!.AllowMoveSealList.Select(x =>
(uint)x)
},
MarbleGameSyncData = { syncDatas.Select(x => x.ToProto()) },
FirstPlayerActionEnd = room.CurMoveTeamType == MarbleTeamType.TeamA
MarbleSyncType = syncType,
CurRound = (uint)room.CurRound,
AllowedMoveSealList =
{
(room.Players[(int)room.CurMoveTeamType - 1] as MarbleGamePlayerInstance)!.AllowMoveSealList.Select(
x =>
(uint)x)
},
MarbleGameSyncData = { syncDatas.Select(x => x.ToProto()) },
FirstPlayerActionEnd = room.CurMoveTeamType == MarbleTeamType.TeamA
}
};
}
}
@@ -32,17 +37,21 @@ public class MarbleGameInfoLaunchingSyncData(
MarbleSyncType syncType,
float time,
int itemId,
List<MarbleGameSealSyncData> syncDatas) : MarbleGameBaseSyncData(type)
List<BaseMarbleGameSyncData> syncDatas) : MarbleGameBaseSyncData(type)
{
public override MarbleGameSyncInfo ToProto()
public override FightGameInfo ToProto()
{
return new MarbleGameSyncInfo
return new FightGameInfo
{
Launching = true,
MarbleSyncType = syncType,
MoveTotalTime = time,
QueuePosition = (uint)itemId,
MarbleGameSyncData = { syncDatas.Select(x => x.ToProto()) }
GameMessageType = (uint)MessageType,
MarbleGameSyncInfo = new MarbleGameSyncInfo
{
Launching = true,
MarbleSyncType = syncType,
MoveTotalTime = time,
QueuePosition = (uint)itemId,
MarbleGameSyncData = { syncDatas.Select(x => x.ToProto()) }
}
};
}
}

View File

@@ -5,11 +5,15 @@ namespace EggLink.DanhengServer.GameServer.Game.MultiPlayer.MarbleGame.Sync;
public class MarblePerformanceSyncData(MarbleNetWorkMsgEnum type) : MarbleGameBaseSyncData(type)
{
public override MarbleGameSyncInfo ToProto()
public override FightGameInfo ToProto()
{
return new MarbleGameSyncInfo
return new FightGameInfo
{
MarbleSyncType = MarbleSyncType.Performance
GameMessageType = (uint)MessageType,
MarbleGameSyncInfo = new MarbleGameSyncInfo
{
MarbleSyncType = MarbleSyncType.Performance
}
};
}
}

View File

@@ -28,7 +28,7 @@ public class MultiPlayerGameServerManager
{
foreach (var room in Rooms.Values)
{
if (room.Players.Any(x => x.LobbyPlayer.Player.Uid == uid))
if (room.Players.Any(x => !x.LeaveGame && x.LobbyPlayer.Player.Uid == uid))
{
return room;
}

View File

@@ -20,7 +20,6 @@
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\DanhengKcpSharp\DanhengKcpSharp.csproj" />
<ProjectReference Include="..\Proto\Proto.csproj" />
<PackageReference Include="Genbox.VelcroPhysics" Version="0.1.0-alpha.2" />
<PackageReference Include="Google.Protobuf" Version="3.29.1" />
<PackageReference Include="Google.Protobuf.Tools" Version="3.29.1" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />

View File

@@ -11,6 +11,11 @@ public class HandlerFightHeartBeatCsReq : Handler
{
var req = FightHeartBeatCsReq.Parser.ParseFrom(data);
if (connection.MarbleRoom != null)
{
await connection.MarbleRoom.OnPlayerHeartBeat();
}
await connection.SendPacket(new PacketFightHeartBeatScRsp(req.ClientTimeMs));
}
}

View File

@@ -0,0 +1,21 @@
using EggLink.DanhengServer.Kcp;
using EggLink.DanhengServer.Proto;
namespace EggLink.DanhengServer.GameServer.Server.Packet.Recv.Multiplayer;
[Opcode(CmdIds.MultiplayerFightGiveUpCsReq)]
public class HandlerMultiplayerFightGiveUpCsReq : Handler
{
public override async Task OnHandle(Connection connection, byte[] header, byte[] data)
{
var req = MultiplayerFightGiveUpCsReq.Parser.ParseFrom(data);
var roomId = req.GateRoomId;
ServerUtils.MultiPlayerGameServerManager.Rooms.TryGetValue((long)roomId, out var room);
var player = room?.GetPlayerById(connection.MarblePlayer?.LobbyPlayer.Player.Uid ?? connection.Player!.Uid);
if (player != null)
player.LeaveGame = true;
await connection.SendPacket(CmdIds.MultiplayerFightGiveUpScRsp);
}
}

View File

@@ -51,11 +51,7 @@ public class PacketFightGeneralScNotify : BasePacket
NetworkMsgType = (uint)msgType,
FightGeneralInfo = new FightGeneralServerInfo
{
FightGameInfo = { sync.Select(x => new FightGameInfo
{
GameMessageType = (uint)x.MessageType,
MarbleGameSyncInfo = x.ToProto()
}) }
FightGameInfo = { sync.Select(x => x.ToProto()) }
}
};

View File

@@ -0,0 +1,22 @@
using EggLink.DanhengServer.GameServer.Game.MultiPlayer;
using EggLink.DanhengServer.Kcp;
using EggLink.DanhengServer.Proto;
namespace EggLink.DanhengServer.GameServer.Server.Packet.Send.Fight;
public class PacketFightSessionStopScNotify : BasePacket
{
public PacketFightSessionStopScNotify(BaseMultiPlayerGameRoomInstance room) : base(CmdIds.FightSessionStopScNotify)
{
var proto = new FightSessionStopScNotify
{
SessionInfo = new FightSessionInfo
{
SessionGameMode = room.GameMode,
SessionRoomId = (ulong)room.RoomId
}
};
SetData(proto);
}
}

View File

@@ -0,0 +1,22 @@
using EggLink.DanhengServer.GameServer.Game.MultiPlayer;
using EggLink.DanhengServer.Kcp;
using EggLink.DanhengServer.Proto;
namespace EggLink.DanhengServer.GameServer.Server.Packet.Send.Multiplayer;
public class PacketMultiplayerFightGameFinishScNotify : BasePacket
{
public PacketMultiplayerFightGameFinishScNotify(BaseMultiPlayerGameRoomInstance room) : base(CmdIds.MultiplayerFightGameFinishScNotify)
{
var proto = new MultiplayerFightGameFinishScNotify
{
SessionInfo = new FightSessionInfo
{
SessionGameMode = room.GameMode,
SessionRoomId = (ulong)room.RoomId
}
};
SetData(proto);
}
}