feat: add backend for multiplayer support
This commit is contained in:
56
backend/.gitignore
vendored
Normal file
56
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
## Build Folders
|
||||
bin/
|
||||
obj/
|
||||
out/
|
||||
|
||||
## User-specific files
|
||||
*.user
|
||||
*.userosscache
|
||||
*.suo
|
||||
*.sln.docstates
|
||||
|
||||
## Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
## Visual Studio Code
|
||||
.vscode/
|
||||
|
||||
## ASP.NET
|
||||
wwwroot/
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
.env.*
|
||||
|
||||
## Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
## Publish output
|
||||
publish/
|
||||
*.db
|
||||
|
||||
## Test Results
|
||||
TestResults/
|
||||
*.trx
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
## NuGet
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
.nuget/
|
||||
packages/
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
## Temporary files
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
|
||||
## OS Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
193
backend/Program.cs
Normal file
193
backend/Program.cs
Normal file
@@ -0,0 +1,193 @@
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var app = builder.Build();
|
||||
|
||||
var games = new ConcurrentDictionary<string, Game>();
|
||||
|
||||
app.UseWebSockets();
|
||||
|
||||
app.Map("/", async context =>
|
||||
{
|
||||
if (!context.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
context.Response.StatusCode = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
var ws = await context.WebSockets.AcceptWebSocketAsync();
|
||||
var remoteAddress = context.Connection.RemoteIpAddress?.ToString();
|
||||
|
||||
var player = new Player(ws, remoteAddress);
|
||||
|
||||
try
|
||||
{
|
||||
var buffer = new byte[4096];
|
||||
var messageBuffer = new ArraySegment<byte>(buffer);
|
||||
while (ws.State == WebSocketState.Open)
|
||||
{
|
||||
var message = new MemoryStream();
|
||||
WebSocketReceiveResult result;
|
||||
|
||||
do
|
||||
{
|
||||
result = await ws.ReceiveAsync(messageBuffer, CancellationToken.None);
|
||||
message.Write(buffer, 0, result.Count);
|
||||
}
|
||||
while (!result.EndOfMessage);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
break;
|
||||
|
||||
var msgText = Encoding.UTF8.GetString(message.ToArray());
|
||||
|
||||
if (msgText == "ping")
|
||||
{
|
||||
await player.SendTextAsync("pong");
|
||||
continue;
|
||||
}
|
||||
|
||||
var json = JsonDocument.Parse(msgText).RootElement;
|
||||
var action = json.GetProperty("action").GetString();
|
||||
var gameId = json.GetProperty("gameId").GetString();
|
||||
var clientId = json.GetProperty("clientId").GetString();
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case "enter_game":
|
||||
await GameHandlers.EnterGame(gameId, clientId, player, games);
|
||||
break;
|
||||
case "send_piece":
|
||||
await GameHandlers.SendToOpponent(gameId, player, "receive_piece", json.GetProperty("piece"), games);
|
||||
break;
|
||||
case "send_stack":
|
||||
await GameHandlers.SendToOpponent(gameId, player, "receive_stack", json.GetProperty("stack"), games);
|
||||
break;
|
||||
case "send_stats":
|
||||
await GameHandlers.SendToOpponent(gameId, player, "receive_stats", json.GetProperty("stats"), games);
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine("Unsupported action.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Error: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.WriteLine("Disconnecting player...");
|
||||
await GameHandlers.ExitGame(player, games);
|
||||
Console.WriteLine("Connection closed.");
|
||||
}
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
// -------------------------
|
||||
// Models
|
||||
// -------------------------
|
||||
|
||||
record Player(WebSocket Socket, string? RemoteAddress)
|
||||
{
|
||||
public async Task SendTextAsync(string message)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(message);
|
||||
await Socket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
|
||||
public async Task SendJsonAsync(object obj)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(obj);
|
||||
await SendTextAsync(json);
|
||||
}
|
||||
}
|
||||
|
||||
class Game
|
||||
{
|
||||
public string GameId { get; }
|
||||
public List<Player> Players { get; } = new();
|
||||
public List<string> Clients { get; } = new();
|
||||
|
||||
public Game(string gameId)
|
||||
{
|
||||
GameId = gameId;
|
||||
}
|
||||
|
||||
public Player? GetOpponent(Player player) => Players.FirstOrDefault(p => p != player);
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// Handlers
|
||||
// -------------------------
|
||||
|
||||
static class GameHandlers
|
||||
{
|
||||
public static async Task EnterGame(string gameId, string clientId, Player player, ConcurrentDictionary<string, Game> games)
|
||||
{
|
||||
var game = games.GetOrAdd(gameId, _ => new Game(gameId));
|
||||
|
||||
if (!game.Clients.Contains(clientId))
|
||||
{
|
||||
game.Clients.Add(clientId);
|
||||
game.Players.Add(player);
|
||||
|
||||
if (game.Players.Count == 1)
|
||||
{
|
||||
await player.SendJsonAsync(new { type = "wait_for_opponent" });
|
||||
}
|
||||
else if (game.Players.Count == 2)
|
||||
{
|
||||
foreach (var p in game.Players)
|
||||
await p.SendJsonAsync(new { type = "start_game" });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Already in game...");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SendToOpponent(string gameId, Player sender, string type, JsonElement content, ConcurrentDictionary<string, Game> games)
|
||||
{
|
||||
if (!games.TryGetValue(gameId, out var game))
|
||||
return;
|
||||
|
||||
var opponent = game.GetOpponent(sender);
|
||||
if (opponent != null)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case "receive_piece":
|
||||
await opponent.SendJsonAsync(new { type, piece = content });
|
||||
break;
|
||||
case "receive_stack":
|
||||
await opponent.SendJsonAsync(new { type, stack = content });
|
||||
break;
|
||||
case "receive_stats":
|
||||
await opponent.SendJsonAsync(new { type, stats = content });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task ExitGame(Player disconnectingPlayer, ConcurrentDictionary<string, Game> games)
|
||||
{
|
||||
var game = games.Values.FirstOrDefault(g => g.Players.Contains(disconnectingPlayer));
|
||||
if (game != null)
|
||||
{
|
||||
games.TryRemove(game.GameId, out _);
|
||||
|
||||
var opponent = game.GetOpponent(disconnectingPlayer);
|
||||
if (opponent != null)
|
||||
{
|
||||
await opponent.SendJsonAsync(new { type = "exit_game" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
backend/Properties/launchSettings.json
Normal file
23
backend/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5085",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7145;http://localhost:5085",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
backend/appsettings.Development.json
Normal file
8
backend/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
backend/appsettings.json
Normal file
9
backend/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
9
backend/backend.csproj
Normal file
9
backend/backend.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
0
.gitignore → frontend/.gitignore
vendored
0
.gitignore → frontend/.gitignore
vendored
@@ -97,5 +97,5 @@ color:
|
||||
piece-ghost: "#9BFCF0"
|
||||
|
||||
online:
|
||||
server-url: "ws://webapi.tetri5.com"
|
||||
server-url: "ws://localhost:5085"
|
||||
#server-url: "ws://localhost:5001"
|
||||
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
@@ -30,8 +30,7 @@ class MultiplayerService():
|
||||
|
||||
@classmethod
|
||||
def init(cls) -> None:
|
||||
thread = Thread(target=asyncio.get_event_loop().run_until_complete,\
|
||||
args=(_NetworkConnectionService.init(),))
|
||||
thread = Thread(target=asyncio.run, args=(_NetworkConnectionService.init(),))
|
||||
thread.start()
|
||||
|
||||
""" SEND """
|
||||
Reference in New Issue
Block a user