From bd9085b510a11a245b089eaf1e049a09279201de Mon Sep 17 00:00:00 2001 From: Cameron Date: Sun, 18 Feb 2024 10:41:50 -0600 Subject: [PATCH] Add project files. --- BuckshotMultiServerMono.sln | 25 ++++ .../BuckshotMultiServerMono.csproj | 21 +++ .../Controllers/GameController.cs | 124 ++++++++++++++++++ BuckshotMultiServerMono/Objects/Game.cs | 55 ++++++++ .../Objects/GameContainer.cs | 63 +++++++++ BuckshotMultiServerMono/Objects/GameState.cs | 45 +++++++ .../Objects/HandcuffState.cs | 20 +++ BuckshotMultiServerMono/Objects/ItemType.cs | 34 +++++ BuckshotMultiServerMono/Objects/Player.cs | 83 ++++++++++++ .../Objects/PlayerConnection.cs | 110 ++++++++++++++++ .../Objects/PlayerInventory.cs | 13 ++ BuckshotMultiServerMono/Program.cs | 34 +++++ .../Serializable/ErrorMessage.cs | 13 ++ .../Serializable/NewPlayer.cs | 13 ++ 14 files changed, 653 insertions(+) create mode 100644 BuckshotMultiServerMono.sln create mode 100644 BuckshotMultiServerMono/BuckshotMultiServerMono.csproj create mode 100644 BuckshotMultiServerMono/Controllers/GameController.cs create mode 100644 BuckshotMultiServerMono/Objects/Game.cs create mode 100644 BuckshotMultiServerMono/Objects/GameContainer.cs create mode 100644 BuckshotMultiServerMono/Objects/GameState.cs create mode 100644 BuckshotMultiServerMono/Objects/HandcuffState.cs create mode 100644 BuckshotMultiServerMono/Objects/ItemType.cs create mode 100644 BuckshotMultiServerMono/Objects/Player.cs create mode 100644 BuckshotMultiServerMono/Objects/PlayerConnection.cs create mode 100644 BuckshotMultiServerMono/Objects/PlayerInventory.cs create mode 100644 BuckshotMultiServerMono/Program.cs create mode 100644 BuckshotMultiServerMono/Serializable/ErrorMessage.cs create mode 100644 BuckshotMultiServerMono/Serializable/NewPlayer.cs diff --git a/BuckshotMultiServerMono.sln b/BuckshotMultiServerMono.sln new file mode 100644 index 0000000..5e719c1 --- /dev/null +++ b/BuckshotMultiServerMono.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34607.119 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BuckshotMultiServerMono", "BuckshotMultiServerMono\BuckshotMultiServerMono.csproj", "{7C4C2010-3AE6-4BF6-985B-C78B69E8A93D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7C4C2010-3AE6-4BF6-985B-C78B69E8A93D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C4C2010-3AE6-4BF6-985B-C78B69E8A93D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C4C2010-3AE6-4BF6-985B-C78B69E8A93D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C4C2010-3AE6-4BF6-985B-C78B69E8A93D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F4AAFB04-1F40-4DA4-980A-AF7AC4C5D54E} + EndGlobalSection +EndGlobal diff --git a/BuckshotMultiServerMono/BuckshotMultiServerMono.csproj b/BuckshotMultiServerMono/BuckshotMultiServerMono.csproj new file mode 100644 index 0000000..e5dcbbf --- /dev/null +++ b/BuckshotMultiServerMono/BuckshotMultiServerMono.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + False + true + + + + + + + + + + + + diff --git a/BuckshotMultiServerMono/Controllers/GameController.cs b/BuckshotMultiServerMono/Controllers/GameController.cs new file mode 100644 index 0000000..9661b25 --- /dev/null +++ b/BuckshotMultiServerMono/Controllers/GameController.cs @@ -0,0 +1,124 @@ +using BuckshotMultiServerMono.Objects; +using BuckshotMultiServerMono.Serializable; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BuckshotMultiServerMono.Controllers +{ + [ApiController] + [Route("api/v1")] + public class GameController : ControllerBase + { + //int counter = 0; + //[HttpGet] + //[ProducesResponseType(StatusCodes.Status200OK)] + //[Route("/count")] + //public ActionResult Count() + //{ + // return new ActionResult(counter++); + //} + [HttpPost] + [Route("/create")] + public IActionResult CreateGame() + { + var game = GameContainer.Get().CreateGame(); + var result = CreatedAtAction(nameof(CreateGame), null); + ((IList)Response.Headers.Location).Add(game.Id); + return result; + } + //TODO move name to payload? + [HttpPut] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [Route("/game/{id}/player/join/")] + public IActionResult JoinGame(string id, [FromBody] NewPlayer newPlayer) + { + var name = newPlayer.Name; + if (string.IsNullOrEmpty(name)) + return new StatusCodeResult(StatusCodes.Status400BadRequest); + if (!GameContainer.ValidateName(name)) + { + var error = new ErrorMessage(); + this.Response.StatusCode = StatusCodes.Status400BadRequest; + return new JsonResult(error); + } + var game = GameContainer.Get().FindGame(id); + if (game is null) + return new StatusCodeResult(StatusCodes.Status404NotFound); + lock (game) + { + //Game.AddPlayer already avoids creating duplicate player, + //but won't tell you whether the player was a duplicate or + //the game was full + if (game.Player1 is Player p1) + { + if (p1.Name == name) + return new StatusCodeResult(StatusCodes.Status204NoContent); + } + if (game.Player2 is Player p2) + { + if (p2.Name == name) + return new StatusCodeResult(StatusCodes.Status204NoContent); + } + return game.AddPlayer(name) ? + new StatusCodeResult(StatusCodes.Status201Created) : + new StatusCodeResult(StatusCodes.Status409Conflict); + } + + + } + [Route("/game/{id}/player/ws")] + public async Task Connect(string id, [FromQuery(Name = "PlayerName")] string? name) + { + if (HttpContext.WebSockets.IsWebSocketRequest) + { + if (name is null) + { + HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + return; + } + var game = GameContainer.Get().FindGame(id); + if (game is null) + { + HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; + return; + } + var player = game.FindPlayer(name); + if (player is null) + { + HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; + return; + } + using var socket = await HttpContext.WebSockets.AcceptWebSocketAsync(); + player.AddConnection(socket); + using ManualResetEventSlim mre = new(); + player.ConnectionClosedEvent += (_, _) => mre.Set(); + //prevent race condition where connection closes before event is registered + if (!player.Connected()) + { + //make sure connection is cleaned up properly + player.Disconnect(); + return; + } + //holds pipeline open for the duration of the connection + mre.Wait(); + player.Disconnect(); + return; + } + else + { + HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + } + } + } +} diff --git a/BuckshotMultiServerMono/Objects/Game.cs b/BuckshotMultiServerMono/Objects/Game.cs new file mode 100644 index 0000000..15bc649 --- /dev/null +++ b/BuckshotMultiServerMono/Objects/Game.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BuckshotMultiServerMono.Objects +{ + public class Game + { + public string Id; + public Player? Player1; + public Player? Player2; + public GameState State; + + public Game(string id) + { + Id = id; + State = new GameState(); + } + //return true if player added or already exists, false if name doesn't + //exist and players are full + //not thread-safe, must be locked externally + public bool AddPlayer(string name) + { + if (Player1 is Player p1) + { + if (p1.Name == name) + return true; + } + if (Player2 is Player p2) + { + if (p2.Name == name) + return true; + } + if (Player1 is null) + { + Player1 = new Player(name); + return true; + } + else if (Player2 is null) + { + Player2 = new Player(name); + return true; + } + else + return false; + } + + internal Player? FindPlayer(string name) + { + throw new NotImplementedException(); + } + } +} diff --git a/BuckshotMultiServerMono/Objects/GameContainer.cs b/BuckshotMultiServerMono/Objects/GameContainer.cs new file mode 100644 index 0000000..dd17455 --- /dev/null +++ b/BuckshotMultiServerMono/Objects/GameContainer.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BuckshotMultiServerMono.Objects +{ + public class GameContainer + { + private readonly List Games = []; + public Game CreateGame() + { + while (true) + { + var id = GenerateId(); + lock(Games) + { + if (!Games.Any(g => g.Id == id)) + { + Game game = new(id); + Games.Add(game); + return game; + } + } + } + } + public bool EndGame(string id) + { + lock (Games) + { + Game? game = Games.FirstOrDefault(g => g.Id == id); + if (game is Game game_notnull) + { + Games.Remove(game_notnull); + return true; + } + else + return false; + } + } + //generates an id which must be valid, but not necessarily unique + protected static string GenerateId() + { + throw new NotImplementedException(); + } + public static bool ValidateName(string name) + { + throw new NotImplementedException(); + } + public static GameContainer Get() + { + throw new NotImplementedException(); + } + public Game? FindGame(string id) + { + lock (Games) + { + return Games.FirstOrDefault(g => g.Id == id); + } + } + } +} diff --git a/BuckshotMultiServerMono/Objects/GameState.cs b/BuckshotMultiServerMono/Objects/GameState.cs new file mode 100644 index 0000000..bc2e659 --- /dev/null +++ b/BuckshotMultiServerMono/Objects/GameState.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BuckshotMultiServerMono.Objects +{ + /// + /// Holds the state of the game, including player state + /// + public class GameState + { + public int Player1Score; + public int Player2Score; + public int RoundNumber; + public int Player1Health; + public int Player2Health; + public HandcuffState CurrentHandcuffState = HandcuffState.None; + public bool SawUsed = false; + public PlayerInventory Player1Inventory = new(); + public PlayerInventory Player2Inventory = new(); + public bool ActivePlayerIs1 = true; + + /// + /// called after shooting (other than shooting self with blank) + /// automatically handles handcuffs + /// active player might not change, do not make any assumptions + /// about the state after callaing this + /// + public void NextTurn() + { + if (CurrentHandcuffState == HandcuffState.Active) + { + CurrentHandcuffState = HandcuffState.Inactive; + } + else + { + if (CurrentHandcuffState == HandcuffState.Inactive) + CurrentHandcuffState = HandcuffState.None; + ActivePlayerIs1 = !ActivePlayerIs1; + } + } + } +} diff --git a/BuckshotMultiServerMono/Objects/HandcuffState.cs b/BuckshotMultiServerMono/Objects/HandcuffState.cs new file mode 100644 index 0000000..e56ad2f --- /dev/null +++ b/BuckshotMultiServerMono/Objects/HandcuffState.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BuckshotMultiServerMono.Objects +{ + public enum HandcuffState + { + //handcuffs have not been used this turn + None, + //handcuffs have been used, and the active player + //will go again + Active, + //handcuffs have been used and the active player went again, + //handcuffs cannot be used again this turn + Inactive, + } +} diff --git a/BuckshotMultiServerMono/Objects/ItemType.cs b/BuckshotMultiServerMono/Objects/ItemType.cs new file mode 100644 index 0000000..9f0a220 --- /dev/null +++ b/BuckshotMultiServerMono/Objects/ItemType.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; + +namespace BuckshotMultiServerMono.Objects +{ + public enum ItemType + { + None, + Handcuff, + Glass, + Beer, + Saw, + Cigarette, + } + public static class ItemTypeGetter + { + public static ItemType Random() + { + ItemType[] choices = {ItemType.Handcuff, ItemType.Glass, ItemType.Beer, + ItemType.Saw, ItemType.Cigarette}; + return choices[System.Random.Shared.Next(choices.Length)]; + } + public static ItemType NotCig() + { + ItemType[] choices = {ItemType.Handcuff, ItemType.Glass, ItemType.Beer, + ItemType.Saw}; + return choices[System.Random.Shared.Next(choices.Length)]; + } + } +} diff --git a/BuckshotMultiServerMono/Objects/Player.cs b/BuckshotMultiServerMono/Objects/Player.cs new file mode 100644 index 0000000..bedc383 --- /dev/null +++ b/BuckshotMultiServerMono/Objects/Player.cs @@ -0,0 +1,83 @@ +using Microsoft.AspNetCore.Razor.TagHelpers; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace BuckshotMultiServerMono.Objects +{ + /// + /// Provides an interface to communicate with players that persists past + /// individual websocket connections. This should + /// + public partial class Player + { + /// + /// A message to be sent to the client + /// + public class PlayerTransmitMessage + { + public ReadOnlyMemory GetMessageData() + { + throw new NotImplementedException(); + } + } + /// + /// A message received from the client + /// + public class PlayerReceiveMessage + { + public static PlayerReceiveMessage? Parse(ReadOnlySpan data) + { + return JsonSerializer.Deserialize(data); + } + } + /// + /// The player's name and identifier + /// + public readonly string Name; + /// + /// Wraps a websocket connection to the client + /// + private PlayerConnection? Connection; + public event EventHandler? MessageReceivedEvent; + public event EventHandler? MessageTransmittedEvent; + public event EventHandler? ConnectionOpenedEvent; + public event EventHandler? ConnectionClosedEvent; + public Player(string name) + { + Name = name; + } + /// + /// attach a new websocket + /// + /// + public void AddConnection(WebSocket socket) + { + if (Connection is not null) + { + Connection.Close(); + Connection = null; + } + Connection = new PlayerConnection(socket); + Connection.ConnectionCloseEvent += (_, args) => + this.ConnectionClosedEvent?.Invoke(this, args); + ConnectionOpenedEvent?.Invoke(this, EventArgs.Empty); + } + public bool Connected() + { + var state = Connection?.Socket.State; + return (state == WebSocketState.Open || state == WebSocketState.Connecting); + } + + internal void Disconnect() + { + throw new NotImplementedException(); + } + } +} diff --git a/BuckshotMultiServerMono/Objects/PlayerConnection.cs b/BuckshotMultiServerMono/Objects/PlayerConnection.cs new file mode 100644 index 0000000..ab624e2 --- /dev/null +++ b/BuckshotMultiServerMono/Objects/PlayerConnection.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading.Tasks; + +namespace BuckshotMultiServerMono.Objects +{ + public partial class Player + { + private class PlayerConnection : IDisposable + { + public static readonly TimeSpan EndThreadTimeout = TimeSpan.FromSeconds(5); + + internal readonly WebSocket Socket; + internal readonly BlockingCollection Queue = []; + internal readonly Thread TransmitThread; + internal readonly Thread ReceiveThread; + internal readonly CancellationTokenSource CancelTokenSource = new(); + private bool disposedValue; + + public event EventHandler? TransmitEvent; + public event EventHandler? ReceiveEvent; + public event EventHandler? ConnectionCloseEvent; + public PlayerConnection(WebSocket socket) + { + Socket = socket; + TransmitThread = new Thread(TransmitFunc); + TransmitThread.Start(); + ReceiveThread = new Thread(ReceiveFunc); + ReceiveThread.Start(); + } + private void TransmitFunc() + { + try + { + while (!CancelTokenSource.IsCancellationRequested) + { + var message = Queue.Take(CancelTokenSource.Token); + if (message is not null) + { + Socket.SendAsync(message.GetMessageData(), WebSocketMessageType.Text, true, CancelTokenSource.Token) + .AsTask().Wait(); + TransmitEvent?.Invoke(this, message); + } + } + } + catch (OperationCanceledException) + { + //this is expected, no need to act on it + } + } + private void ReceiveFunc() + { + var buffer = new Memory(); + while (!CancelTokenSource.IsCancellationRequested) + { + List message = []; + ValueWebSocketReceiveResult res; + do + { + var task = Socket.ReceiveAsync(buffer, CancelTokenSource.Token).AsTask(); + task.Wait(); + if (task.Exception?.InnerException is OperationCanceledException) + return; + res = task.Result; + message.AddRange(buffer.Span); + if (res.MessageType == WebSocketMessageType.Close) + { + Close(); + return; + } + } while (!res.EndOfMessage); + var sendMessage = PlayerReceiveMessage.Parse(message.ToArray()); + if (sendMessage is not null) + ReceiveEvent?.Invoke(this, sendMessage); + } + } + public void EnqueueMessage(PlayerTransmitMessage message) + { + Queue.Add(message); + } + public void Close() + { + CancelTokenSource.Cancel(); + ConnectionCloseEvent?.Invoke(this, EventArgs.Empty); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + Close(); + } + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } + } +} diff --git a/BuckshotMultiServerMono/Objects/PlayerInventory.cs b/BuckshotMultiServerMono/Objects/PlayerInventory.cs new file mode 100644 index 0000000..8a3b50b --- /dev/null +++ b/BuckshotMultiServerMono/Objects/PlayerInventory.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BuckshotMultiServerMono.Objects +{ + public class PlayerInventory + { + + } +} diff --git a/BuckshotMultiServerMono/Program.cs b/BuckshotMultiServerMono/Program.cs new file mode 100644 index 0000000..99db399 --- /dev/null +++ b/BuckshotMultiServerMono/Program.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; + +// See https://aka.ms/new-console-template for more information +//Console.WriteLine("Hello, World!"); + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo { Title = "GameController API" }); +}); + +builder.Services.AddControllers(); + +var app = builder.Build(); +app.UseWebSockets(); +app.UseSwagger(); +app.UseSwaggerUI(c => +{ + c.SwaggerEndpoint("/swagger/v1/swagger.json", "GameController API V1"); +}); + +//app.MapPost("/api/v1/newgame", () => +//{ + +//}); + + + +app.Run(); diff --git a/BuckshotMultiServerMono/Serializable/ErrorMessage.cs b/BuckshotMultiServerMono/Serializable/ErrorMessage.cs new file mode 100644 index 0000000..8c73f9e --- /dev/null +++ b/BuckshotMultiServerMono/Serializable/ErrorMessage.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BuckshotMultiServerMono.Serializable +{ + public class ErrorMessage + { + + } +} diff --git a/BuckshotMultiServerMono/Serializable/NewPlayer.cs b/BuckshotMultiServerMono/Serializable/NewPlayer.cs new file mode 100644 index 0000000..ec58efb --- /dev/null +++ b/BuckshotMultiServerMono/Serializable/NewPlayer.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BuckshotMultiServerMono.Serializable +{ + public class NewPlayer + { + public string? Name { get; set; } + } +}