Merge pull request #115 from emberal/dotnet8-and-csharp12
Dotnet8 and csharp12
This commit is contained in:
commit
931f393425
@ -1,30 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
<LangVersion>12</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="3.9.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0"/>
|
||||
<PackageReference Include="NSubstitute" Version="5.1.0"/>
|
||||
<PackageReference Include="NUnit" Version="4.0.1"/>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0"/>
|
||||
<PackageReference Include="NUnit.Analyzers" Version="3.10.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\pac-man-board-game\pac-man-board-game.csproj" />
|
||||
<ProjectReference Include="..\pac-man-board-game\pac-man-board-game.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -11,12 +11,10 @@ using pacMan.Utils;
|
||||
|
||||
namespace BackendTests.Controllers;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(nameof(GameController))]
|
||||
public class GameControllerTests
|
||||
{
|
||||
private IActionService _actionService = null!;
|
||||
private GameController _controller = null!;
|
||||
private GameService _gameService = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
@ -27,6 +25,10 @@ public class GameControllerTests
|
||||
_controller = new GameController(Substitute.For<ILogger<GameController>>(), _gameService, _actionService);
|
||||
}
|
||||
|
||||
private IActionService _actionService = null!;
|
||||
private GameController _controller = null!;
|
||||
private GameService _gameService = null!;
|
||||
|
||||
[Test]
|
||||
public void Run_ReturnsSame()
|
||||
{
|
||||
@ -51,4 +53,22 @@ public class GameControllerTests
|
||||
else
|
||||
Assert.Fail("Result is not an ArraySegment<byte>");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DoAction_NegativeAction()
|
||||
{
|
||||
const string data = "Nothing happens";
|
||||
var message = new ActionMessage { Action = (GameAction)(-1), Data = data };
|
||||
_controller.DoAction(message);
|
||||
Assert.That(message.Data, Is.EqualTo(data));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DoAction_OutOfBoundsAction()
|
||||
{
|
||||
const string data = "Nothing happens";
|
||||
var message = new ActionMessage { Action = (GameAction)100, Data = data };
|
||||
_controller.DoAction(message);
|
||||
Assert.That(message.Data, Is.EqualTo(data));
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
using pacMan.Controllers;
|
||||
|
||||
namespace BackendTests.Controllers;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(nameof(PlayerController))]
|
||||
public class PlayerControllerTests
|
||||
{
|
||||
// TODO
|
||||
|
@ -2,6 +2,8 @@ using pacMan.GameStuff.Items;
|
||||
|
||||
namespace BackendTests.Game.Items;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(nameof(DiceCup))]
|
||||
public class DiceCupTests
|
||||
{
|
||||
[Test]
|
||||
|
@ -2,6 +2,8 @@ using pacMan.GameStuff.Items;
|
||||
|
||||
namespace BackendTests.Game.Items;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(nameof(Dice))]
|
||||
public class DiceTests
|
||||
{
|
||||
[Test]
|
||||
|
@ -2,6 +2,7 @@ using System.Text.Json;
|
||||
using BackendTests.TestUtils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using pacMan.DTOs;
|
||||
using pacMan.Exceptions;
|
||||
using pacMan.GameStuff;
|
||||
using pacMan.GameStuff.Items;
|
||||
@ -9,21 +10,9 @@ using pacMan.Services;
|
||||
|
||||
namespace BackendTests.Services;
|
||||
|
||||
[TestFixture, TestOf(nameof(ActionService))]
|
||||
public class ActionServiceTests
|
||||
{
|
||||
private readonly Player _blackPlayer = Players.Create("black");
|
||||
private readonly Player _redPlayer = Players.Create("red");
|
||||
private readonly Player _whitePlayer = Players.Create("white");
|
||||
private ActionMessage _blackMessage = null!;
|
||||
private pacMan.Services.Game _game = null!;
|
||||
private GameService _gameService = null!;
|
||||
private ActionMessage _redMessage = null!;
|
||||
private IActionService _service = null!;
|
||||
|
||||
private Queue<DirectionalPosition> _spawns = null!;
|
||||
private ActionMessage _whiteMessage = null!;
|
||||
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
@ -36,6 +25,18 @@ public class ActionServiceTests
|
||||
_service = new ActionService(Substitute.For<ILogger<ActionService>>(), _gameService);
|
||||
}
|
||||
|
||||
private readonly Player _blackPlayer = Players.Create("black");
|
||||
private readonly Player _redPlayer = Players.Create("red");
|
||||
private readonly Player _whitePlayer = Players.Create("white");
|
||||
private ActionMessage _blackMessage = null!;
|
||||
private pacMan.Services.Game _game = null!;
|
||||
private GameService _gameService = null!;
|
||||
private ActionMessage _redMessage = null!;
|
||||
private IActionService _service = null!;
|
||||
|
||||
private Queue<DirectionalPosition> _spawns = null!;
|
||||
private ActionMessage _whiteMessage = null!;
|
||||
|
||||
private JsonElement SerializeData(string username) =>
|
||||
JsonDocument.Parse(JsonSerializer.Serialize(
|
||||
new JoinGameData { Username = username, GameId = _game.Id }
|
||||
@ -50,8 +51,6 @@ public class ActionServiceTests
|
||||
new() { At = new Position { X = 9, Y = 9 }, Direction = Direction.Right }
|
||||
});
|
||||
|
||||
#region RollDice()
|
||||
|
||||
[Test]
|
||||
public void RollDice_ReturnsListOfIntegers()
|
||||
{
|
||||
@ -64,10 +63,6 @@ public class ActionServiceTests
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region PlayerInfo(ActionMessage message)
|
||||
|
||||
[Test]
|
||||
public void PlayerInfo_DataIsNull()
|
||||
{
|
||||
@ -97,32 +92,6 @@ public class ActionServiceTests
|
||||
Assert.That(new List<Player> { _whitePlayer }, Is.EqualTo(players));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DoAction(ActionMessage message)
|
||||
|
||||
[Test]
|
||||
public void DoAction_NegativeAction()
|
||||
{
|
||||
const string data = "Nothing happens";
|
||||
var message = new ActionMessage { Action = (GameAction)(-1), Data = data };
|
||||
_service.DoAction(message);
|
||||
Assert.That(message.Data, Is.EqualTo(data));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DoAction_OutOfBoundsAction()
|
||||
{
|
||||
const string data = "Nothing happens";
|
||||
var message = new ActionMessage { Action = (GameAction)100, Data = data };
|
||||
_service.DoAction(message);
|
||||
Assert.That(message.Data, Is.EqualTo(data));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Ready()
|
||||
|
||||
[Test]
|
||||
public void Ready_PlayerIsNull()
|
||||
{
|
||||
@ -183,15 +152,11 @@ public class ActionServiceTests
|
||||
Is.EqualTo(_blackPlayer.Username).Or.EqualTo(_whitePlayer.Username));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region FindNextPlayer()
|
||||
|
||||
[Test]
|
||||
public void FindNextPlayer_NoPlayers()
|
||||
{
|
||||
_service.Game = new pacMan.Services.Game(new Queue<DirectionalPosition>());
|
||||
Assert.Throws<InvalidOperationException>(() => _service.FindNextPlayer());
|
||||
Assert.Throws<PlayerNotFoundException>(() => _service.FindNextPlayer());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -221,6 +186,4 @@ public class ActionServiceTests
|
||||
var second = _service.FindNextPlayer();
|
||||
Assert.That(second, Is.EqualTo(_whitePlayer.Username));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -3,19 +3,14 @@ using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using pacMan.Exceptions;
|
||||
using pacMan.GameStuff;
|
||||
using pacMan.GameStuff.Items;
|
||||
using pacMan.Services;
|
||||
|
||||
namespace BackendTests.Services;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(nameof(GameService))]
|
||||
public class GameServiceTests
|
||||
{
|
||||
private readonly DirectionalPosition _spawn3By3Up = new()
|
||||
{ At = new Position { X = 3, Y = 3 }, Direction = Direction.Up };
|
||||
|
||||
private GameService _service = null!;
|
||||
private Queue<DirectionalPosition> _spawns = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
@ -29,7 +24,11 @@ public class GameServiceTests
|
||||
});
|
||||
}
|
||||
|
||||
#region CreateAndJoin(IPlayer player, Queue<DirectionalPosition> spawns)
|
||||
private readonly DirectionalPosition _spawn3By3Up = new()
|
||||
{ At = new Position { X = 3, Y = 3 }, Direction = Direction.Up };
|
||||
|
||||
private GameService _service = null!;
|
||||
private Queue<DirectionalPosition> _spawns = null!;
|
||||
|
||||
[Test]
|
||||
public void CreateAndJoin_WhenEmpty()
|
||||
@ -53,15 +52,11 @@ public class GameServiceTests
|
||||
Assert.Throws<ArgumentException>(() => _service.CreateAndJoin(player, _spawns));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region JoinbyId(Guid id)
|
||||
|
||||
[Test]
|
||||
public void JoinById_WhenIdNotExists()
|
||||
{
|
||||
var player = Players.Create("white");
|
||||
_service.Games.Add(new pacMan.Services.Game(_spawns) { Players = new List<Player> { player } });
|
||||
_service.Games.Add(new pacMan.Services.Game(_spawns) { Players = [player] });
|
||||
|
||||
Assert.Throws<GameNotFoundException>(() => _service.JoinById(Guid.NewGuid(), player));
|
||||
}
|
||||
@ -70,7 +65,7 @@ public class GameServiceTests
|
||||
public void JoinById_WhenIdExists()
|
||||
{
|
||||
var player = Players.Create("white");
|
||||
var game = new pacMan.Services.Game(_spawns) { Players = new List<Player> { player } };
|
||||
var game = new pacMan.Services.Game(_spawns) { Players = [player] };
|
||||
_service.Games.Add(game);
|
||||
|
||||
|
||||
@ -84,6 +79,4 @@ public class GameServiceTests
|
||||
Assert.That(_service.Games, Has.Count.EqualTo(1));
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -6,8 +6,23 @@ using pacMan.Utils;
|
||||
|
||||
namespace BackendTests.Services;
|
||||
|
||||
[TestFixture, TestOf(nameof(pacMan.Services.Game))]
|
||||
public class GameTests
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_spawns = new Queue<DirectionalPosition>(
|
||||
new[] { _spawn3By3Up, _spawn7By7Left, _spawn7By7Down, _spawn7By7Right });
|
||||
|
||||
_game = new pacMan.Services.Game(_spawns);
|
||||
_redPlayer = Players.Create("red");
|
||||
_bluePlayer = Players.Create("blue");
|
||||
_yellowPlayer = Players.Create("yellow");
|
||||
_greenPlayer = Players.Create("green");
|
||||
_purplePlayer = Players.Create("purple");
|
||||
}
|
||||
|
||||
private readonly DirectionalPosition _spawn3By3Up = new()
|
||||
{ At = new Position { X = 3, Y = 3 }, Direction = Direction.Up };
|
||||
|
||||
@ -29,20 +44,6 @@ public class GameTests
|
||||
private Queue<DirectionalPosition> _spawns = null!;
|
||||
private Player _yellowPlayer = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_spawns = new Queue<DirectionalPosition>(
|
||||
new[] { _spawn3By3Up, _spawn7By7Left, _spawn7By7Down, _spawn7By7Right });
|
||||
|
||||
_game = new pacMan.Services.Game(_spawns);
|
||||
_redPlayer = Players.Create("red");
|
||||
_bluePlayer = Players.Create("blue");
|
||||
_yellowPlayer = Players.Create("yellow");
|
||||
_greenPlayer = Players.Create("green");
|
||||
_purplePlayer = Players.Create("purple");
|
||||
}
|
||||
|
||||
private void AddFullParty()
|
||||
{
|
||||
_game.AddPlayer(_bluePlayer);
|
||||
@ -51,18 +52,12 @@ public class GameTests
|
||||
_game.AddPlayer(_greenPlayer);
|
||||
}
|
||||
|
||||
#region NextPlayer()
|
||||
|
||||
[Test]
|
||||
public void NextPlayer_WhenEmpty()
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() => _game.NextPlayer());
|
||||
Assert.Throws<PlayerNotFoundException>(() => _game.NextPlayer());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsGameStarted
|
||||
|
||||
[Test]
|
||||
public void IsGameStarted_WhenEmpty()
|
||||
{
|
||||
@ -101,10 +96,6 @@ public class GameTests
|
||||
Assert.That(_game.IsGameStarted, Is.True);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region AddPlayer(Player player)
|
||||
|
||||
[Test]
|
||||
public void AddPlayer_WhenEmpty()
|
||||
{
|
||||
@ -157,10 +148,6 @@ public class GameTests
|
||||
Assert.Throws<GameNotPlayableException>(() => _game.AddPlayer(_greenPlayer));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sendtoall(ArraySegment<byte> segment)
|
||||
|
||||
[Test]
|
||||
public void SendToAll_WhenConnectionsIsNull()
|
||||
{
|
||||
@ -171,7 +158,6 @@ public class GameTests
|
||||
public void SendToAll_WhenConnectionsIsNotNull()
|
||||
{
|
||||
var counter = 0;
|
||||
async Task Send(ArraySegment<byte> segment) => await Task.Run(() => counter++);
|
||||
|
||||
_game.Connections += Send;
|
||||
_game.Connections += Send;
|
||||
@ -182,12 +168,11 @@ public class GameTests
|
||||
while (counter < 2) { }
|
||||
|
||||
Assert.That(counter, Is.EqualTo(2));
|
||||
return;
|
||||
|
||||
async Task Send(ArraySegment<byte> segment) => await Task.Run(() => counter++);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SetReady(Player player)
|
||||
|
||||
[Test]
|
||||
public void SetReady_ReturnsAllPlayers()
|
||||
{
|
||||
@ -222,10 +207,6 @@ public class GameTests
|
||||
Assert.Throws<PlayerNotFoundException>(() => _game.SetReady(_redPlayer.Username));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SetAllIngame()
|
||||
|
||||
[Test]
|
||||
public void SetAllInGame_SetsStateToInGame()
|
||||
{
|
||||
@ -257,10 +238,6 @@ public class GameTests
|
||||
Assert.That(_game.Players, Is.Empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsGameStarted()
|
||||
|
||||
[Test]
|
||||
public void IsGameStarted_AllWaiting()
|
||||
{
|
||||
@ -275,6 +252,4 @@ public class GameTests
|
||||
_game.Players.ForEach(player => player.State = State.InGame);
|
||||
Assert.That(_game.IsGameStarted, Is.True);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -1,23 +1,22 @@
|
||||
using System.Net.WebSockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using pacMan.Interfaces;
|
||||
using pacMan.Services;
|
||||
using pacMan.Utils;
|
||||
|
||||
namespace BackendTests.Services;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(nameof(WebSocketService))]
|
||||
public class WebSocketServiceTests
|
||||
{
|
||||
private IWebSocketService _service = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
_service = new WebSocketService(Substitute.For<ILogger<WebSocketService>>());
|
||||
}
|
||||
|
||||
#region Send(Websocket, ArraySegment<byte>)
|
||||
private IWebSocketService _service = null!;
|
||||
|
||||
[Test]
|
||||
public void Send_OpenWebsocket()
|
||||
@ -48,10 +47,6 @@ public class WebSocketServiceTests
|
||||
webSocket.Received().SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Receive(Websocket, byte[])
|
||||
|
||||
[Test]
|
||||
public void Receive_ExactBuffer()
|
||||
{
|
||||
@ -90,10 +85,6 @@ public class WebSocketServiceTests
|
||||
webSocket.ReceivedWithAnyArgs().ReceiveAsync(default, CancellationToken.None);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Close(Websocket, WebSocketCloseStatus, string?)
|
||||
|
||||
[Test]
|
||||
public void Close_OpenWebsocket()
|
||||
{
|
||||
@ -124,6 +115,4 @@ public class WebSocketServiceTests
|
||||
|
||||
webSocket.ReceivedWithAnyArgs().CloseAsync(default, default, CancellationToken.None);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -3,9 +3,15 @@ using pacMan.Utils;
|
||||
|
||||
namespace BackendTests.Utils;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(nameof(Extensions))]
|
||||
public class ExtensionsTests
|
||||
{
|
||||
#region ToArraySegment(this object obj)
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_bytes = "Hello World!"u8.ToArray();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ToArraySegmentValidObject()
|
||||
@ -24,18 +30,8 @@ public class ExtensionsTests
|
||||
Assert.That(segment, Has.Count.EqualTo(4));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GetString(this byte[] bytes, int length)
|
||||
|
||||
private byte[] _bytes = null!;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_bytes = "Hello World!"u8.ToArray();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetString_ValidByteArray()
|
||||
{
|
||||
@ -65,6 +61,4 @@ public class ExtensionsTests
|
||||
{
|
||||
Assert.That(_bytes.GetString(_bytes.Length / 2), Is.EqualTo("Hello "));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>DAL</RootNamespace>
|
||||
<LangVersion>12</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -4,8 +4,8 @@ namespace DAL.Database.Service;
|
||||
|
||||
public class UserService
|
||||
{
|
||||
private readonly List<User> _users = new()
|
||||
{
|
||||
private readonly List<User> _users =
|
||||
[
|
||||
new User
|
||||
{
|
||||
Username = "Firefox",
|
||||
@ -18,10 +18,10 @@ public class UserService
|
||||
Password = "Chrome",
|
||||
Colour = "blue"
|
||||
}
|
||||
};
|
||||
];
|
||||
|
||||
public async Task<User?> Login(string username, string password)
|
||||
public Task<User?> Login(string username, string password)
|
||||
{
|
||||
return await Task.Run(() => _users.FirstOrDefault(x => x.Username == username && x.Password == password));
|
||||
return Task.Run(() => _users.FirstOrDefault(x => x.Username == username && x.Password == password));
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=4f001339_002D2d48_002D46c8_002D91bc_002D45608c0ab446/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &lt;BackendTests&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<Project Location="/home/martin/Git/Csharp/pac-man-board-game/BackendTests" Presentation="&lt;BackendTests&gt;" />
|
||||
</SessionState></s:String></wpf:ResourceDictionary>
|
||||
<s:Boolean x:Key="/Default/Environment/UnitTesting/CreateUnitTestDialog/ShowAdvancedOptions/@EntryValue">True</s:Boolean>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/CreateUnitTestDialog/TestProjectMapping/=60072632_002DA16F_002D4007_002D8A97_002DAC74B7E6703B/@EntryIndexedValue">35336347-32EB-4764-A28E-3F8FF6CA54C4</s:String>
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/CreateUnitTestDialog/TestTemplateMapping/=NUnit3x/@EntryIndexedValue">db4927dd-2e12-48a7-9a84-2b7e3e31b9c8</s:String>
|
||||
</wpf:ResourceDictionary>
|
@ -2,35 +2,37 @@
|
||||
"name": "pac_man_board_game",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"jotai": "^2.5.0",
|
||||
"jotai-devtools": "^0.7.0",
|
||||
"@heroicons/react": "^2.1.1",
|
||||
"jotai": "^2.6.0",
|
||||
"jotai-devtools": "^0.7.1",
|
||||
"oidc-client": "^1.11.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.17.0",
|
||||
"react-router-dom": "^6.21.0",
|
||||
"web-vitals": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.33",
|
||||
"@types/react-dom": "^18.2.14",
|
||||
"@vitejs/plugin-react": "^4.1.0",
|
||||
"@vitest/coverage-c8": "^0.33.0",
|
||||
"@types/node": "^20.10.7",
|
||||
"@types/react": "^18.2.45",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@vitest/coverage-v8": "^1.1.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"cross-env": "^7.0.3",
|
||||
"happy-dom": "^12.10.3",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.0.3",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-node-polyfills": "^0.15.0",
|
||||
"vite-plugin-svgr": "^4.1.0",
|
||||
"vite-tsconfig-paths": "^4.2.1",
|
||||
"vitest": "^0.34.6"
|
||||
"postcss": "^8.4.32",
|
||||
"prettier": "^3.1.1",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.10",
|
||||
"vite-plugin-node-polyfills": "^0.17.0",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^4.2.2",
|
||||
"vitest": "^1.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"css-what": "^5.0.1",
|
||||
@ -43,7 +45,7 @@
|
||||
"serve": "vite preview",
|
||||
"test": "cross-env CI=true vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
|
||||
"format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,md}\""
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
1275
pac-man-board-game/ClientApp/pnpm-lock.yaml
generated
1275
pac-man-board-game/ClientApp/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -14,6 +14,14 @@ import { getData } from "../utils/api"
|
||||
|
||||
const wsService = new WebSocketService(import.meta.env.VITE_API_WS)
|
||||
|
||||
/**
|
||||
* Represents the main game component.
|
||||
* @component
|
||||
* @param player - The current player.
|
||||
* @param map - The current game map.
|
||||
*
|
||||
* @returns The rendered game component.
|
||||
*/
|
||||
export const GameComponent: FC<{ player: Player; map: GameMap }> = ({ player, map }) => {
|
||||
const players = useAtomValue(playersAtom)
|
||||
const dice = useAtomValue(diceAtom)
|
||||
|
@ -28,7 +28,7 @@ export const GameTile: FC<TileWithCharacterProps> = ({
|
||||
showPath = false,
|
||||
}) => (
|
||||
<Tile
|
||||
className={`${possiblePath?.end ? "border-4 border-white" : ""}`}
|
||||
className={`${possiblePath?.end && "border-4 border-white"}`}
|
||||
type={type}
|
||||
onClick={possiblePath ? () => handleMoveCharacter?.(possiblePath) : undefined}
|
||||
onMouseEnter={possiblePath ? () => handleStartShowPath?.(possiblePath) : undefined}
|
||||
|
@ -4,6 +4,11 @@ import { getDefaultStore } from "jotai"
|
||||
import { currentPlayerNameAtom, playersAtom } from "../utils/state"
|
||||
import rules from "./rules"
|
||||
|
||||
/**
|
||||
* Represents the different states of a game.
|
||||
*
|
||||
* @enum {number}
|
||||
*/
|
||||
export enum State {
|
||||
waitingForPlayers,
|
||||
ready,
|
||||
|
@ -1,3 +1,11 @@
|
||||
/**
|
||||
* getData is an asynchronous function that makes an API request to retrieve data.
|
||||
* If the mode is test, it returns a promise that resolves to an empty array.
|
||||
*
|
||||
* @param path - The path of the API endpoint.
|
||||
* @param headers - The headers to be included in the request.
|
||||
* @returns - A promise that resolves to the response from the API.
|
||||
*/
|
||||
export const getData: Api = async (path, { headers } = {}) => {
|
||||
if (import.meta.env.MODE === "test") return Promise.resolve(new Response(JSON.stringify([])))
|
||||
return await fetch(import.meta.env.VITE_API_HTTP + path, {
|
||||
@ -6,6 +14,14 @@ export const getData: Api = async (path, { headers } = {}) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a POST request to the API endpoint.
|
||||
*
|
||||
* @param path - The path of the endpoint.
|
||||
* @param body - The payload of the request.
|
||||
* @param headers - Additional headers for the request.
|
||||
* @returns - A Promise that resolves to the Response object representing the server's response.
|
||||
*/
|
||||
export const postData: Api = async (path, { body, headers } = {}) => {
|
||||
return await fetch(import.meta.env.VITE_API_HTTP + path, {
|
||||
method: "POST",
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Net.WebSockets;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using pacMan.DTOs;
|
||||
using pacMan.Exceptions;
|
||||
using pacMan.GameStuff;
|
||||
using pacMan.GameStuff.Items;
|
||||
@ -8,37 +9,43 @@ using pacMan.Utils;
|
||||
|
||||
namespace pacMan.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controls the game logic and handles requests related to games.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class GameController : GenericController
|
||||
public class GameController(ILogger<GameController> logger, IGameService webSocketService, IActionService actionService)
|
||||
: GenericController(logger, webSocketService)
|
||||
{
|
||||
private readonly IActionService _actionService;
|
||||
private readonly GameService _gameService;
|
||||
[HttpGet("[action]")]
|
||||
public override Task Connect() => base.Connect();
|
||||
|
||||
public GameController(ILogger<GameController> logger, GameService webSocketService, IActionService actionService) :
|
||||
base(logger, webSocketService)
|
||||
/// <summary>
|
||||
/// Retrieves all games from the WebSocketService.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An IEnumerable of Game objects representing all the games.
|
||||
/// </returns>
|
||||
[HttpGet("[action]")]
|
||||
public IEnumerable<Game> All()
|
||||
{
|
||||
_gameService = webSocketService;
|
||||
_actionService = actionService;
|
||||
Logger.LogDebug("Returning all games");
|
||||
return webSocketService.Games;
|
||||
}
|
||||
|
||||
[HttpGet("connect")]
|
||||
public override async Task Accept() => await base.Accept();
|
||||
|
||||
[HttpGet("all")]
|
||||
public IEnumerable<Game> GetAllGames()
|
||||
/// <summary>
|
||||
/// Adds a player to a game.
|
||||
/// </summary>
|
||||
/// <param name="gameId">The unique identifier of the game.</param>
|
||||
/// <param name="player">The player to be joined.</param>
|
||||
/// <returns>An IActionResult representing the result of the operation.</returns>
|
||||
[HttpPost("[action]/{gameId:guid}")]
|
||||
public IActionResult Join(Guid gameId, [FromBody] Player player) // TODO what if player is in a game already?
|
||||
{
|
||||
Logger.Log(LogLevel.Debug, "Returning all games");
|
||||
return _gameService.Games;
|
||||
}
|
||||
|
||||
[HttpPost("join/{gameId:guid}")]
|
||||
public IActionResult JoinGame(Guid gameId, [FromBody] Player player) // TODO what if player is in a game already?
|
||||
{
|
||||
Logger.Log(LogLevel.Debug, "Joining game {}", gameId);
|
||||
Logger.LogDebug("Joining game {}", gameId);
|
||||
try
|
||||
{
|
||||
_gameService.JoinById(gameId, player);
|
||||
webSocketService.JoinById(gameId, player);
|
||||
return Ok("Game joined successfully");
|
||||
}
|
||||
catch (GameNotFoundException e)
|
||||
@ -51,20 +58,39 @@ public class GameController : GenericController
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("exists/{gameId:guid}")]
|
||||
public IActionResult GameExists(Guid gameId)
|
||||
/// <summary>
|
||||
/// Checks if a game with the specified ID exists.
|
||||
/// </summary>
|
||||
/// <param name="gameId">The ID of the game to check.</param>
|
||||
/// <returns>
|
||||
/// Returns an <see cref="IActionResult" /> representing the result of the operation.
|
||||
/// If a game with the specified ID exists, returns an <see cref="OkResult" />.
|
||||
/// If a game with the specified ID doesn't exist, returns a <see cref="NotFoundResult" />.
|
||||
/// </returns>
|
||||
[HttpGet("[action]/{gameId:guid}")]
|
||||
public IActionResult Exists(Guid gameId)
|
||||
{
|
||||
Logger.Log(LogLevel.Debug, "Checking if game {} exists", gameId);
|
||||
return _gameService.Games.Any(game => game.Id == gameId) ? Ok() : NotFound();
|
||||
Logger.LogDebug("Checking if game {} exists", gameId);
|
||||
return webSocketService.Games.Any(game => game.Id == gameId) ? Ok() : NotFound();
|
||||
}
|
||||
|
||||
[HttpPost("create")]
|
||||
public IActionResult CreateGame([FromBody] CreateGameData data)
|
||||
/// <summary>
|
||||
/// Creates a new game and adds the specified player to it.
|
||||
/// </summary>
|
||||
/// <param name="data">The data required to create the game.</param>
|
||||
/// <returns>
|
||||
/// Returns an <see cref="IActionResult" /> representing the result of the operation.
|
||||
/// If the game is successfully created, returns a <see cref="CreatedResult" /> with the game details and a location
|
||||
/// URL.
|
||||
/// If there is an error during creation, returns a <see cref="BadRequestObjectResult" /> with the error message.
|
||||
/// </returns>
|
||||
[HttpPost("[action]")]
|
||||
public IActionResult Create([FromBody] CreateGameData data)
|
||||
{
|
||||
Logger.Log(LogLevel.Debug, "Creating game");
|
||||
Logger.LogDebug("Creating game");
|
||||
try
|
||||
{
|
||||
var game = _gameService.CreateAndJoin(data.Player, data.Spawns);
|
||||
var game = webSocketService.CreateAndJoin(data.Player, data.Spawns);
|
||||
return Created($"/{game.Id}", game);
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -75,41 +101,69 @@ public class GameController : GenericController
|
||||
|
||||
protected override Task Echo()
|
||||
{
|
||||
_actionService.WebSocket = WebSocket ?? throw new NullReferenceException("WebSocket is null");
|
||||
actionService.WebSocket = WebSocket ?? throw new NullReferenceException("WebSocket is null");
|
||||
return base.Echo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the given WebSocketReceiveResult and byte array data to perform an action.
|
||||
/// </summary>
|
||||
/// <param name="result">The WebSocketReceiveResult object containing information about the received data.</param>
|
||||
/// <param name="data">The byte array data received from the WebSocket.</param>
|
||||
/// <returns>
|
||||
/// Returns an ArraySegment object representing the action response in byte array format.
|
||||
/// </returns>
|
||||
protected override ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data)
|
||||
{
|
||||
var stringResult = data.GetString(result.Count);
|
||||
|
||||
Logger.Log(LogLevel.Information, "Received: {}", stringResult);
|
||||
Logger.LogInformation("Received: {}", stringResult);
|
||||
var action = ActionMessage.FromJson(stringResult);
|
||||
|
||||
try
|
||||
{
|
||||
_actionService.DoAction(action);
|
||||
DoAction(action);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log(LogLevel.Error, "{}", e.Message);
|
||||
Logger.LogError("{}", e.Message);
|
||||
action = new ActionMessage { Action = GameAction.Error, Data = e.Message };
|
||||
}
|
||||
|
||||
return action.ToArraySegment();
|
||||
}
|
||||
|
||||
protected override async void Send(ArraySegment<byte> segment)
|
||||
/// <summary>
|
||||
/// Sends the specified data segment.
|
||||
/// </summary>
|
||||
/// <param name="segment">The data segment to send.</param>
|
||||
protected override void Send(ArraySegment<byte> segment)
|
||||
{
|
||||
if (_actionService.Game is not null)
|
||||
_actionService.SendToAll(segment);
|
||||
if (actionService.Game is not null)
|
||||
actionService.SendToAll(segment);
|
||||
else if (WebSocket is not null)
|
||||
await _gameService.Send(WebSocket, segment);
|
||||
webSocketService.Send(WebSocket, segment);
|
||||
}
|
||||
|
||||
protected override ArraySegment<byte>? Disconnect() =>
|
||||
new ActionMessage { Action = GameAction.Disconnect, Data = _actionService.Disconnect() }
|
||||
new ActionMessage { Action = GameAction.Disconnect, Data = actionService.Disconnect() }
|
||||
.ToArraySegment();
|
||||
|
||||
protected override void SendDisconnectMessage(ArraySegment<byte> segment) => _actionService.SendToAll(segment);
|
||||
protected override void SendDisconnectMessage(ArraySegment<byte> segment) => actionService.SendToAll(segment);
|
||||
|
||||
/// <summary>
|
||||
/// Performs the specified action based on the given message.
|
||||
/// </summary>
|
||||
/// <param name="message">The action message containing the action to be performed.</param>
|
||||
public void DoAction(ActionMessage message) =>
|
||||
message.Data = message.Action switch
|
||||
{
|
||||
GameAction.RollDice => actionService.RollDice(),
|
||||
GameAction.MoveCharacter => actionService.HandleMoveCharacter(message.Data),
|
||||
GameAction.JoinGame => actionService.FindGame(message.Data),
|
||||
GameAction.Ready => actionService.Ready(),
|
||||
GameAction.NextPlayer => actionService.FindNextPlayer(),
|
||||
GameAction.Disconnect => actionService.LeaveGame(),
|
||||
_ => message.Data
|
||||
};
|
||||
}
|
||||
|
@ -1,29 +1,42 @@
|
||||
using System.Net.WebSockets;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using pacMan.Interfaces;
|
||||
using pacMan.Services;
|
||||
|
||||
namespace pacMan.Controllers;
|
||||
|
||||
public abstract class GenericController : ControllerBase
|
||||
/// <summary>
|
||||
/// Represents a generic controller for handling WebSocket connections.
|
||||
/// </summary>
|
||||
public abstract class GenericController(ILogger<GenericController> logger, IWebSocketService webSocketService)
|
||||
: ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Buffer size used for processing data.
|
||||
/// </summary>
|
||||
private const int BufferSize = 1024 * 4;
|
||||
private readonly IWebSocketService _webSocketService;
|
||||
protected readonly ILogger<GenericController> Logger;
|
||||
|
||||
protected readonly ILogger<GenericController> Logger = logger;
|
||||
protected WebSocket? WebSocket;
|
||||
|
||||
protected GenericController(ILogger<GenericController> logger, IWebSocketService webSocketService)
|
||||
{
|
||||
Logger = logger;
|
||||
_webSocketService = webSocketService;
|
||||
Logger.Log(LogLevel.Debug, "WebSocket Controller created");
|
||||
}
|
||||
|
||||
public virtual async Task Accept()
|
||||
/// <summary>
|
||||
/// Establishes a WebSocket connection with the client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method checks if the HTTP request is a WebSocket request. If it is, it accepts the WebSocket connection, logs
|
||||
/// the connection establishment, and sets the WebSocket property to
|
||||
/// the accepted WebSocket instance.
|
||||
/// After the connection is established, the method calls the Echo method to start echoing messages.
|
||||
/// If the request is not a WebSocket request, it sets the HTTP response status code to 400 (BadRequest).
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// The task representing the asynchronous operation.
|
||||
/// </returns>
|
||||
public virtual async Task Connect()
|
||||
{
|
||||
if (HttpContext.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
Logger.Log(LogLevel.Information, "WebSocket connection established to {}", HttpContext.Connection.Id);
|
||||
Logger.LogInformation("WebSocket connection established to {}", HttpContext.Connection.Id);
|
||||
WebSocket = webSocket;
|
||||
await Echo();
|
||||
}
|
||||
@ -33,16 +46,21 @@ public abstract class GenericController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An asynchronous method that reads data from the WebSocket connection,
|
||||
/// processes it, and sends back the processed data.
|
||||
/// </summary>
|
||||
/// <returns>A Task representing the asynchronous operation.</returns>
|
||||
protected virtual async Task Echo()
|
||||
{
|
||||
if (WebSocket == null) return;
|
||||
if (WebSocket is null) return;
|
||||
try
|
||||
{
|
||||
WebSocketReceiveResult? result;
|
||||
do
|
||||
{
|
||||
var buffer = new byte[BufferSize];
|
||||
result = await _webSocketService.Receive(WebSocket, buffer);
|
||||
result = await webSocketService.Receive(WebSocket, buffer);
|
||||
|
||||
if (result.CloseStatus.HasValue) break;
|
||||
|
||||
@ -52,21 +70,26 @@ public abstract class GenericController : ControllerBase
|
||||
} while (true);
|
||||
|
||||
var disconnectSegment = Disconnect();
|
||||
if (disconnectSegment != null)
|
||||
if (disconnectSegment is not null)
|
||||
SendDisconnectMessage((ArraySegment<byte>)disconnectSegment);
|
||||
|
||||
await _webSocketService.Close(WebSocket, result.CloseStatus.Value, result.CloseStatusDescription);
|
||||
await webSocketService.Close(WebSocket, result.CloseStatus.Value, result.CloseStatusDescription);
|
||||
}
|
||||
catch (WebSocketException e)
|
||||
{
|
||||
Logger.Log(LogLevel.Error, "{}", e.Message);
|
||||
Logger.LogError("{}", e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual async void Send(ArraySegment<byte> segment)
|
||||
/// <summary>
|
||||
/// Sends the specified byte segment using the WebSocket connection.
|
||||
/// If the WebSocket connection is null, the method does nothing.
|
||||
/// </summary>
|
||||
/// <param name="segment">The byte segment to send.</param>
|
||||
protected virtual void Send(ArraySegment<byte> segment)
|
||||
{
|
||||
if (WebSocket == null) return;
|
||||
await _webSocketService.Send(WebSocket, segment);
|
||||
if (WebSocket is null) return;
|
||||
webSocketService.Send(WebSocket, segment);
|
||||
}
|
||||
|
||||
protected abstract ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data);
|
||||
|
@ -6,21 +6,22 @@ using pacMan.GameStuff.Items;
|
||||
namespace pacMan.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class PlayerController : ControllerBase
|
||||
[Route("api/[controller]/[action]")]
|
||||
public class PlayerController(UserService userService) : ControllerBase
|
||||
{
|
||||
private readonly UserService _userService;
|
||||
|
||||
public PlayerController(UserService userService) => _userService = userService;
|
||||
|
||||
[HttpPost("login")]
|
||||
/// <summary>
|
||||
/// Logs in a user.
|
||||
/// </summary>
|
||||
/// <param name="user">The user object containing the username and password.</param>
|
||||
/// <returns>Returns an IActionResult indicating the login result.</returns>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Login([FromBody] User user)
|
||||
{
|
||||
var result = await _userService.Login(user.Username, user.Password);
|
||||
var result = await userService.Login(user.Username, user.Password);
|
||||
if (result is null) return Unauthorized("Invalid username or password");
|
||||
return Ok((Player)result);
|
||||
}
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> Register([FromBody] User user) => throw new NotSupportedException();
|
||||
[HttpPost]
|
||||
public Task<IActionResult> Register([FromBody] User user) => throw new NotSupportedException();
|
||||
}
|
||||
|
@ -6,12 +6,11 @@ namespace pacMan.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class WsController : GenericController
|
||||
public class WsController(ILogger<WsController> logger, IWebSocketService gameService) :
|
||||
GenericController(logger, gameService)
|
||||
{
|
||||
public WsController(ILogger<WsController> logger, GameService gameService) : base(logger, gameService) { }
|
||||
|
||||
[HttpGet]
|
||||
public override async Task Accept() => await base.Accept();
|
||||
public override async Task Connect() => await base.Connect();
|
||||
|
||||
protected override ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data)
|
||||
{
|
||||
|
40
pac-man-board-game/DTOs/DTO.cs
Normal file
40
pac-man-board-game/DTOs/DTO.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using pacMan.GameStuff;
|
||||
using pacMan.GameStuff.Items;
|
||||
|
||||
namespace pacMan.DTOs;
|
||||
|
||||
public readonly record struct JoinGameData(
|
||||
[property: JsonInclude, JsonPropertyName("username"), JsonRequired]
|
||||
string Username,
|
||||
[property: JsonInclude, JsonPropertyName("gameId"), JsonRequired]
|
||||
Guid GameId
|
||||
)
|
||||
{
|
||||
public void Deconstruct(out string username, out Guid gameId) => (username, gameId) = (Username, GameId);
|
||||
}
|
||||
|
||||
public readonly record struct CreateGameData(
|
||||
[property: JsonInclude, JsonPropertyName("player"), JsonRequired]
|
||||
Player Player,
|
||||
[property: JsonInclude, JsonPropertyName("spawns"), JsonRequired]
|
||||
Queue<DirectionalPosition> Spawns
|
||||
);
|
||||
|
||||
public readonly record struct ReadyData(
|
||||
[property: JsonInclude, JsonPropertyName("allReady"), JsonRequired]
|
||||
bool AllReady,
|
||||
[property: JsonInclude, JsonPropertyName("players"), JsonRequired]
|
||||
IEnumerable<Player> Players
|
||||
);
|
||||
|
||||
public readonly record struct MovePlayerData(
|
||||
[property: JsonInclude, JsonPropertyName("players"), JsonRequired]
|
||||
List<Player> Players,
|
||||
[property: JsonInclude, JsonPropertyName("ghosts"), JsonRequired]
|
||||
List<Character> Ghosts,
|
||||
[property: JsonInclude, JsonPropertyName("dice"), JsonRequired]
|
||||
List<int> Dice,
|
||||
[property: JsonInclude, JsonPropertyName("eatenPellets"), JsonRequired]
|
||||
List<Position> EatenPellets
|
||||
);
|
@ -1,9 +1,9 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||
&& apt-get install -y \
|
||||
|
5
pac-man-board-game/Exceptions/GameExceptions.cs
Normal file
5
pac-man-board-game/Exceptions/GameExceptions.cs
Normal file
@ -0,0 +1,5 @@
|
||||
namespace pacMan.Exceptions;
|
||||
|
||||
public class GameNotFoundException(string message = "Game not found") : Exception(message);
|
||||
|
||||
public class GameNotPlayableException(string message = "Game is not allowed to be played") : Exception(message);
|
@ -1,6 +0,0 @@
|
||||
namespace pacMan.Exceptions;
|
||||
|
||||
public class GameNotFoundException : Exception
|
||||
{
|
||||
public GameNotFoundException(string message = "Game not found") : base(message) { }
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
namespace pacMan.Exceptions;
|
||||
|
||||
public class GameNotPlayableException : Exception
|
||||
{
|
||||
public GameNotPlayableException(string message = "Game is not allowed to be played") : base(message) { }
|
||||
}
|
3
pac-man-board-game/Exceptions/PlayerExceptions.cs
Normal file
3
pac-man-board-game/Exceptions/PlayerExceptions.cs
Normal file
@ -0,0 +1,3 @@
|
||||
namespace pacMan.Exceptions;
|
||||
|
||||
public class PlayerNotFoundException(string? message = "Player not found") : Exception(message);
|
@ -1,6 +0,0 @@
|
||||
namespace pacMan.Exceptions;
|
||||
|
||||
public class PlayerNotFoundException : Exception
|
||||
{
|
||||
public PlayerNotFoundException(string? message = "Player not found") : base(message) { }
|
||||
}
|
@ -3,6 +3,9 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace pacMan.GameStuff;
|
||||
|
||||
/// <summary>
|
||||
/// Represents various actions that can be performed in a game.
|
||||
/// </summary>
|
||||
public enum GameAction
|
||||
{
|
||||
Error,
|
||||
@ -14,13 +17,23 @@ public enum GameAction
|
||||
Disconnect
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an action message with optional data of type <typeparamref name="T" />.
|
||||
/// Every Action may have a different type of data, or no data at all.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the data.</typeparam>
|
||||
public class ActionMessage<T>
|
||||
{
|
||||
[JsonPropertyName("action")] public GameAction Action { get; init; }
|
||||
|
||||
[JsonPropertyName("data")] public T? Data { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Parses a JSON string into an ActionMessage object. With dynamic data.
|
||||
/// </summary>
|
||||
/// <param name="json">The JSON string to deserialize.</param>
|
||||
/// <returns>An ActionMessage object populated with the deserialized data.</returns>
|
||||
public static ActionMessage FromJson(string json) => JsonSerializer.Deserialize<ActionMessage>(json)!;
|
||||
}
|
||||
|
||||
public class ActionMessage : ActionMessage<dynamic> { }
|
||||
public class ActionMessage : ActionMessage<dynamic>;
|
||||
|
@ -31,9 +31,12 @@ public class Character : IEquatable<Character>
|
||||
return obj.GetType() == GetType() && Equals((Character)obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(Colour, Position, IsEatable, SpawnPosition, (int?)Type);
|
||||
public override int GetHashCode() => HashCode.Combine(Colour, Type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the types of characters in a game.
|
||||
/// </summary>
|
||||
public enum CharacterType
|
||||
{
|
||||
PacMan,
|
||||
|
@ -4,10 +4,17 @@ namespace pacMan.GameStuff.Items;
|
||||
|
||||
public class Box : IEquatable<Box>
|
||||
{
|
||||
[JsonPropertyName("pellets")] public int Pellets { get; init; }
|
||||
[JsonPropertyName("powerPellets")] public int PowerPellet { get; init; }
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("pellets")]
|
||||
public int Pellets { get; init; }
|
||||
|
||||
[JsonPropertyName("colour")] public required string Colour { get; init; }
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("powerPellets")]
|
||||
public int PowerPellet { get; init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("colour")]
|
||||
public required string Colour { get; init; }
|
||||
|
||||
public bool Equals(Box? other)
|
||||
{
|
||||
|
@ -6,7 +6,14 @@ public class Dice
|
||||
{
|
||||
private readonly Random _random = new();
|
||||
|
||||
[JsonInclude] public int Value { get; private set; }
|
||||
/// <summary>
|
||||
/// Represents the value of the previous roll.
|
||||
/// </summary>
|
||||
[JsonInclude]
|
||||
public int Value { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rolls a dice by generating a random number between 1 and 6 and assigns it to the 'Value' property of the dice.
|
||||
/// </summary>
|
||||
public void Roll() => Value = _random.Next(1, 7);
|
||||
}
|
||||
|
@ -2,18 +2,24 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace pacMan.GameStuff.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a cup containing multiple dice.
|
||||
/// </summary>
|
||||
public class DiceCup
|
||||
{
|
||||
private readonly List<Dice> _dices;
|
||||
private readonly List<Dice> _dices = [new Dice(), new Dice()];
|
||||
|
||||
public DiceCup() =>
|
||||
_dices = new List<Dice>
|
||||
{
|
||||
new(),
|
||||
new()
|
||||
};
|
||||
|
||||
[JsonInclude] public List<int> Values => _dices.Select(dice => dice.Value).ToList();
|
||||
/// <summary>
|
||||
/// Gets a list of integer values representing the values of the dices.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A list of integer values representing the values of the dices.
|
||||
/// </value>
|
||||
[JsonInclude]
|
||||
public List<int> Values => _dices.Select(dice => dice.Value).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Rolls all the dice in the list.
|
||||
/// </summary>
|
||||
public void Roll() => _dices.ForEach(dice => dice.Roll());
|
||||
}
|
||||
|
@ -3,6 +3,9 @@ using DAL.Database.Models;
|
||||
|
||||
namespace pacMan.GameStuff.Items;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the various states of a 'Player'.
|
||||
/// </summary>
|
||||
public enum State
|
||||
{
|
||||
WaitingForPlayers,
|
||||
@ -11,6 +14,9 @@ public enum State
|
||||
Disconnected
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a player in the game.
|
||||
/// </summary>
|
||||
public class Player : IEquatable<Player>, ICloneable
|
||||
{
|
||||
[JsonPropertyName("username")] public required string Username { get; init; }
|
||||
|
@ -2,6 +2,9 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace pacMan.GameStuff;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a move path consisting of a sequence of positions and a target end position with a specified direction.
|
||||
/// </summary>
|
||||
public class MovePath : IEquatable<MovePath>
|
||||
{
|
||||
[JsonInclude]
|
||||
@ -19,6 +22,11 @@ public class MovePath : IEquatable<MovePath>
|
||||
return Equals(Path, other.Path) && End.Equals(other.End) && Direction == other.Direction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a DirectionalPosition object to a MovePath object.
|
||||
/// </summary>
|
||||
/// <param name="path">The DirectionalPosition object to convert.</param>
|
||||
/// <returns>A MovePath object with the same End and Direction as the DirectionalPosition object.</returns>
|
||||
public static implicit operator MovePath(DirectionalPosition path) =>
|
||||
new()
|
||||
{
|
||||
@ -33,9 +41,12 @@ public class MovePath : IEquatable<MovePath>
|
||||
return obj.GetType() == GetType() && Equals((MovePath)obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(Path, End, (int)Direction);
|
||||
public override int GetHashCode() => HashCode.Combine(End, (int)Direction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a position with x and y coordinates.
|
||||
/// </summary>
|
||||
public class Position : IEquatable<Position>
|
||||
{
|
||||
[JsonPropertyName("x")] public int X { get; init; }
|
||||
@ -59,6 +70,9 @@ public class Position : IEquatable<Position>
|
||||
public override int GetHashCode() => HashCode.Combine(X, Y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enum representing the possible directions: Left, Up, Right, and Down.
|
||||
/// </summary>
|
||||
public enum Direction
|
||||
{
|
||||
Left,
|
||||
@ -67,6 +81,9 @@ public enum Direction
|
||||
Down
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a directional position with a coordinate and a direction.
|
||||
/// </summary>
|
||||
public class DirectionalPosition : IEquatable<DirectionalPosition>
|
||||
{
|
||||
[JsonPropertyName("at")] public required Position At { get; init; }
|
||||
@ -80,6 +97,11 @@ public class DirectionalPosition : IEquatable<DirectionalPosition>
|
||||
return At.Equals(other.At) && Direction == other.Direction;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a MovePath object to a DirectionalPosition object.
|
||||
/// </summary>
|
||||
/// <param name="path">The MovePath object to convert.</param>
|
||||
/// <returns>A DirectionalPosition object representing the converted MovePath object.</returns>
|
||||
public static explicit operator DirectionalPosition(MovePath path) =>
|
||||
new()
|
||||
{
|
||||
|
@ -1,6 +1,9 @@
|
||||
namespace pacMan.GameStuff;
|
||||
|
||||
public class Rules
|
||||
/// <summary>
|
||||
/// The Rules class holds constant values related to the game rules.
|
||||
/// </summary>
|
||||
public static class Rules
|
||||
{
|
||||
public const int MinPlayers = 2;
|
||||
public const int MaxPlayers = 4;
|
||||
|
@ -1,10 +0,0 @@
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace pacMan.Interfaces;
|
||||
|
||||
public interface IWebSocketService
|
||||
{
|
||||
Task Send(WebSocket webSocket, ArraySegment<byte> segment);
|
||||
Task<WebSocketReceiveResult> Receive(WebSocket webSocket, byte[] buffer);
|
||||
Task Close(WebSocket webSocket, WebSocketCloseStatus closeStatus, string? closeStatusDescription);
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using DAL.Database.Service;
|
||||
using pacMan.Interfaces;
|
||||
using pacMan.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@ -8,6 +7,7 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddControllersWithViews();
|
||||
builder.Services
|
||||
.AddSingleton<IGameService, GameService>()
|
||||
.AddSingleton<IWebSocketService, WebSocketService>()
|
||||
.AddSingleton<GameService>()
|
||||
.AddScoped<UserService>()
|
||||
|
@ -1,8 +1,7 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using pacMan.DTOs;
|
||||
using pacMan.Exceptions;
|
||||
using pacMan.GameStuff;
|
||||
using pacMan.GameStuff.Items;
|
||||
|
||||
namespace pacMan.Services;
|
||||
@ -11,11 +10,10 @@ public interface IActionService
|
||||
{
|
||||
Player Player { set; }
|
||||
Game? Game { get; set; }
|
||||
WebSocket? WebSocket { set; }
|
||||
void DoAction(ActionMessage message);
|
||||
WebSocket WebSocket { set; }
|
||||
List<int> RollDice();
|
||||
List<Player> FindGame(JsonElement? jsonElement);
|
||||
object? HandleMoveCharacter(JsonElement? jsonElement);
|
||||
MovePlayerData HandleMoveCharacter(JsonElement? jsonElement);
|
||||
ReadyData Ready();
|
||||
string FindNextPlayer();
|
||||
List<Player> LeaveGame();
|
||||
@ -23,68 +21,65 @@ public interface IActionService
|
||||
List<Player>? Disconnect();
|
||||
}
|
||||
|
||||
public class ActionService : IActionService
|
||||
/// <summary>
|
||||
/// Provides various actions that can be performed in a game
|
||||
/// </summary>
|
||||
public class ActionService(ILogger<ActionService> logger, IGameService gameService) : IActionService
|
||||
{
|
||||
private readonly GameService _gameService;
|
||||
private readonly ILogger<ActionService> _logger;
|
||||
|
||||
public ActionService(ILogger<ActionService> logger, GameService gameService)
|
||||
{
|
||||
_logger = logger;
|
||||
_gameService = gameService;
|
||||
}
|
||||
|
||||
public WebSocket? WebSocket { private get; set; }
|
||||
public WebSocket WebSocket { private get; set; } = null!;
|
||||
|
||||
public Game? Game { get; set; }
|
||||
|
||||
public Player? Player { get; set; }
|
||||
|
||||
public void DoAction(ActionMessage message)
|
||||
{
|
||||
message.Data = message.Action switch
|
||||
{
|
||||
GameAction.RollDice => RollDice(),
|
||||
GameAction.MoveCharacter => HandleMoveCharacter(message.Data),
|
||||
GameAction.JoinGame => FindGame(message.Data),
|
||||
GameAction.Ready => Ready(),
|
||||
GameAction.NextPlayer => FindNextPlayer(),
|
||||
GameAction.Disconnect => LeaveGame(),
|
||||
_ => message.Data
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rolls the dice and returns the result. If the game is null, an empty list is returned.
|
||||
/// </summary>
|
||||
/// <returns>A list of integers representing the values rolled on the dice.</returns>
|
||||
public List<int> RollDice()
|
||||
{
|
||||
Game?.DiceCup.Roll();
|
||||
var rolls = Game?.DiceCup.Values ?? new List<int>();
|
||||
_logger.Log(LogLevel.Information, "Rolled [{}]", string.Join(", ", rolls));
|
||||
var rolls = Game?.DiceCup.Values ?? [];
|
||||
logger.LogInformation("Rolled [{}]", string.Join(", ", rolls));
|
||||
|
||||
return rolls;
|
||||
}
|
||||
|
||||
public object? HandleMoveCharacter(JsonElement? jsonElement)
|
||||
/// <summary>
|
||||
/// Handles the movement of the character based on the provided JSON element.
|
||||
/// </summary>
|
||||
/// <param name="jsonElement">The JSON element containing the data to move the character.</param>
|
||||
/// <returns>The MovePlayerData object representing the updated character movement information.</returns>
|
||||
public MovePlayerData HandleMoveCharacter(JsonElement? jsonElement)
|
||||
{
|
||||
if (Game != null && jsonElement.HasValue)
|
||||
var data = jsonElement?.Deserialize<MovePlayerData>() ?? throw new NullReferenceException("Data is null");
|
||||
if (Game is not null)
|
||||
{
|
||||
Game.Ghosts = jsonElement.Value.GetProperty("ghosts").Deserialize<List<Character>>() ??
|
||||
throw new NullReferenceException("Ghosts is null");
|
||||
Game.Players = jsonElement.Value.GetProperty("players").Deserialize<List<Player>>() ??
|
||||
throw new NullReferenceException("Players is null");
|
||||
Game.Ghosts = data.Ghosts;
|
||||
Game.Players = data.Players;
|
||||
}
|
||||
|
||||
return jsonElement;
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a game based on the given JSON element.
|
||||
/// </summary>
|
||||
/// <param name="jsonElement">The JSON data containing the username and gameId.</param>
|
||||
/// <returns>The list of players in the found game.</returns>
|
||||
/// <exception cref="NullReferenceException">Thrown when the JSON data is null.</exception>
|
||||
/// <exception cref="GameNotFoundException">Thrown when the game with the given gameId does not exist.</exception>
|
||||
/// <exception cref="PlayerNotFoundException">Thrown when the player with the given username is not found in the game.</exception>
|
||||
public List<Player> FindGame(JsonElement? jsonElement)
|
||||
{
|
||||
var data = jsonElement?.Deserialize<JoinGameData>() ?? throw new NullReferenceException("Data is null");
|
||||
var (username, gameId) =
|
||||
jsonElement?.Deserialize<JoinGameData>() ?? throw new NullReferenceException("Data is null");
|
||||
|
||||
var game = _gameService.Games.FirstOrDefault(game => game.Id == data.GameId) ??
|
||||
throw new GameNotFoundException($"Game was not found, id \"{data.GameId}\" does not exist");
|
||||
var game = gameService.FindGameById(gameId) ??
|
||||
throw new GameNotFoundException($"Game was not found, id \"{gameId}\" does not exist");
|
||||
|
||||
var player = game.Players.Find(p => p.Username == data.Username)
|
||||
?? throw new PlayerNotFoundException($"Player \"{data.Username}\" was not found in game");
|
||||
var player = game.FindPlayerByUsername(username) ??
|
||||
throw new PlayerNotFoundException($"Player \"{username}\" was not found in game");
|
||||
|
||||
player.State = game.IsGameStarted ? State.InGame : State.WaitingForPlayers; // TODO doesn't work anymore
|
||||
Player = player;
|
||||
@ -94,10 +89,18 @@ public class ActionService : IActionService
|
||||
return Game.Players;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the game and returns relevant data.
|
||||
/// </summary>
|
||||
/// <exception cref="PlayerNotFoundException">Thrown when the player is not found.</exception>
|
||||
/// <exception cref="GameNotFoundException">Thrown when the game is not found.</exception>
|
||||
/// <returns>A <see cref="ReadyData" /> object containing information about game readiness.</returns>
|
||||
public ReadyData Ready()
|
||||
{
|
||||
if (Player == null || Game == null)
|
||||
if (Player is null)
|
||||
throw new PlayerNotFoundException("Player not found, please create a new player");
|
||||
if (Game is null)
|
||||
throw new GameNotFoundException();
|
||||
|
||||
var players = Game.SetReady(Player.Username).ToArray();
|
||||
// TODO roll to start
|
||||
@ -107,61 +110,55 @@ public class ActionService : IActionService
|
||||
return new ReadyData { AllReady = allReady, Players = players };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the next player in the game.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// The username of the next player in the game, if available.
|
||||
/// </returns>
|
||||
/// <exception cref="GameNotFoundException">
|
||||
/// Thrown if the game is not found.
|
||||
/// </exception>
|
||||
public string FindNextPlayer() => Game?.NextPlayer().Username ?? throw new GameNotFoundException();
|
||||
|
||||
/// <summary>
|
||||
/// Removes the player from the game.
|
||||
/// </summary>
|
||||
/// <exception cref="NullReferenceException">Throws if the game or player is null.</exception>
|
||||
/// <returns>A list of remaining players in the game.</returns>
|
||||
public List<Player> LeaveGame()
|
||||
{
|
||||
if (Game == null || Player == null) throw new NullReferenceException("Game or Player is null");
|
||||
if (Game is null) throw new NullReferenceException("Game is null");
|
||||
if (Player is null) throw new NullReferenceException("Player is null");
|
||||
Game.RemovePlayer(Player.Username);
|
||||
return Game.Players;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects the player from the game.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Returns the list of players in the game after disconnecting the player.
|
||||
/// Returns null if the player is already disconnected or is not connected to a game.
|
||||
/// </returns>
|
||||
public List<Player>? Disconnect()
|
||||
{
|
||||
if (Player == null) return null;
|
||||
if (Player is null) return null;
|
||||
Player.State = State.Disconnected;
|
||||
if (Game != null) Game.Connections -= SendSegment;
|
||||
if (Game is not null) Game.Connections -= SendSegment;
|
||||
return Game?.Players;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a given byte segment to all players in the game.
|
||||
/// </summary>
|
||||
/// <param name="segment">The byte segment to send.</param>
|
||||
public void SendToAll(ArraySegment<byte> segment) => Game?.SendToAll(segment);
|
||||
|
||||
private async Task SendSegment(ArraySegment<byte> segment)
|
||||
{
|
||||
if (WebSocket != null) await _gameService.Send(WebSocket, segment);
|
||||
else await Task.FromCanceled(new CancellationToken(true));
|
||||
}
|
||||
}
|
||||
|
||||
public struct JoinGameData
|
||||
{
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("username")]
|
||||
public required string Username { get; init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("gameId")]
|
||||
public required Guid GameId { get; init; }
|
||||
}
|
||||
|
||||
public struct CreateGameData
|
||||
{
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("player")]
|
||||
public required Player Player { get; init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("spawns")]
|
||||
public required Queue<DirectionalPosition> Spawns { get; init; }
|
||||
}
|
||||
|
||||
public struct ReadyData
|
||||
{
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("allReady")]
|
||||
public required bool AllReady { get; init; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("players")]
|
||||
public required IEnumerable<Player> Players { get; set; }
|
||||
/// <summary>
|
||||
/// Sends an array segment of bytes through the WebSocket connection.
|
||||
/// </summary>
|
||||
/// <param name="segment">The array segment of bytes to send.</param>
|
||||
/// <returns>A task that represents the asynchronous send operation.</returns>
|
||||
private Task SendSegment(ArraySegment<byte> segment) => gameService.Send(WebSocket, segment);
|
||||
}
|
||||
|
@ -5,16 +5,21 @@ using pacMan.GameStuff.Items;
|
||||
|
||||
namespace pacMan.Services;
|
||||
|
||||
public class Game
|
||||
/// <summary>
|
||||
/// Represents a game instance.
|
||||
/// </summary>
|
||||
public class Game(Queue<DirectionalPosition> spawns)
|
||||
{
|
||||
private readonly Random _random = new();
|
||||
private int _currentPlayerIndex;
|
||||
private List<Player> _players = new();
|
||||
|
||||
public Game(Queue<DirectionalPosition> spawns) => Spawns = spawns;
|
||||
private List<Player> _players = [];
|
||||
|
||||
[JsonInclude] public Guid Id { get; } = Guid.NewGuid();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of players.
|
||||
/// When setting, the mutable values of the players are updated instead of replacing the list.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public List<Player> Players
|
||||
{
|
||||
@ -34,17 +39,36 @@ public class Game
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore] public List<Character> Ghosts { get; set; } = new(); // TODO include
|
||||
[JsonIgnore] public List<Character> Ghosts { get; set; } = []; // TODO include
|
||||
|
||||
[JsonIgnore] private Queue<DirectionalPosition> Spawns { get; }
|
||||
/// <summary>
|
||||
/// The spawn locations on the map.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// A Queue of DirectionalPositions representing the spawn locations on the map.
|
||||
/// </value>
|
||||
[JsonIgnore]
|
||||
private Queue<DirectionalPosition> Spawns { get; } = spawns;
|
||||
|
||||
[JsonIgnore] public DiceCup DiceCup { get; } = new(); // TODO include
|
||||
|
||||
[JsonInclude] public int Count => Players.Count;
|
||||
|
||||
// TODO edge-case when game has started but all players have disconnected, Disconnected property?
|
||||
[JsonInclude] public bool IsGameStarted => Count > 0 && Players.Any(player => player.State is State.InGame);
|
||||
/// <summary>
|
||||
/// Whether or not the game has started.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The game is considered started if the count is greater than zero and at least one player is in the "InGame" state.
|
||||
/// </remarks>
|
||||
[JsonInclude]
|
||||
public bool IsGameStarted => Count > 0 && Players.Any(player => player.State is State.InGame);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next player in the game.
|
||||
/// </summary>
|
||||
/// <returns>The next player.</returns>
|
||||
/// <exception cref="PlayerNotFoundException">Thrown when there are no players in the game.</exception>
|
||||
public Player NextPlayer()
|
||||
{
|
||||
try
|
||||
@ -53,7 +77,7 @@ public class Game
|
||||
}
|
||||
catch (DivideByZeroException)
|
||||
{
|
||||
throw new InvalidOperationException("There are no players in the game.");
|
||||
throw new PlayerNotFoundException("There are no players in the game.");
|
||||
}
|
||||
|
||||
return Players[_currentPlayerIndex];
|
||||
@ -61,8 +85,22 @@ public class Game
|
||||
|
||||
public void Shuffle() => Players.Sort((_, _) => _random.Next(-1, 2));
|
||||
|
||||
/// <summary>
|
||||
/// An event that is invoked when a message is to be sent to all connections.
|
||||
/// Each player in the game should be listening to this event.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The event handler is of type <see cref="Func{T, TResult}" /> which accepts an <see cref="ArraySegment{T}" /> of
|
||||
/// bytes and returns a <see cref="Task" />.
|
||||
/// This event is typically used to perform some action when something happens.
|
||||
/// </remarks>
|
||||
public event Func<ArraySegment<byte>, Task>? Connections;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a player to the game.
|
||||
/// </summary>
|
||||
/// <param name="player">The player to be added.</param>
|
||||
/// <exception cref="GameNotPlayableException">Thrown when the game is already full or has already started.</exception>
|
||||
public void AddPlayer(Player player)
|
||||
{
|
||||
if (Players.Count >= Rules.MaxPlayers)
|
||||
@ -86,6 +124,10 @@ public class Game
|
||||
return removedPlayer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the spawn position and current position of the specified player's PacMan character.
|
||||
/// </summary>
|
||||
/// <param name="player">The player whose PacMan character's spawn and current positions will be set.</param>
|
||||
private void SetSpawn(Player player)
|
||||
{
|
||||
if (player.PacMan.SpawnPosition is not null) return;
|
||||
@ -94,8 +136,17 @@ public class Game
|
||||
player.PacMan.Position = spawn;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the specified byte segment to all connected clients.
|
||||
/// </summary>
|
||||
/// <param name="segment">The byte segment to send.</param>
|
||||
public void SendToAll(ArraySegment<byte> segment) => Connections?.Invoke(segment);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the state of the player with the specified username to Ready.
|
||||
/// </summary>
|
||||
/// <param name="username">The username of the player.</param>
|
||||
/// <returns>An enumerable collection of Player objects.</returns>
|
||||
public IEnumerable<Player> SetReady(string username)
|
||||
{
|
||||
var player = Players.FirstOrDefault(p => p.Username == username);
|
||||
@ -105,11 +156,25 @@ public class Game
|
||||
return Players;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets all players to the "InGame" state if they are currently in the "Ready" state.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// Returns true if all players were successfully set to the "InGame" state, false otherwise.
|
||||
/// </returns>
|
||||
public bool SetAllInGame()
|
||||
{
|
||||
if (Players.Any(player => player.State != State.Ready)) return false;
|
||||
if (Players.Any(player => player.State is not State.Ready)) return false;
|
||||
|
||||
foreach (var player in Players) player.State = State.InGame;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a player by their username.
|
||||
/// </summary>
|
||||
/// <param name="username">The username of the player to find.</param>
|
||||
/// <returns>The found Player object if a player with the given username is found; otherwise, null.</returns>
|
||||
public Player? FindPlayerByUsername(string username) =>
|
||||
Players.FirstOrDefault(player => player.Username == username);
|
||||
}
|
||||
|
@ -4,14 +4,21 @@ using pacMan.GameStuff.Items;
|
||||
|
||||
namespace pacMan.Services;
|
||||
|
||||
public interface IGameService : IWebSocketService
|
||||
{
|
||||
SynchronizedCollection<Game> Games { get; }
|
||||
Game JoinById(Guid id, Player player);
|
||||
Game CreateAndJoin(Player player, Queue<DirectionalPosition> spawns);
|
||||
Game? FindGameById(Guid id);
|
||||
Game? FindGameByUsername(string username);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The GameService class provides functionality for managing games in a WebSocket environment. It inherits from the
|
||||
/// WebSocketService class.
|
||||
/// </summary>
|
||||
public class GameService : WebSocketService
|
||||
public class GameService(ILogger<WebSocketService> logger) : WebSocketService(logger), IGameService
|
||||
{
|
||||
public GameService(ILogger<GameService> logger) : base(logger) { }
|
||||
|
||||
/// <summary>
|
||||
/// A thread-safe collection (SynchronizedCollection) of "Game" objects. Utilized for managing multiple game instances
|
||||
/// simultaneously.
|
||||
@ -57,8 +64,20 @@ public class GameService : WebSocketService
|
||||
return game;
|
||||
}
|
||||
|
||||
public Game? FindGameByUsername(string username)
|
||||
{
|
||||
return Games.FirstOrDefault(game => game.Players.Exists(player => player.Username == username));
|
||||
}
|
||||
/// <summary>
|
||||
/// Finds a game by its ID.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the game.</param>
|
||||
/// <returns>The game with the specified ID, or null if no game was found.</returns>
|
||||
public Game? FindGameById(Guid id) => Games.FirstOrDefault(game => game.Id == id);
|
||||
|
||||
/// <summary>
|
||||
/// Finds a game by the given username.
|
||||
/// </summary>
|
||||
/// <param name="username">The username to search for.</param>
|
||||
/// <returns>
|
||||
/// The found game, if any. Returns null if no game is found.
|
||||
/// </returns>
|
||||
public Game? FindGameByUsername(string username) =>
|
||||
Games.FirstOrDefault(game => game.Players.Exists(player => player.Username == username));
|
||||
}
|
||||
|
@ -1,20 +1,33 @@
|
||||
using System.Net.WebSockets;
|
||||
using pacMan.Interfaces;
|
||||
using pacMan.Utils;
|
||||
|
||||
namespace pacMan.Services;
|
||||
|
||||
|
||||
public class WebSocketService : IWebSocketService
|
||||
public interface IWebSocketService
|
||||
{
|
||||
protected readonly ILogger<WebSocketService> Logger;
|
||||
|
||||
public WebSocketService(ILogger<WebSocketService> logger)
|
||||
{
|
||||
Logger = logger;
|
||||
logger.Log(LogLevel.Debug, "WebSocket Service created");
|
||||
}
|
||||
Task Send(WebSocket webSocket, ArraySegment<byte> segment);
|
||||
Task<WebSocketReceiveResult> Receive(WebSocket webSocket, byte[] buffer);
|
||||
Task Close(WebSocket webSocket, WebSocketCloseStatus closeStatus, string? closeStatusDescription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WebSocketService class provides methods to send, receive and close a WebSocket connection.
|
||||
/// </summary>
|
||||
public class WebSocketService(ILogger<WebSocketService> logger) : IWebSocketService
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends the specified byte array as a text message through the WebSocket connection.
|
||||
/// </summary>
|
||||
/// <param name="webSocket">The WebSocket connection.</param>
|
||||
/// <param name="segment">The byte array to send.</param>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous operation of sending the message.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This method sends the specified byte array as a text message through the WebSocket connection.
|
||||
/// It uses the WebSocket.SendAsync method to send the message asynchronously.
|
||||
/// After sending the message, it logs a debug message using the logger provided.
|
||||
/// </remarks>
|
||||
public async Task Send(WebSocket webSocket, ArraySegment<byte> segment)
|
||||
{
|
||||
await webSocket.SendAsync(
|
||||
@ -23,18 +36,34 @@ public class WebSocketService : IWebSocketService
|
||||
true,
|
||||
CancellationToken.None);
|
||||
|
||||
Logger.Log(LogLevel.Debug, "Message sent to WebSocket");
|
||||
logger.LogDebug("Message sent through WebSocket");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Receives data from a websocket and logs a debug message.
|
||||
/// </summary>
|
||||
/// <param name="webSocket">The websocket to receive data from.</param>
|
||||
/// <param name="buffer">The buffer to store the received data.</param>
|
||||
/// <returns>
|
||||
/// A task representing the asynchronous operation. The result contains the <see cref="WebSocketReceiveResult" />
|
||||
/// which contains information about the received data.
|
||||
/// </returns>
|
||||
public async Task<WebSocketReceiveResult> Receive(WebSocket webSocket, byte[] buffer)
|
||||
{
|
||||
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
|
||||
Logger.Log(LogLevel.Debug,
|
||||
logger.LogDebug(
|
||||
"Message \"{}\" received from WebSocket",
|
||||
buffer.GetString(result.Count));
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the WebSocket connection with the specified close status and description.
|
||||
/// </summary>
|
||||
/// <param name="webSocket">The WebSocket connection to close.</param>
|
||||
/// <param name="closeStatus">The status code indicating the reason for the close.</param>
|
||||
/// <param name="closeStatusDescription">The optional description explaining the reason for the close.</param>
|
||||
/// <returns>A task representing the asynchronous operation.</returns>
|
||||
public async Task Close(WebSocket webSocket, WebSocketCloseStatus closeStatus, string? closeStatusDescription)
|
||||
{
|
||||
await webSocket.CloseAsync(
|
||||
@ -42,6 +71,6 @@ public class WebSocketService : IWebSocketService
|
||||
closeStatusDescription,
|
||||
CancellationToken.None);
|
||||
|
||||
Logger.Log(LogLevel.Information, "WebSocket connection closed");
|
||||
logger.LogInformation("WebSocket connection closed");
|
||||
}
|
||||
}
|
||||
|
@ -6,13 +6,24 @@ namespace pacMan.Utils;
|
||||
|
||||
public static partial class Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts the specified byte array into a string using UTF-8 encoding and then removes any invalid characters.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The byte array to convert.</param>
|
||||
/// <param name="length">The number of bytes to decode.</param>
|
||||
/// <returns>The decoded string without any invalid characters.</returns>
|
||||
public static string GetString(this byte[] bytes, int length)
|
||||
{
|
||||
var s = Encoding.UTF8.GetString(bytes, 0, length);
|
||||
// Removes invalid characters from the string
|
||||
return InvalidCharacters().Replace(s, "");
|
||||
return InvalidCharacters().Replace(s, string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an object to an <see cref="ArraySegment{T}" /> of bytes.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to convert.</param>
|
||||
/// <returns>An <see cref="ArraySegment{T}" /> of bytes representing the serialized object in UTF-8 encoding.</returns>
|
||||
public static ArraySegment<byte> ToArraySegment(this object obj)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(obj);
|
||||
@ -20,6 +31,10 @@ public static partial class Extensions
|
||||
return new ArraySegment<byte>(bytes, 0, json.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a regular expression pattern that matches invalid characters.
|
||||
/// </summary>
|
||||
/// <returns>A regular expression pattern for matching invalid characters.</returns>
|
||||
[GeneratedRegex("\\p{C}+")]
|
||||
private static partial Regex InvalidCharacters();
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
|
||||
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
||||
@ -13,70 +13,71 @@
|
||||
<RootNamespace>pacMan</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<LangVersion>12</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="7.0.13" />
|
||||
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.2.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SpaProxy" Version="8.0.0"/>
|
||||
<PackageReference Include="Microsoft.TypeScript.MSBuild" Version="5.3.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.ServiceModel.Primitives" Version="6.1.0" />
|
||||
<PackageReference Include="System.ServiceModel.Primitives" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Don't publish the SPA source files, but do show them in the project files list -->
|
||||
<Content Remove="$(SpaRoot)**" />
|
||||
<Content Remove="$(SpaRoot)**"/>
|
||||
<Content Include="..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
<None Remove="$(SpaRoot)**" />
|
||||
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
|
||||
<None Remove="$(SpaRoot)**"/>
|
||||
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<TypeScriptCompile Remove="ClientApp\src\components\Counter.tsx" />
|
||||
<TypeScriptCompile Remove="ClientApp\src\components\FetchData.tsx" />
|
||||
<TypeScriptCompile Remove="ClientApp\src\components\Home.tsx" />
|
||||
<TypeScriptCompile Remove="ClientApp\src\pages\FetchData.tsx" />
|
||||
<TypeScriptCompile Remove="ClientApp\src\classes\tileMap.ts" />
|
||||
<TypeScriptCompile Remove="ClientApp\src\game\tileMap.ts" />
|
||||
<TypeScriptCompile Remove="ClientApp\src\components\gameCanvas.tsx" />
|
||||
<TypeScriptCompile Remove="ClientApp\src\game\game.ts" />
|
||||
<TypeScriptCompile Remove="ClientApp\src\App.test.tsx" />
|
||||
<TypeScriptCompile Remove="ClientApp\src\game\playerStats.tsx" />
|
||||
<TypeScriptCompile Remove="ClientApp\src\websockets\actions.ts" />
|
||||
<TypeScriptCompile Remove="ClientApp\src\utils\colours.ts" />
|
||||
<TypeScriptCompile Remove="ClientApp\src\utils\dom.ts" />
|
||||
<TypeScriptCompile Remove="ClientApp\src\game\pellet.ts" />
|
||||
<TypeScriptCompile Remove="ClientApp\src\components\Counter.tsx"/>
|
||||
<TypeScriptCompile Remove="ClientApp\src\components\FetchData.tsx"/>
|
||||
<TypeScriptCompile Remove="ClientApp\src\components\Home.tsx"/>
|
||||
<TypeScriptCompile Remove="ClientApp\src\pages\FetchData.tsx"/>
|
||||
<TypeScriptCompile Remove="ClientApp\src\classes\tileMap.ts"/>
|
||||
<TypeScriptCompile Remove="ClientApp\src\game\tileMap.ts"/>
|
||||
<TypeScriptCompile Remove="ClientApp\src\components\gameCanvas.tsx"/>
|
||||
<TypeScriptCompile Remove="ClientApp\src\game\game.ts"/>
|
||||
<TypeScriptCompile Remove="ClientApp\src\App.test.tsx"/>
|
||||
<TypeScriptCompile Remove="ClientApp\src\game\playerStats.tsx"/>
|
||||
<TypeScriptCompile Remove="ClientApp\src\websockets\actions.ts"/>
|
||||
<TypeScriptCompile Remove="ClientApp\src\utils\colours.ts"/>
|
||||
<TypeScriptCompile Remove="ClientApp\src\utils\dom.ts"/>
|
||||
<TypeScriptCompile Remove="ClientApp\src\game\pellet.ts"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="ClientApp\tests\utils\" />
|
||||
<Folder Include="ClientApp\tests\utils\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\DataAccessLayer\DataAccessLayer.csproj" />
|
||||
<ProjectReference Include="..\DataAccessLayer\DataAccessLayer.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
|
||||
<!-- Ensure Node.js is installed -->
|
||||
<Exec Command="node --version" ContinueOnError="true">
|
||||
<Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
|
||||
<Output TaskParameter="ExitCode" PropertyName="ErrorCode"/>
|
||||
</Exec>
|
||||
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
|
||||
<Message Importance="high" Text="Restoring dependencies using 'pnpm'. This may take several minutes..." />
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="pnpm install" />
|
||||
<Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE."/>
|
||||
<Message Importance="high" Text="Restoring dependencies using 'pnpm'. This may take several minutes..."/>
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="pnpm install"/>
|
||||
</Target>
|
||||
|
||||
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
|
||||
<!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="pnpm install" />
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="pnpm build" />
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="pnpm install"/>
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="pnpm build"/>
|
||||
|
||||
<!-- Include the newly-built files in the publish output -->
|
||||
<ItemGroup>
|
||||
<DistFiles Include="$(SpaRoot)build\**" />
|
||||
<DistFiles Include="$(SpaRoot)build\**"/>
|
||||
<ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
|
||||
<RelativePath>wwwroot\%(RecursiveDir)%(FileName)%(Extension)</RelativePath>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
|
Loading…
x
Reference in New Issue
Block a user