diff --git a/src/CommandHandlers/PublicMessageHandlers.cs b/src/CommandHandlers/PublicMessageHandlers.cs new file mode 100644 index 0000000..c09133d --- /dev/null +++ b/src/CommandHandlers/PublicMessageHandlers.cs @@ -0,0 +1,31 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +namespace sodoffmmo.CommandHandlers; + +[ExtensionCommandHandler("PM")] +class RacingPMHandler : ICommandHandler +{ + // rec: {"a":13,"c":1,"p":{"c":"PM","p":{"M":"DT:c4647597-a72a-4f34-973c-5a10218d9a64:1000","en":"we"},"r":-1}} + public void Handle(Client client, NetworkObject receivedObject) { + // send: {"a":13,"c":1,"p":{"c":"PM","p":{"arr":[{"M":["DT:f05fc387-7358-4bff-be04-7c316f0a8de8:1000"],"MID":3529441}]}}} + NetworkObject cmd = new(); + NetworkObject p = new(); + NetworkArray arr = new(); + NetworkObject data = new(); + data.Add("M", new string[] { + receivedObject.Get("p").Get("M") + }); + arr.Add(data); + p.Add("arr", arr); + p.Add("MID", client.ClientID); + cmd.Add("c", "PM"); + cmd.Add("p", p); + NetworkPacket packet = NetworkObject.WrapObject(1, 13, cmd).Serialize(); + + foreach (var roomClient in client.Room.Clients) { + roomClient.Send(packet); + } + } +} diff --git a/src/CommandHandlers/RacingHandlers.cs b/src/CommandHandlers/RacingHandlers.cs new file mode 100644 index 0000000..0c31a57 --- /dev/null +++ b/src/CommandHandlers/RacingHandlers.cs @@ -0,0 +1,80 @@ +using sodoffmmo.Attributes; +using sodoffmmo.Core; +using sodoffmmo.Data; + +using System.Timers; + +namespace sodoffmmo.CommandHandlers; + +// Set Player Ready +[ExtensionCommandHandler("dr.PR")] +class RacingPlayerReadyHandler : ICommandHandler +{ + public void Handle(Client client, NetworkObject receivedObject) { // {"a":13,"c":1,"p":{"c":"dr.PR","p":{"IMR":"True","en":""},"r":-1}} + NetworkObject p = receivedObject.Get("p"); + RacingPlayerState ready = p.Get("IMR") == "True" ? RacingPlayerState.Ready : RacingPlayerState.NotReady; + + // client send also {"a":7,"c":0,"p":{"m":"IMR:ae70ef16-c52c-43e4-9305-d6ea3e378a0d:True","r":412456,"t":0,"u":3529456}} + // so server do not need generate this packet + + if (client.Room.Group == "RacingDragon") { + RacingRoom room = client.Room as RacingRoom; + room.SetPlayerState(client, ready); + Console.WriteLine($"IMR Lobby: {client.ClientID} {ready}"); + room.TryLoad(); + } else { + RacingLobby.SetPlayerState(client, ready); + Console.WriteLine($"IMR: {client.ClientID} {ready}"); + } + } +} + +// Player Status Request +[ExtensionCommandHandler("dr.PS")] +class RacingPlayerStatusHandler : ICommandHandler +{ + public void Handle(Client client, NetworkObject receivedObject) { + RacingLobby.SendToRacingRoom(); + client.Send(RacingLobby.GetPS()); + } +} + +// User Ready ACK +[ExtensionCommandHandler("dr.UACK")] +class RacingUACKHandler : ICommandHandler +{ + public void Handle(Client client, NetworkObject receivedObject) { + RacingRoom room = client.Room as RacingRoom; + room.SetPlayerState(client, RacingPlayerState.RaceReady1); + } +} + +// All Ready ACK +[ExtensionCommandHandler("dr.ARACK")] +class RacingARACKHandler : ICommandHandler +{ + public void Handle(Client client, NetworkObject receivedObject) { + RacingRoom room = client.Room as RacingRoom; + room.SetPlayerState(client, RacingPlayerState.RaceReady2); + + if (room.GetPlayersCount(RacingPlayerState.RaceReady2) == room.ClientsCount) { + NetworkPacket packet = room.GetSTAPacket(); + foreach (var roomClient in room.Clients) { + roomClient.Send(packet); + } + Console.WriteLine($"STA"); + } + } +} + +[ExtensionCommandHandler("dr.AR")] +class RacingARHandler : ICommandHandler +{ + public void Handle(Client client, NetworkObject receivedObject) { // {"a":13,"c":1,"p":{"c":"dr.AR","p":{"CT":"112.1268","FD":"3008.283","LC":"3","UN":"scourgexxwulf","en":""},"r":412467}} + RacingRoom room = client.Room as RacingRoom; + NetworkObject p = receivedObject.Get("p"); + + room.SetResults(client, p.Get("UN"), p.Get("CT"), p.Get("LC")); + room.SendResults(); + } +} diff --git a/src/Core/Configuration.cs b/src/Core/Configuration.cs index dad0c40..993758d 100644 --- a/src/Core/Configuration.cs +++ b/src/Core/Configuration.cs @@ -28,6 +28,8 @@ internal sealed class ServerConfiguration { public int Port { get; set; } = 9933; public int EventTimer { get; set; } = 30; public int FirstEventTimer { get; set; } = 10; + public int RacingMaxPlayers { get; set; } = 6; + public int RacingMinPlayers { get; set; } = 2; public bool EnableChat { get; set; } = true; public bool AllowChaos { get; set; } = false; } diff --git a/src/Core/Racing.cs b/src/Core/Racing.cs new file mode 100644 index 0000000..7ba4966 --- /dev/null +++ b/src/Core/Racing.cs @@ -0,0 +1,312 @@ +using System.Globalization; +using sodoffmmo.Data; +using System.Timers; + +namespace sodoffmmo.Core; + +public enum RacingPlayerState { + NotReady, + Ready, + InRacingRoom, + RaceReady1, + RaceReady2 +} + +public class RacingRoom : Room { + static List RacingRooms = new(); + static Random random = new Random(); + + public static RacingRoom Get() { + foreach (var room in RacingRooms) { + if (room.ClientsCount == 0) { + room.Reset(); + return room; + } + } + var newroom = new RacingRoom("RacingDragon" + "_" + RacingRooms.Count.ToString()); + RacingRooms.Add(newroom); + return newroom; + } + + public RacingRoom(string name, int? trackId = null) : base (name, "RacingDragon") { + Reset(trackId); + } + + private void Reset(int? trackId = null) { + if (trackId is null) { + TID = random.Next(105); + } else { + TID = (int)trackId; + } + + players = new(); + results = new(); + timer = null; + + base.RoomVariables = new(); + base.RoomVariables.Add(NetworkArray.VlElement("IS_RACE_ROOM", "SINGLERACE#1#1#SINGLERACE#0#2")); + base.RoomVariables.Add(NetworkArray.VlElement("TID", TID)); + } + + public int TID; + + // players ready status + + Dictionary players; + + public void SetPlayerState(Client client, RacingPlayerState state) { + players[client] = state; + } + + public bool IsPlayerState(Client client, RacingPlayerState state) { + if (players.TryGetValue(client, out var info)) { + return info == state; + } + return false; + } + + public int GetPlayersCount(RacingPlayerState state) { + int count = 0; + foreach(var player in players) { + if (player.Value == state) ++count; + } + return count; + } + + // results + + class Result { + public string userName; + public string time; + public string laps; + } + + SortedDictionary results; + + public void SetResults(Client client, string userName, string time, string laps) { + float timef = float.Parse(time, System.Globalization.CultureInfo.InvariantCulture); + while (results.ContainsKey(timef)) + timef += 0.000001f; + + results.Add(timef, new Result { + userName = userName, + time = time, + laps = laps + }); + } + + public void SendResults() { + if (ClientsCount == results.Count) { + // {"a":13,"c":1,"p":{"c":"","p":{"arr":["RA","","GR","Zavertin","91.81613","3","scourgexxwulf","111.81613","3"]},"r":412467}} + List info = new(); + info.Add("RA"); + info.Add(""); + info.Add("GR"); + foreach(var result in results) { + info.Add(result.Value.userName); + info.Add(result.Value.time); + info.Add(result.Value.laps); + } + + NetworkPacket packet = Utils.ArrNetworkPacket(info.ToArray(), "", Id); + foreach (var roomClient in Clients) { + roomClient.Send(packet); + } + } + } + + // start and countdown + + System.Timers.Timer timer; + int counter; + + private void SetTimer(double timeout, System.Timers.ElapsedEventHandler callback, bool AutoReset = false) { + if (timer != null) { + timer.Stop(); + timer.Close(); + } + + timer = new System.Timers.Timer(timeout * 1000); + timer.AutoReset = AutoReset; + timer.Enabled = true; + timer.Elapsed += callback; + } + + public void Init() { + counter = 20; + SetTimer(0.2, SendJoin); + } + + private void SendJoin(Object source, ElapsedEventArgs e) { + foreach(var player in players) { + Console.WriteLine($"Join Racing Room: {Name} RoomID: {Id} IID: {player.Key.ClientID}"); + player.Key.JoinRoom(this); + } + SetTimer(1, CountDown, true); + } + + private void CountDown(Object source, ElapsedEventArgs e) { + if (!TryLoad()) { + if (counter == 0) { + Load(); + } else { + // {"a":13,"c":1,"p":{"c":"","p":{"arr":["RA","","LT","18"]},"r":412467}} + NetworkPacket packet = Utils.ArrNetworkPacket(new string[] { + "RA", + "", + "LT", + (--counter).ToString() + }, "", Id); + foreach (var roomClient in Clients) { + roomClient.Send(packet); + } + } + } + } + + public bool TryLoad() { + if (GetPlayersCount(RacingPlayerState.Ready) == ClientsCount) { + Load(); + return true; + } + return false; + } + + public void Load() { + timer.Stop(); + timer.Close(); + + // {"a":13,"c":1,"p":{"c":"","p":{"arr":["RA","","ST"]},"r":412467}} + NetworkPacket packet = Utils.ArrNetworkPacket(new string[] { + "RA", + "", + "ST" + }, "", Id); + foreach (var roomClient in Clients) { + roomClient.Send(packet); + } + } + + // TODO StratTimer → kick out to main lobby players without RacingPlayerState.RaceReady1 after timeout, next kick out players without RacingPlayerState.RaceReady2 after timeout2 + + // TODO EndTimer → {"a":13,"c":1,"p":{"c":"","p":{"arr":["RA","","ET","1"]},"r":412467}} + + // utils + + public NetworkPacket GetTIDPacket() { + // {"a":13,"c":1,"p":{"c":"","p":{"arr":["RA","","TID","","10","0","0","0","0"]}}} + return Utils.ArrNetworkPacket(new string[] { + "RA", + "", + "TID", + "", // game mode (unused) + TID.ToString(), // track id + "0", // theme id + "0","0","0" // unused (?) + }); + } + + public NetworkPacket GetSTAPacket() { + string staList = ""; + int i = 0; + foreach(var player in players) { + if (i > 0) + staList += ":"; + staList += player.Key.PlayerData.Uid + ":" + (++i).ToString(); + } + + // {"a":13,"c":1,"p":{"c":"","p":{"arr":["RA","","STA","c4647597-a72a-4f34-973c-5a10218d9a64:1:f05fc387-7358-4bff-be04-7c316f0a8de8:2:ae70ef16-c52c-43e4-9305-d6ea3e378a0d:3","1688128174649"]},"r":412467}} + return Utils.ArrNetworkPacket(new string[] { + "RA", + "", + "STA", + staList + }, "", Id); + } +} + +public class RacingLobby { + class Status { + public string uid; + public RacingPlayerState state = RacingPlayerState.NotReady; + } + + static object lobbyLock = new object(); + + static Dictionary lobbyPlayers = new(); + + public static void SetPlayerState(Client client, RacingPlayerState state) { + lock (lobbyLock) { + if (!lobbyPlayers.ContainsKey(client)) { + lobbyPlayers[client] = new Status { uid = client.PlayerData.Uid }; + } + lobbyPlayers[client].state = state; + } + } + + public static bool IsPlayerState(Client client, RacingPlayerState state) { + lock (lobbyLock) { + if (lobbyPlayers.TryGetValue(client, out var info)) { + return info.state == state; + } + return false; + } + } + + public static int GetPlayersCount(RacingPlayerState state) { + lock (lobbyLock) { + int count = 0; + foreach(var player in lobbyPlayers) { + if (player.Value.state == state) ++count; + } + return count; + } + } + + public static bool SendToRacingRoom() { + lock (lobbyLock) { + if (GetPlayersCount(RacingPlayerState.Ready) >= Configuration.ServerConfiguration.RacingMinPlayers) { + int i = 0; + List toRemove = new(); + RacingRoom room = RacingRoom.Get(); + foreach (var player in lobbyPlayers) { + if (player.Value.state == RacingPlayerState.Ready) { + if (++i > Configuration.ServerConfiguration.RacingMaxPlayers) + break; + // set client state in Lobby + player.Value.state = RacingPlayerState.InRacingRoom; + // mark client to remove from Lobby + toRemove.Add(player.Key); + // send TID info to client + player.Key.Send(room.GetTIDPacket()); + // set client state in racing room + room.SetPlayerState(player.Key, RacingPlayerState.NotReady); + } + } + // join clients to racing room and start countdown + room.Init(); + + foreach (var player in toRemove) { + lobbyPlayers.Remove(player); + } + + return true; + } + return false; + } + } + + public static NetworkPacket GetPS() { + // {"a":13,"c":1,"p":{"c":"PS","p":{"arr":["RA","","PS","e6147216-8100-4552-864d-be8f1347e201","8cb5842d-735a-4259-80af-e2e204b9c2bd"]}}} + List info = new(); + info.Add("RA"); + info.Add(""); + info.Add("PS"); + foreach(var player in lobbyPlayers) { + if (player.Value.state != RacingPlayerState.InRacingRoom) { + info.Add(player.Value.uid); + } + } + return Utils.ArrNetworkPacket(info.ToArray(), "PS"); + } +} diff --git a/src/appsettings.json b/src/appsettings.json index 24f6877..cd6f75a 100644 --- a/src/appsettings.json +++ b/src/appsettings.json @@ -15,6 +15,12 @@ "// EventTimer": "time between start of world events (battle ship events)", "EventTimer": 30, + "// RacingMaxPlayers": "maximum players allowed in Thunder Run Racing (no more than 6)", + "RacingMaxPlayers": 6, + + "// RacingMinPlayers": "minimum players to start Thunder Run Racing", + "RacingMinPlayers": 2, + "// AllowChaos": "disable server side exploit protection", "AllowChaos": false }