From 5f5e9fb716a3eaf0c4017330b8333fe0b1210ff0 Mon Sep 17 00:00:00 2001 From: Vinzenz Schroeter Date: Sun, 5 May 2024 13:51:28 +0200 Subject: [PATCH] add endpoint for requesting map data --- tanks-backend/TanksServer/Endpoints.cs | 21 +++++++- .../TanksServer/GameLogic/MapPrototype.cs | 8 +++ .../TanksServer/GameLogic/MapService.cs | 51 ++++++++++++------- .../GameLogic/SpriteMapPrototype.cs | 13 ++--- .../TanksServer/GameLogic/TextMapPrototype.cs | 14 ++--- .../TanksServer/Graphics/DrawMapStep.cs | 18 ++++++- .../Interactivity/AppSerializerContext.cs | 1 + tanks-backend/TanksServer/Models/MapInfo.cs | 3 ++ 8 files changed, 97 insertions(+), 32 deletions(-) create mode 100644 tanks-backend/TanksServer/GameLogic/MapPrototype.cs create mode 100644 tanks-backend/TanksServer/Models/MapInfo.cs diff --git a/tanks-backend/TanksServer/Endpoints.cs b/tanks-backend/TanksServer/Endpoints.cs index a65b582..0eb0100 100644 --- a/tanks-backend/TanksServer/Endpoints.cs +++ b/tanks-backend/TanksServer/Endpoints.cs @@ -24,14 +24,18 @@ internal sealed class Endpoints( app.Map("/controls", ConnectControlsAsync); app.MapGet("/map", () => mapService.MapNames); app.MapPost("/map", PostMap); + app.MapGet("/map/{name}", GetMapByName); } private Results, NotFound, Ok> PostMap([FromQuery] string name) { if (string.IsNullOrWhiteSpace(name)) return TypedResults.BadRequest("invalid map name"); - if (!mapService.TryGetMapByName(name, out var map)) + + name = name.Trim().ToUpperInvariant(); + if (!mapService.TryGetPrototype(name, out var map)) return TypedResults.NotFound("map with name not found"); + changeToRequestedMap.Request(map); return TypedResults.Ok(); } @@ -88,4 +92,19 @@ internal sealed class Endpoints( var player = playerService.GetOrAdd(name); return TypedResults.Ok(player.Name); } + + private Results, NotFound, BadRequest> GetMapByName(string name) + { + name = name.Trim().ToUpperInvariant(); + if (string.IsNullOrEmpty(name)) + return TypedResults.BadRequest("map name cannot be empty"); + + if (!mapService.TryGetPrototype(name, out var prototype)) + return TypedResults.NotFound(); + if (!mapService.TryGetPreview(name, out var preview)) + return TypedResults.NotFound(); + + var mapInfo = new MapInfo(prototype.Name, prototype.GetType().Name, preview.Data); + return TypedResults.Ok(mapInfo); + } } diff --git a/tanks-backend/TanksServer/GameLogic/MapPrototype.cs b/tanks-backend/TanksServer/GameLogic/MapPrototype.cs new file mode 100644 index 0000000..cff453d --- /dev/null +++ b/tanks-backend/TanksServer/GameLogic/MapPrototype.cs @@ -0,0 +1,8 @@ +namespace TanksServer.GameLogic; + +internal abstract class MapPrototype +{ + public abstract string Name { get; } + + public abstract Map CreateInstance(); +} diff --git a/tanks-backend/TanksServer/GameLogic/MapService.cs b/tanks-backend/TanksServer/GameLogic/MapService.cs index dccf166..b9263f4 100644 --- a/tanks-backend/TanksServer/GameLogic/MapService.cs +++ b/tanks-backend/TanksServer/GameLogic/MapService.cs @@ -1,14 +1,10 @@ using System.Diagnostics.CodeAnalysis; using System.IO; +using DisplayCommands; using TanksServer.Graphics; namespace TanksServer.GameLogic; -internal abstract class MapPrototype -{ - public abstract Map CreateInstance(); -} - internal sealed class MapService { public const ushort TilesPerRow = 44; @@ -17,9 +13,10 @@ internal sealed class MapService public const ushort PixelsPerRow = TilesPerRow * TileSize; public const ushort PixelsPerColumn = TilesPerColumn * TileSize; - private readonly Dictionary _maps = new(); + private readonly Dictionary _mapPrototypes = new(); + private readonly Dictionary _mapPreviews = new(); - public IEnumerable MapNames => _maps.Keys; + public IEnumerable MapNames => _mapPrototypes.Keys; public Map Current { get; private set; } @@ -29,29 +26,49 @@ internal sealed class MapService LoadMapString(file); foreach (var file in Directory.EnumerateFiles("./assets/maps/", "*.png")) LoadMapPng(file); - - var chosenMapIndex = Random.Shared.Next(_maps.Count); - var chosenMapName = _maps.Keys.Skip(chosenMapIndex).First(); - Current = _maps[chosenMapName].CreateInstance(); + Current = GetRandomMap(); } - public bool TryGetMapByName(string name, [MaybeNullWhen(false)] out MapPrototype map) - => _maps.TryGetValue(name, out map); + public bool TryGetPrototype(string name, [MaybeNullWhen(false)] out MapPrototype map) + => _mapPrototypes.TryGetValue(name, out map); public void SwitchTo(MapPrototype prototype) => Current = prototype.CreateInstance(); + public bool TryGetPreview(string name, [MaybeNullWhen(false)] out PixelGrid pixelGrid) + { + if (_mapPreviews.TryGetValue(name, out pixelGrid)) + return true; // already generated + if (!_mapPrototypes.TryGetValue(name, out var prototype)) + return false; // name not found + + pixelGrid = new PixelGrid(PixelsPerRow, PixelsPerColumn); + DrawMapStep.Draw(pixelGrid, prototype.CreateInstance()); + + _mapPreviews.TryAdd(name, pixelGrid); // another thread may have added the map already + return true; // new preview + } + private void LoadMapPng(string file) { - var name = Path.GetFileName(file); + var name = MapNameFromFilePath(file); var prototype = new SpriteMapPrototype(name, Sprite.FromImageFile(file)); - _maps.Add(Path.GetFileName(file), prototype); + _mapPrototypes.Add(name, prototype); } private void LoadMapString(string file) { + var name = MapNameFromFilePath(file); var map = File.ReadAllText(file).ReplaceLineEndings(string.Empty).Trim(); - var name = Path.GetFileName(file); var prototype = new TextMapPrototype(name, map); - _maps.Add(name, prototype); + _mapPrototypes.Add(name, prototype); + } + + private static string MapNameFromFilePath(string filePath) => Path.GetFileName(filePath).ToUpperInvariant(); + + private Map GetRandomMap() + { + var chosenMapIndex = Random.Shared.Next(_mapPrototypes.Count); + var chosenMapName = _mapPrototypes.Keys.Skip(chosenMapIndex).First(); + return _mapPrototypes[chosenMapName].CreateInstance(); } } diff --git a/tanks-backend/TanksServer/GameLogic/SpriteMapPrototype.cs b/tanks-backend/TanksServer/GameLogic/SpriteMapPrototype.cs index 8d383eb..202bf98 100644 --- a/tanks-backend/TanksServer/GameLogic/SpriteMapPrototype.cs +++ b/tanks-backend/TanksServer/GameLogic/SpriteMapPrototype.cs @@ -5,17 +5,18 @@ namespace TanksServer.GameLogic; internal sealed class SpriteMapPrototype : MapPrototype { - private readonly string _name; - private readonly Sprite _sprite; + public override string Name { get; } + + public Sprite Sprite { get; } public SpriteMapPrototype(string name, Sprite sprite) { if (sprite.Width != MapService.PixelsPerRow || sprite.Height != MapService.PixelsPerColumn) - throw new FileLoadException($"invalid image size in file {_name}"); + throw new FileLoadException($"invalid image size in file {Name}"); - _name = name; - _sprite = sprite; + Name = name; + Sprite = sprite; } - public override Map CreateInstance() => new(_name, _sprite.ToBoolArray()); + public override Map CreateInstance() => new(Name, Sprite.ToBoolArray()); } diff --git a/tanks-backend/TanksServer/GameLogic/TextMapPrototype.cs b/tanks-backend/TanksServer/GameLogic/TextMapPrototype.cs index 3195453..21f95d8 100644 --- a/tanks-backend/TanksServer/GameLogic/TextMapPrototype.cs +++ b/tanks-backend/TanksServer/GameLogic/TextMapPrototype.cs @@ -2,17 +2,19 @@ namespace TanksServer.GameLogic; internal sealed class TextMapPrototype : MapPrototype { - private readonly string _name; - private readonly string _text; + public override string Name { get; } + + public string Text { get; } public TextMapPrototype(string name, string text) { if (text.Length != MapService.TilesPerColumn * MapService.TilesPerRow) throw new ArgumentException($"cannot load map {name}: invalid length"); - _name = name; - _text = text; + Name = name; + Text = text; } + public override Map CreateInstance() { var walls = new bool[MapService.PixelsPerRow, MapService.PixelsPerColumn]; @@ -21,7 +23,7 @@ internal sealed class TextMapPrototype : MapPrototype for (ushort tileY = 0; tileY < MapService.TilesPerColumn; tileY++) { var tile = new TilePosition(tileX, tileY); - if (_text[tileX + tileY * MapService.TilesPerRow] != '#') + if (Text[tileX + tileY * MapService.TilesPerRow] != '#') continue; for (byte pixelInTileX = 0; pixelInTileX < MapService.TileSize; pixelInTileX++) @@ -32,6 +34,6 @@ internal sealed class TextMapPrototype : MapPrototype } } - return new Map(_name, walls); + return new Map(Name, walls); } } diff --git a/tanks-backend/TanksServer/Graphics/DrawMapStep.cs b/tanks-backend/TanksServer/Graphics/DrawMapStep.cs index cf23bf0..3be6a37 100644 --- a/tanks-backend/TanksServer/Graphics/DrawMapStep.cs +++ b/tanks-backend/TanksServer/Graphics/DrawMapStep.cs @@ -1,18 +1,32 @@ +using DisplayCommands; using TanksServer.GameLogic; namespace TanksServer.Graphics; internal sealed class DrawMapStep(MapService map) : IDrawStep { - public void Draw(GamePixelGrid pixels) + public void Draw(GamePixelGrid pixels) => Draw(pixels, map.Current); + + private static void Draw(GamePixelGrid pixels, Map map) { for (ushort y = 0; y < MapService.PixelsPerColumn; y++) for (ushort x = 0; x < MapService.PixelsPerRow; x++) { - if (!map.Current.IsWall(x, y)) + if (!map.IsWall(x, y)) continue; pixels[x, y].EntityType = GamePixelEntityType.Wall; } } + + public static void Draw(PixelGrid pixels, Map map) + { + for (ushort y = 0; y < MapService.PixelsPerColumn; y++) + for (ushort x = 0; x < MapService.PixelsPerRow; x++) + { + if (!map.IsWall(x, y)) + continue; + pixels[x, y] = true; + } + } } diff --git a/tanks-backend/TanksServer/Interactivity/AppSerializerContext.cs b/tanks-backend/TanksServer/Interactivity/AppSerializerContext.cs index 004c313..e9560d5 100644 --- a/tanks-backend/TanksServer/Interactivity/AppSerializerContext.cs +++ b/tanks-backend/TanksServer/Interactivity/AppSerializerContext.cs @@ -7,5 +7,6 @@ namespace TanksServer.Interactivity; [JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(PlayerInfo))] +[JsonSerializable(typeof(MapInfo))] [JsonSourceGenerationOptions(JsonSerializerDefaults.Web)] internal sealed partial class AppSerializerContext : JsonSerializerContext; diff --git a/tanks-backend/TanksServer/Models/MapInfo.cs b/tanks-backend/TanksServer/Models/MapInfo.cs new file mode 100644 index 0000000..637f8bb --- /dev/null +++ b/tanks-backend/TanksServer/Models/MapInfo.cs @@ -0,0 +1,3 @@ +namespace TanksServer.Models; + +public record MapInfo(string Name, string TypeName, Memory Preview);