Add project files.

This commit is contained in:
Cameron
2024-02-18 10:41:50 -06:00
parent 21ede8e0fb
commit bd9085b510
14 changed files with 653 additions and 0 deletions

View File

@@ -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

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>False</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="Models\" />
</ItemGroup>
</Project>

View File

@@ -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<int> Count()
//{
// return new ActionResult<int>(counter++);
//}
[HttpPost]
[Route("/create")]
public IActionResult CreateGame()
{
var game = GameContainer.Get().CreateGame();
var result = CreatedAtAction(nameof(CreateGame), null);
((IList<string?>)Response.Headers.Location).Add(game.Id);
return result;
}
//TODO move name to payload?
[HttpPut]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType<ErrorMessage>(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;
}
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<Game> 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);
}
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BuckshotMultiServerMono.Objects
{
/// <summary>
/// Holds the state of the game, including player state
/// </summary>
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;
/// <summary>
/// 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
/// </summary>
public void NextTurn()
{
if (CurrentHandcuffState == HandcuffState.Active)
{
CurrentHandcuffState = HandcuffState.Inactive;
}
else
{
if (CurrentHandcuffState == HandcuffState.Inactive)
CurrentHandcuffState = HandcuffState.None;
ActivePlayerIs1 = !ActivePlayerIs1;
}
}
}
}

View File

@@ -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,
}
}

View File

@@ -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)];
}
}
}

View File

@@ -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
{
/// <summary>
/// Provides an interface to communicate with players that persists past
/// individual websocket connections. This should
/// </summary>
public partial class Player
{
/// <summary>
/// A message to be sent to the client
/// </summary>
public class PlayerTransmitMessage
{
public ReadOnlyMemory<byte> GetMessageData()
{
throw new NotImplementedException();
}
}
/// <summary>
/// A message received from the client
/// </summary>
public class PlayerReceiveMessage
{
public static PlayerReceiveMessage? Parse(ReadOnlySpan<byte> data)
{
return JsonSerializer.Deserialize<PlayerReceiveMessage>(data);
}
}
/// <summary>
/// The player's name and identifier
/// </summary>
public readonly string Name;
/// <summary>
/// Wraps a websocket connection to the client
/// </summary>
private PlayerConnection? Connection;
public event EventHandler<PlayerReceiveMessage>? MessageReceivedEvent;
public event EventHandler<PlayerTransmitMessage>? MessageTransmittedEvent;
public event EventHandler? ConnectionOpenedEvent;
public event EventHandler? ConnectionClosedEvent;
public Player(string name)
{
Name = name;
}
/// <summary>
/// attach a new websocket
/// </summary>
/// <param name="socket"></param>
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();
}
}
}

View File

@@ -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<PlayerTransmitMessage> Queue = [];
internal readonly Thread TransmitThread;
internal readonly Thread ReceiveThread;
internal readonly CancellationTokenSource CancelTokenSource = new();
private bool disposedValue;
public event EventHandler<PlayerTransmitMessage>? TransmitEvent;
public event EventHandler<PlayerReceiveMessage>? 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<byte>();
while (!CancelTokenSource.IsCancellationRequested)
{
List<byte> 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);
}
}
}
}

View File

@@ -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
{
}
}

View File

@@ -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();

View File

@@ -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
{
}
}

View File

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