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; }
+ }
+}