diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 890fe59..f619f3e 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,21 +16,21 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 7.0.x - + dotnet-version: 10.0.x + - name: Check Tag id: check-tag run: | if [[ v${{ github.event.ref }} =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo ::set-output name=match::true + echo "match=true" >> $GITHUB_OUTPUT fi - + - name: Run Unit Tests run: | dotnet restore dotnet build dotnet test test/NosCore.PathFinder.Tests -v m - + - name: Build Artifact if: steps.check-tag.outputs.match == 'true' id: build_artifact @@ -39,23 +39,23 @@ jobs: dotnet build -c Release dotnet pack -c Release -o /tmp/nupkgs -v m -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg dotnet nuget push /tmp/nupkgs/NosCore.PathFinder.${{github.event.ref}}.nupkg -s https://api.nuget.org/v3/index.json -k ${{secrets.NUGET_API_KEY}} - echo ::set-output name=ARTIFACT_PATH::/tmp/nupkgs/NosCore.PathFinder.${{github.event.ref}}.nupkg - echo ::set-output name=ARTIFACT_NAME::NosCore.PathFinder.${{github.event.ref}}.nupkg - + echo "ARTIFACT_PATH=/tmp/nupkgs/NosCore.PathFinder.${{github.event.ref}}.nupkg" >> $GITHUB_OUTPUT + echo "ARTIFACT_NAME=NosCore.PathFinder.${{github.event.ref}}.nupkg" >> $GITHUB_OUTPUT + - name: Gets Latest Release if: steps.check-tag.outputs.match == 'true' id: latest_release_info uses: jossef/action-latest-release-info@v1.1.0 env: GITHUB_TOKEN: ${{ secrets.REPO_TOKEN }} - + - name: Upload Release Asset if: steps.check-tag.outputs.match == 'true' uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.REPO_TOKEN }} with: - upload_url: ${{ steps.latest_release_info.outputs.upload_url }} + upload_url: ${{ steps.latest_release_info.outputs.upload_url }} asset_path: ${{ steps.build_artifact.outputs.ARTIFACT_PATH }} asset_name: ${{ steps.build_artifact.outputs.ARTIFACT_NAME }} asset_content_type: application/zip diff --git a/NosCore.PathFinder.sln b/NosCore.PathFinder.sln index e181676..51774cc 100644 --- a/NosCore.PathFinder.sln +++ b/NosCore.PathFinder.sln @@ -1,38 +1,60 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28527.54 +# Visual Studio Version 18 +VisualStudioVersion = 18.2.11408.102 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NosCore.PathFinder", "src\NosCore.PathFinder\NosCore.PathFinder.csproj", "{8DC7839A-EB02-4E71-95DC-2D2BD9651E19}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NosCore.PathFinder.Tests", "test\NosCore.PathFinder.Tests\NosCore.PathFinder.Tests.csproj", "{BEEC67B8-A63B-43DE-87A5-73341AEFD62F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NosCore.PathFinder.Gui", "src\NosCore.PathFinder.Gui\NosCore.PathFinder.Gui.csproj", "{FA7E228B-399D-4E03-994C-3C3D057E134E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosCore.PathFinder.Benchmark", "test\NosCore.PathFinder.Benchmark\NosCore.PathFinder.Benchmark\NosCore.PathFinder.Benchmark.csproj", "{159A99CE-A97C-4215-8FA8-38BC7C8D382D}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosCore.PathFinder.Api", "src\NosCore.PathFinder.Api\NosCore.PathFinder.Api.csproj", "{6642B6A1-6EBE-4E38-A812-435286A0E04E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {8DC7839A-EB02-4E71-95DC-2D2BD9651E19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8DC7839A-EB02-4E71-95DC-2D2BD9651E19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8DC7839A-EB02-4E71-95DC-2D2BD9651E19}.Debug|x64.ActiveCfg = Debug|Any CPU + {8DC7839A-EB02-4E71-95DC-2D2BD9651E19}.Debug|x64.Build.0 = Debug|Any CPU + {8DC7839A-EB02-4E71-95DC-2D2BD9651E19}.Debug|x86.ActiveCfg = Debug|Any CPU + {8DC7839A-EB02-4E71-95DC-2D2BD9651E19}.Debug|x86.Build.0 = Debug|Any CPU {8DC7839A-EB02-4E71-95DC-2D2BD9651E19}.Release|Any CPU.ActiveCfg = Release|Any CPU {8DC7839A-EB02-4E71-95DC-2D2BD9651E19}.Release|Any CPU.Build.0 = Release|Any CPU + {8DC7839A-EB02-4E71-95DC-2D2BD9651E19}.Release|x64.ActiveCfg = Release|Any CPU + {8DC7839A-EB02-4E71-95DC-2D2BD9651E19}.Release|x64.Build.0 = Release|Any CPU + {8DC7839A-EB02-4E71-95DC-2D2BD9651E19}.Release|x86.ActiveCfg = Release|Any CPU + {8DC7839A-EB02-4E71-95DC-2D2BD9651E19}.Release|x86.Build.0 = Release|Any CPU {BEEC67B8-A63B-43DE-87A5-73341AEFD62F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BEEC67B8-A63B-43DE-87A5-73341AEFD62F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BEEC67B8-A63B-43DE-87A5-73341AEFD62F}.Debug|x64.ActiveCfg = Debug|Any CPU + {BEEC67B8-A63B-43DE-87A5-73341AEFD62F}.Debug|x64.Build.0 = Debug|Any CPU + {BEEC67B8-A63B-43DE-87A5-73341AEFD62F}.Debug|x86.ActiveCfg = Debug|Any CPU + {BEEC67B8-A63B-43DE-87A5-73341AEFD62F}.Debug|x86.Build.0 = Debug|Any CPU {BEEC67B8-A63B-43DE-87A5-73341AEFD62F}.Release|Any CPU.ActiveCfg = Release|Any CPU {BEEC67B8-A63B-43DE-87A5-73341AEFD62F}.Release|Any CPU.Build.0 = Release|Any CPU - {FA7E228B-399D-4E03-994C-3C3D057E134E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FA7E228B-399D-4E03-994C-3C3D057E134E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FA7E228B-399D-4E03-994C-3C3D057E134E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FA7E228B-399D-4E03-994C-3C3D057E134E}.Release|Any CPU.Build.0 = Release|Any CPU - {159A99CE-A97C-4215-8FA8-38BC7C8D382D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {159A99CE-A97C-4215-8FA8-38BC7C8D382D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {159A99CE-A97C-4215-8FA8-38BC7C8D382D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {159A99CE-A97C-4215-8FA8-38BC7C8D382D}.Release|Any CPU.Build.0 = Release|Any CPU + {BEEC67B8-A63B-43DE-87A5-73341AEFD62F}.Release|x64.ActiveCfg = Release|Any CPU + {BEEC67B8-A63B-43DE-87A5-73341AEFD62F}.Release|x64.Build.0 = Release|Any CPU + {BEEC67B8-A63B-43DE-87A5-73341AEFD62F}.Release|x86.ActiveCfg = Release|Any CPU + {BEEC67B8-A63B-43DE-87A5-73341AEFD62F}.Release|x86.Build.0 = Release|Any CPU + {6642B6A1-6EBE-4E38-A812-435286A0E04E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6642B6A1-6EBE-4E38-A812-435286A0E04E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6642B6A1-6EBE-4E38-A812-435286A0E04E}.Debug|x64.ActiveCfg = Debug|Any CPU + {6642B6A1-6EBE-4E38-A812-435286A0E04E}.Debug|x64.Build.0 = Debug|Any CPU + {6642B6A1-6EBE-4E38-A812-435286A0E04E}.Debug|x86.ActiveCfg = Debug|Any CPU + {6642B6A1-6EBE-4E38-A812-435286A0E04E}.Debug|x86.Build.0 = Debug|Any CPU + {6642B6A1-6EBE-4E38-A812-435286A0E04E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6642B6A1-6EBE-4E38-A812-435286A0E04E}.Release|Any CPU.Build.0 = Release|Any CPU + {6642B6A1-6EBE-4E38-A812-435286A0E04E}.Release|x64.ActiveCfg = Release|Any CPU + {6642B6A1-6EBE-4E38-A812-435286A0E04E}.Release|x64.Build.0 = Release|Any CPU + {6642B6A1-6EBE-4E38-A812-435286A0E04E}.Release|x86.ActiveCfg = Release|Any CPU + {6642B6A1-6EBE-4E38-A812-435286A0E04E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/NosCore.PathFinder.sln.DotSettings b/NosCore.PathFinder.sln.DotSettings index 53bb462..6e70b1b 100644 --- a/NosCore.PathFinder.sln.DotSettings +++ b/NosCore.PathFinder.sln.DotSettings @@ -8,4 +8,5 @@ ----------------------------------- True False - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/configuration/pathfinder.yml b/configuration/pathfinder.yml index f6e8edb..47e592e 100644 --- a/configuration/pathfinder.yml +++ b/configuration/pathfinder.yml @@ -2,6 +2,6 @@ Language: en Database: Host: localhost Port: 5432 - Database: postgres + Database: noscore Username: postgres Password: password diff --git a/documentation/BrushFireTests.Test_BrushFire.approved.md b/documentation/BrushFireTests.Test_BrushFire.approved.md index 9d0b862..8e12a52 100644 --- a/documentation/BrushFireTests.Test_BrushFire.approved.md +++ b/documentation/BrushFireTests.Test_BrushFire.approved.md @@ -1,5 +1,5 @@ -# NosCore.Pathfinder's Documentation +# NosCore.Pathfinder's Documentation ## Brushfire - Filename: brushfire.png -- Checksum: 7dcc6fcc2533ffe40798dcd1771c3c1d28d10fdf10bed8a4b2cad8ef8bc4009b +- Checksum: 29051db500b2fac5499053f08f66418ad3d6d43efb4af774d07e5ef6e0c9abaf ![brushfire](./brushfire.png) diff --git a/documentation/FlowFieldTests.Test_FlowField.approved.md b/documentation/FlowFieldTests.Test_FlowField.approved.md new file mode 100644 index 0000000..308b784 --- /dev/null +++ b/documentation/FlowFieldTests.Test_FlowField.approved.md @@ -0,0 +1,5 @@ +# NosCore.Pathfinder's Documentation +## Flow Field (Vector Field Pathfinding) +- Filename: flow-field.png +- Checksum: 29051db500b2fac5499053f08f66418ad3d6d43efb4af774d07e5ef6e0c9abaf +![brushfire](./flow-field.png) diff --git a/documentation/FlowFieldTests.Test_FlowField_MonsterPath.approved.md b/documentation/FlowFieldTests.Test_FlowField_MonsterPath.approved.md new file mode 100644 index 0000000..c3a7144 --- /dev/null +++ b/documentation/FlowFieldTests.Test_FlowField_MonsterPath.approved.md @@ -0,0 +1,5 @@ +# NosCore.Pathfinder's Documentation +## Flow Field Path (Monster following vectors to Player) +- Filename: flow-field-path.png +- Checksum: c7697a4c6a7405866a41bab1bc4cb58ad5fa501be81ab3ecf80e72d4d78788ff +![brushfire](./flow-field-path.png) diff --git a/documentation/GoalBasedPathfinderTests.Test_GoalBasedPathfinder.approved.md b/documentation/GoalBasedPathfinderTests.Test_GoalBasedPathfinder.approved.md index 6288f47..c689cf6 100644 --- a/documentation/GoalBasedPathfinderTests.Test_GoalBasedPathfinder.approved.md +++ b/documentation/GoalBasedPathfinderTests.Test_GoalBasedPathfinder.approved.md @@ -1,5 +1,5 @@ # NosCore.Pathfinder's Documentation ## Goal Based Pathfinder - Filename: goal-based-pathfinder.png -- Checksum: 508c4ed1101bab0cc5f36e6e785a69b6e7892a9a511ebe23b338e4edf1cebc6a +- Checksum: c7697a4c6a7405866a41bab1bc4cb58ad5fa501be81ab3ecf80e72d4d78788ff ![brushfire](./goal-based-pathfinder.png) diff --git a/documentation/JumpPointSearchPathfinderTests.Test_JumpPointSearchPathfinder.approved.md b/documentation/JumpPointSearchPathfinderTests.Test_JumpPointSearchPathfinder.approved.md index d48274c..a5a9f1c 100644 --- a/documentation/JumpPointSearchPathfinderTests.Test_JumpPointSearchPathfinder.approved.md +++ b/documentation/JumpPointSearchPathfinderTests.Test_JumpPointSearchPathfinder.approved.md @@ -1,5 +1,5 @@ # NosCore.Pathfinder's Documentation ## Jump Point Search Pathfinder (break at walls) - Filename: jump-point-search-pathfinder.png -- Checksum: 0944988268fb7f1f6601bf1516aaccaca25a43bb583c5a92b1da3d6cd4f8ccc7 +- Checksum: c255a02a91718ee50823cc1f4070d782e7a06f9e79cabcd1aaad780882b4c165 ![brushfire](./jump-point-search-pathfinder.png) diff --git a/documentation/brushfire.png b/documentation/brushfire.png index 5eabab5..540b68d 100644 Binary files a/documentation/brushfire.png and b/documentation/brushfire.png differ diff --git a/documentation/flow-field-path.png b/documentation/flow-field-path.png new file mode 100644 index 0000000..2b74368 Binary files /dev/null and b/documentation/flow-field-path.png differ diff --git a/documentation/flow-field.png b/documentation/flow-field.png new file mode 100644 index 0000000..38840d3 Binary files /dev/null and b/documentation/flow-field.png differ diff --git a/documentation/goal-based-pathfinder.png b/documentation/goal-based-pathfinder.png index 9a93149..c636a01 100644 Binary files a/documentation/goal-based-pathfinder.png and b/documentation/goal-based-pathfinder.png differ diff --git a/documentation/jump-point-search-pathfinder.png b/documentation/jump-point-search-pathfinder.png index 1474272..5b5be09 100644 Binary files a/documentation/jump-point-search-pathfinder.png and b/documentation/jump-point-search-pathfinder.png differ diff --git a/src/NosCore.PathFinder.Api/Database/Map.cs b/src/NosCore.PathFinder.Api/Database/Map.cs new file mode 100644 index 0000000..4ac8cc1 --- /dev/null +++ b/src/NosCore.PathFinder.Api/Database/Map.cs @@ -0,0 +1,23 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace NosCore.PathFinder.Api.Database; + +public class Map +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public short MapId { get; set; } + + public byte[] Data { get; set; } = null!; + + public virtual ICollection MapMonster { get; set; } = new HashSet(); + + public virtual ICollection MapNpc { get; set; } = new HashSet(); +} diff --git a/src/NosCore.PathFinder.Api/Database/MapMonster.cs b/src/NosCore.PathFinder.Api/Database/MapMonster.cs new file mode 100644 index 0000000..796ea70 --- /dev/null +++ b/src/NosCore.PathFinder.Api/Database/MapMonster.cs @@ -0,0 +1,27 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace NosCore.PathFinder.Api.Database; + +public class MapMonster +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int MapMonsterId { get; set; } + + public short MapId { get; set; } + + public short MapX { get; set; } + + public short MapY { get; set; } + + public short VNum { get; set; } + + public virtual Map Map { get; set; } = null!; +} diff --git a/src/NosCore.PathFinder.Api/Database/MapNpc.cs b/src/NosCore.PathFinder.Api/Database/MapNpc.cs new file mode 100644 index 0000000..6598de7 --- /dev/null +++ b/src/NosCore.PathFinder.Api/Database/MapNpc.cs @@ -0,0 +1,27 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace NosCore.PathFinder.Api.Database; + +public class MapNpc +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int MapNpcId { get; set; } + + public short MapId { get; set; } + + public short MapX { get; set; } + + public short MapY { get; set; } + + public short VNum { get; set; } + + public virtual Map Map { get; set; } = null!; +} diff --git a/src/NosCore.PathFinder.Api/Database/PathFinderContext.cs b/src/NosCore.PathFinder.Api/Database/PathFinderContext.cs new file mode 100644 index 0000000..307d065 --- /dev/null +++ b/src/NosCore.PathFinder.Api/Database/PathFinderContext.cs @@ -0,0 +1,39 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using Microsoft.EntityFrameworkCore; + +namespace NosCore.PathFinder.Api.Database; + +public class PathFinderContext : DbContext +{ + public PathFinderContext(DbContextOptions options) : base(options) + { + } + + public virtual DbSet Map { get; set; } = null!; + public virtual DbSet MapMonster { get; set; } = null!; + public virtual DbSet MapNpc { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToTable("Map"); + modelBuilder.Entity().ToTable("MapMonster"); + modelBuilder.Entity().ToTable("MapNpc"); + + modelBuilder.Entity() + .HasMany(e => e.MapMonster) + .WithOne(e => e.Map) + .HasForeignKey(e => e.MapId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasMany(e => e.MapNpc) + .WithOne(e => e.Map) + .HasForeignKey(e => e.MapId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/src/NosCore.PathFinder.Api/LogLanguageKey.cs b/src/NosCore.PathFinder.Api/LogLanguageKey.cs new file mode 100644 index 0000000..cf425fc --- /dev/null +++ b/src/NosCore.PathFinder.Api/LogLanguageKey.cs @@ -0,0 +1,29 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using System.Diagnostics.CodeAnalysis; + +namespace NosCore.PathFinder.Api; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public enum LogLanguageKey +{ + DATABASE_CONFIGURED, + NO_DATABASE_CONFIGURED, + BASE_DIRECTORY, + CURRENT_DIRECTORY, + SEARCHING_FOR_CONFIG, + CHECKING_PATH, + LOADED_DATABASE_CONFIG, + FAILED_TO_PARSE_CONFIG, + USING_CONNECTION_STRING_FROM_APPSETTINGS, + NO_DATABASE_CONFIGURATION_FOUND, + LOADED_MAPS_FROM_DATABASE, + FAILED_TO_LOAD_MAP, + FAILED_TO_CONNECT_TO_DATABASE, + API_STARTED, + LANGUAGE_LOADED +} diff --git a/src/NosCore.PathFinder.Api/MapStore.cs b/src/NosCore.PathFinder.Api/MapStore.cs new file mode 100644 index 0000000..c7cd4d9 --- /dev/null +++ b/src/NosCore.PathFinder.Api/MapStore.cs @@ -0,0 +1,276 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using Microsoft.EntityFrameworkCore; +using NosCore.PathFinder.Api.Database; +using NosCore.PathFinder.Interfaces; + +namespace NosCore.PathFinder.Api; + +public class MapStore +{ + private readonly Dictionary _sampleMaps = new(); + private readonly Dictionary _databaseMaps = new(); + private readonly Dictionary> _monsters = new(); + private readonly Dictionary> _npcs = new(); + private readonly IServiceProvider? _serviceProvider; + private readonly ILogger? _logger; + private bool _databaseLoaded; + + public MapStore() + { + InitializeSampleMaps(); + } + + public MapStore(IServiceProvider serviceProvider, ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + InitializeSampleMaps(); + } + + private void InitializeSampleMaps() + { + _sampleMaps[-1] = new SimpleMap(new byte[][] + { + new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1}, + new byte[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} + }); + + _sampleMaps[-2] = new SimpleMap(GenerateMaze(51, 51)); + } + + public async Task LoadFromDatabaseAsync() + { + if (_serviceProvider == null || _databaseLoaded) return; + + try + { + using var scope = _serviceProvider.CreateScope(); + var context = scope.ServiceProvider.GetService(); + if (context == null) return; + + var maps = await context.Map.ToListAsync(); + foreach (var map in maps) + { + try + { + var dbMap = new DatabaseMap(map.Data); + if (dbMap.Width > 0 && dbMap.Height > 0) + { + _databaseMaps[map.MapId] = dbMap; + } + } + catch (Exception ex) + { + _logger?.LogWarning(ex, "Failed to load map {MapId}", map.MapId); + } + } + + var monsters = await context.MapMonster.ToListAsync(); + foreach (var monster in monsters) + { + if (!_monsters.ContainsKey(monster.MapId)) + _monsters[monster.MapId] = new List(); + _monsters[monster.MapId].Add(new EntityPosition(monster.MapMonsterId, monster.MapX, monster.MapY, monster.VNum)); + } + + var npcs = await context.MapNpc.ToListAsync(); + foreach (var npc in npcs) + { + if (!_npcs.ContainsKey(npc.MapId)) + _npcs[npc.MapId] = new List(); + _npcs[npc.MapId].Add(new EntityPosition(npc.MapNpcId, npc.MapX, npc.MapY, npc.VNum)); + } + + _databaseLoaded = true; + _logger?.LogInformation("Loaded {MapCount} maps, {MonsterCount} monsters, {NpcCount} NPCs from database", + _databaseMaps.Count, monsters.Count, npcs.Count); + } + catch (Exception ex) + { + _logger?.LogError(ex, "Failed to connect to database - using sample maps only"); + } + } + + public IMapGrid? GetMap(int mapId) + { + if (_databaseMaps.TryGetValue(mapId, out var dbMap)) + return dbMap; + return _sampleMaps.GetValueOrDefault(mapId); + } + + public List GetMonsters(int mapId) => _monsters.GetValueOrDefault(mapId) ?? new List(); + + public List GetNpcs(int mapId) => _npcs.GetValueOrDefault(mapId) ?? new List(); + + public IEnumerable GetMapList() + { + var result = new List(); + + foreach (var (id, map) in _databaseMaps) + { + result.Add(new { Id = id, map.Width, map.Height, Source = "database" }); + } + + foreach (var (id, map) in _sampleMaps) + { + result.Add(new { Id = id, map.Width, map.Height, Source = "sample" }); + } + + return result.OrderBy(m => ((dynamic)m).Id); + } + + public bool HasDatabaseMaps => _databaseMaps.Count > 0; + + private static byte[][] GenerateMaze(int width, int height) + { + var maze = new byte[height][]; + var random = new Random(42); + + for (var y = 0; y < height; y++) + { + maze[y] = new byte[width]; + for (var x = 0; x < width; x++) + { + if (x == 0 || y == 0 || x == width - 1 || y == height - 1) + { + maze[y][x] = 1; + } + else if (x % 4 == 0 && y % 2 == 0 && random.NextDouble() > 0.3) + { + maze[y][x] = 1; + } + else if (y % 4 == 0 && x % 2 == 0 && random.NextDouble() > 0.3) + { + maze[y][x] = 1; + } + else + { + maze[y][x] = 0; + } + } + } + + return maze; + } +} + +public record EntityPosition(int Id, short X, short Y, short VNum); + +public class SimpleMap : IMapGrid +{ + private readonly byte[][] _data; + private byte[]? _walkabilityGrid; + + public SimpleMap(byte[][] data) + { + _data = data; + Height = (short)data.Length; + Width = (short)data[0].Length; + } + + public short Width { get; } + public short Height { get; } + + public byte this[short x, short y] => _data[y][x]; + + public bool IsWalkable(short x, short y) + { + if (x < 0 || x >= Width || y < 0 || y >= Height) return false; + return _data[y][x] == 0; + } + + public byte[] GetWalkabilityGrid() + { + if (_walkabilityGrid != null) return _walkabilityGrid; + + var size = Width * Height; + var bytesNeeded = (size + 7) / 8; + _walkabilityGrid = new byte[bytesNeeded]; + + for (var y = 0; y < Height; y++) + { + for (var x = 0; x < Width; x++) + { + var idx = y * Width + x; + if (!IsWalkable((short)x, (short)y)) + { + _walkabilityGrid[idx / 8] |= (byte)(1 << (idx % 8)); + } + } + } + + return _walkabilityGrid; + } +} + +public class DatabaseMap : IMapGrid +{ + private readonly byte[] _data; + private readonly short _width; + private readonly short _height; + private byte[]? _walkabilityGrid; + + public DatabaseMap(byte[] data) + { + _data = data; + _width = BitConverter.ToInt16(data, 0); + _height = BitConverter.ToInt16(data, 2); + } + + public short Width => _width; + public short Height => _height; + + public byte this[short x, short y] => _data[4 + y * _width + x]; + + public bool IsWalkable(short x, short y) + { + if (x < 0 || x >= Width || y < 0 || y >= Height) return false; + var value = this[x, y]; + return value == 0 || value == 2 || (value >= 16 && value <= 19); + } + + public byte[] GetWalkabilityGrid() + { + if (_walkabilityGrid != null) return _walkabilityGrid; + + var size = _width * _height; + var bytesNeeded = (size + 7) / 8; + _walkabilityGrid = new byte[bytesNeeded]; + + for (var y = 0; y < _height; y++) + { + for (var x = 0; x < _width; x++) + { + var idx = y * _width + x; + if (!IsWalkable((short)x, (short)y)) + { + _walkabilityGrid[idx / 8] |= (byte)(1 << (idx % 8)); + } + } + } + + return _walkabilityGrid; + } +} diff --git a/src/NosCore.PathFinder.Api/NosCore.PathFinder.Api.csproj b/src/NosCore.PathFinder.Api/NosCore.PathFinder.Api.csproj new file mode 100644 index 0000000..f8d0b63 --- /dev/null +++ b/src/NosCore.PathFinder.Api/NosCore.PathFinder.Api.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + enable + enable + latest + + + + ../../build + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/src/NosCore.PathFinder.Api/PerformanceTracker.cs b/src/NosCore.PathFinder.Api/PerformanceTracker.cs new file mode 100644 index 0000000..7c5ad45 --- /dev/null +++ b/src/NosCore.PathFinder.Api/PerformanceTracker.cs @@ -0,0 +1,110 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using System.Collections.Concurrent; +using System.Diagnostics; + +namespace NosCore.PathFinder.Api; + +public class PerformanceTracker +{ + private readonly ConcurrentQueue _brushfireEntries = new(); + private readonly ConcurrentQueue _flowfieldEntries = new(); + private const int MaxEntries = 1000; + + private readonly Process _process = Process.GetCurrentProcess(); + private TimeSpan _lastCpuTime = TimeSpan.Zero; + private DateTime _lastCpuCheck = DateTime.UtcNow; + private double _cpuUsage = 0; + + public void RecordBrushfire(TimeSpan elapsed, int cellCount) + { + _brushfireEntries.Enqueue(new PerformanceEntry(DateTime.UtcNow, elapsed, cellCount)); + TrimQueue(_brushfireEntries); + } + + public void RecordFlowField(TimeSpan elapsed, int vectorCount) + { + _flowfieldEntries.Enqueue(new PerformanceEntry(DateTime.UtcNow, elapsed, vectorCount)); + TrimQueue(_flowfieldEntries); + } + + public object GetStats() + { + var brushfireStats = CalculateStats(_brushfireEntries.ToArray()); + var flowfieldStats = CalculateStats(_flowfieldEntries.ToArray()); + UpdateCpuUsage(); + + _process.Refresh(); + var memoryMb = _process.WorkingSet64 / 1024.0 / 1024.0; + + return new + { + Brushfire = brushfireStats, + FlowField = flowfieldStats, + TotalRequests = _brushfireEntries.Count + _flowfieldEntries.Count, + CpuPercent = Math.Round(_cpuUsage, 1), + MemoryMb = Math.Round(memoryMb, 1) + }; + } + + private void UpdateCpuUsage() + { + var now = DateTime.UtcNow; + var elapsed = now - _lastCpuCheck; + if (elapsed.TotalMilliseconds < 500) return; + + _process.Refresh(); + var cpuTime = _process.TotalProcessorTime; + var cpuDelta = cpuTime - _lastCpuTime; + _cpuUsage = cpuDelta.TotalMilliseconds / elapsed.TotalMilliseconds / Environment.ProcessorCount * 100; + + _lastCpuTime = cpuTime; + _lastCpuCheck = now; + } + + private static object CalculateStats(PerformanceEntry[] entries) + { + if (entries.Length == 0) + { + return new { Count = 0, AvgMs = 0.0, MinMs = 0.0, MaxMs = 0.0, AvgCells = 0.0, CellsPerSecond = 0.0 }; + } + + var times = entries.Select(e => e.Elapsed.TotalMilliseconds).ToArray(); + var cells = entries.Select(e => e.CellCount).ToArray(); + var avgMs = times.Average(); + var avgCells = cells.Average(); + + return new + { + Count = entries.Length, + AvgMs = Math.Round(avgMs, 3), + MinMs = Math.Round(times.Min(), 3), + MaxMs = Math.Round(times.Max(), 3), + P95Ms = Math.Round(Percentile(times, 95), 3), + AvgCells = Math.Round(avgCells, 1), + CellsPerSecond = avgMs > 0 ? Math.Round(avgCells / avgMs * 1000, 0) : 0 + }; + } + + private static double Percentile(double[] values, int percentile) + { + if (values.Length == 0) return 0; + var sorted = values.OrderBy(v => v).ToArray(); + var index = (int)Math.Ceiling(percentile / 100.0 * sorted.Length) - 1; + return sorted[Math.Max(0, index)]; + } + + private static void TrimQueue(ConcurrentQueue queue) + { + while (queue.Count > MaxEntries) + { + queue.TryDequeue(out _); + } + } + + private record PerformanceEntry(DateTime Timestamp, TimeSpan Elapsed, int CellCount); +} diff --git a/src/NosCore.PathFinder.Api/Program.cs b/src/NosCore.PathFinder.Api/Program.cs new file mode 100644 index 0000000..4ac6864 --- /dev/null +++ b/src/NosCore.PathFinder.Api/Program.cs @@ -0,0 +1,341 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using System.Diagnostics; +using System.Globalization; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Localization; +using NosCore.PathFinder.Api; +using NosCore.PathFinder.Api.Database; +using NosCore.PathFinder.Api.Resource; +using NosCore.PathFinder.Brushfire; +using NosCore.PathFinder.Heuristic; +using NosCore.PathFinder.Interfaces; +using NosCore.Shared.I18N; +using Serilog; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using Logger = NosCore.Shared.I18N.Logger; + +var pathfinderConfig = LoadPathfinderConfig(); +Logger.PrintHeader("PATHFINDER API - NosCoreIO"); + +var builder = WebApplication.CreateBuilder(args); +builder.Host.UseSerilog(); + +builder.Services.AddLocalization(); +builder.Services.AddI18NLogs(); +builder.Services.AddTransient(typeof(ILogLanguageLocalizer), + x => new LogLanguageLocalizer( + x.GetRequiredService>())); + +builder.Services.ConfigureHttpJsonOptions(options => +{ + options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; +}); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var connectionString = LoadConnectionString(builder.Configuration, pathfinderConfig); +if (!string.IsNullOrEmpty(connectionString)) +{ + builder.Services.AddDbContext(options => + options.UseNpgsql(connectionString)); + builder.Services.AddSingleton(sp => + new MapStore(sp, sp.GetRequiredService>())); +} +else +{ + builder.Services.AddSingleton(); +} + +var app = builder.Build(); + +var logLanguage = app.Services.GetRequiredService>(); +Log.Information(logLanguage[LogLanguageKey.LANGUAGE_LOADED], CultureInfo.CurrentCulture.Name); + +if (!string.IsNullOrEmpty(connectionString)) +{ + Log.Information(logLanguage[LogLanguageKey.DATABASE_CONFIGURED]); +} +else +{ + Log.Information(logLanguage[LogLanguageKey.NO_DATABASE_CONFIGURED]); +} + +var mapStore = app.Services.GetRequiredService(); +await mapStore.LoadFromDatabaseAsync(); + +app.UseDefaultFiles(); +app.UseStaticFiles(); +app.UseWebSockets(); + +var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; + +app.MapGet("/api/maps", (MapStore store) => +{ + return Results.Ok(store.GetMapList()); +}); + +app.MapGet("/api/maps/{mapId}", (int mapId, MapStore store) => +{ + var map = store.GetMap(mapId); + if (map == null) return Results.NotFound(); + + byte[] gridData; + if (map is DatabaseMap dbMap) + gridData = dbMap.GetWalkabilityGrid(); + else if (map is SimpleMap simpleMap) + gridData = simpleMap.GetWalkabilityGrid(); + else + gridData = Array.Empty(); + + var monsters = store.GetMonsters(mapId).Select(m => new { id = m.Id, x = m.X, y = m.Y, vNum = m.VNum, type = "monster" }); + var npcs = store.GetNpcs(mapId).Select(n => new { id = n.Id, x = n.X, y = n.Y, vNum = n.VNum, type = "npc" }); + + return Results.Ok(new + { + width = map.Width, + height = map.Height, + grid = Convert.ToBase64String(gridData), + entities = monsters.Concat(npcs) + }); +}); + +app.MapGet("/api/maps/{mapId}/entities", (int mapId, MapStore store) => +{ + var monsters = store.GetMonsters(mapId).Select(m => new { m.Id, m.X, m.Y, m.VNum, Type = "monster" }); + var npcs = store.GetNpcs(mapId).Select(n => new { n.Id, n.X, n.Y, n.VNum, Type = "npc" }); + return Results.Ok(new { Monsters = monsters, Npcs = npcs }); +}); + +app.MapGet("/api/maps/{mapId}/brushfire", (int mapId, short x, short y, short maxDistance, string? heuristic, MapStore store, HeuristicProvider heuristicProvider, PerformanceTracker perf) => +{ + var map = store.GetMap(mapId); + if (map == null) return Results.NotFound(); + + var h = heuristicProvider.Get(heuristic); + var sw = Stopwatch.StartNew(); + var brushfire = map.LoadBrushFire((x, y), h, maxDistance); + sw.Stop(); + + perf.RecordBrushfire(sw.Elapsed, brushfire.Distances.Count); + + var cells = brushfire.Distances + .Select(kvp => new { X = kvp.Key.X, Y = kvp.Key.Y, Distance = kvp.Value }) + .ToList(); + + return Results.Ok(new + { + Origin = new { x, y }, + Cells = cells, + Performance = new { ElapsedMs = sw.Elapsed.TotalMilliseconds, CellCount = cells.Count } + }); +}); + +app.MapGet("/api/maps/{mapId}/flowfield", (int mapId, short x, short y, short maxDistance, double stopDistance, string? heuristic, MapStore store, HeuristicProvider heuristicProvider, PerformanceTracker perf) => +{ + var map = store.GetMap(mapId); + if (map == null) return Results.NotFound(); + + var h = heuristicProvider.Get(heuristic); + var sw = Stopwatch.StartNew(); + var brushfire = map.LoadBrushFire((x, y), h, maxDistance); + var flowfield = brushfire.GetFlowField(map, stopDistance); + sw.Stop(); + + perf.RecordFlowField(sw.Elapsed, flowfield.Vectors.Count); + + var vectors = flowfield.Vectors + .Select(kvp => new { X = kvp.Key.X, Y = kvp.Key.Y, Dx = kvp.Value.X, Dy = kvp.Value.Y }) + .ToList(); + + return Results.Ok(new + { + Origin = new { x, y }, + Vectors = vectors, + Performance = new { ElapsedMs = sw.Elapsed.TotalMilliseconds, VectorCount = vectors.Count } + }); +}); + +app.MapGet("/api/performance", (PerformanceTracker perf) => +{ + return Results.Ok(perf.GetStats()); +}); + +app.Map("/ws", async (HttpContext context, MapStore store, HeuristicProvider heuristicProvider, PerformanceTracker perf) => +{ + if (!context.WebSockets.IsWebSocketRequest) + { + context.Response.StatusCode = 400; + return; + } + + using var ws = await context.WebSockets.AcceptWebSocketAsync(); + var buffer = new byte[4096]; + + while (ws.State == WebSocketState.Open) + { + var result = await ws.ReceiveAsync(buffer, CancellationToken.None); + if (result.MessageType == WebSocketMessageType.Close) + { + await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); + break; + } + + var message = Encoding.UTF8.GetString(buffer, 0, result.Count); + var request = JsonSerializer.Deserialize(message, jsonOptions); + + if (request == null) continue; + + var map = store.GetMap(request.MapId); + if (map == null) continue; + + var heuristic = heuristicProvider.Get(request.Heuristic); + var sw = Stopwatch.StartNew(); + var brushfire = map.LoadBrushFire((request.X, request.Y), heuristic, request.MaxDistance); + var flowfield = brushfire.GetFlowField(map, request.StopDistance); + sw.Stop(); + + perf.RecordFlowField(sw.Elapsed, flowfield.Vectors.Count); + + var monsters = store.GetMonsters(request.MapId); + var npcs = store.GetNpcs(request.MapId); + + var response = new + { + Type = "flowfield", + Origin = new { request.X, request.Y }, + Vectors = flowfield.Vectors.Select(kvp => new { X = kvp.Key.X, Y = kvp.Key.Y, Dx = kvp.Value.X, Dy = kvp.Value.Y }), + Distances = brushfire.Distances.Select(kvp => new { X = kvp.Key.X, Y = kvp.Key.Y, D = kvp.Value }), + Monsters = monsters.Select(m => new { m.X, m.Y }), + Npcs = npcs.Select(n => new { n.X, n.Y }), + Performance = new { ElapsedMs = sw.Elapsed.TotalMilliseconds, VectorCount = flowfield.Vectors.Count } + }; + + var responseJson = JsonSerializer.Serialize(response, jsonOptions); + var responseBytes = Encoding.UTF8.GetBytes(responseJson); + await ws.SendAsync(responseBytes, WebSocketMessageType.Text, true, CancellationToken.None); + } +}); + +app.Run(); + +static PathfinderConfig? LoadPathfinderConfig() +{ + var yamlPaths = GetConfigPaths(); + + foreach (var yamlPath in yamlPaths) + { + var fullPath = Path.GetFullPath(yamlPath); + if (File.Exists(fullPath)) + { + try + { + var yaml = File.ReadAllText(fullPath); + var deserializer = new DeserializerBuilder() + .WithNamingConvention(PascalCaseNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .Build(); + return deserializer.Deserialize(yaml); + } + catch + { + } + } + } + + return null; +} + +static string? LoadConnectionString(IConfiguration configuration, PathfinderConfig? pathfinderConfig) +{ + var yamlPaths = GetConfigPaths(); + + foreach (var yamlPath in yamlPaths) + { + var fullPath = Path.GetFullPath(yamlPath); + if (File.Exists(fullPath)) + { + try + { + var yaml = File.ReadAllText(fullPath); + var deserializer = new DeserializerBuilder() + .WithNamingConvention(PascalCaseNamingConvention.Instance) + .IgnoreUnmatchedProperties() + .Build(); + var config = deserializer.Deserialize(yaml); + if (config?.Database != null) + { + var db = config.Database; + var connStr = $"Host={db.Host};Port={db.Port};Database={db.Database};Username={db.Username};Password={db.Password}"; + return connStr; + } + } + catch + { + } + } + } + + var jsonConnStr = configuration.GetConnectionString("NosCore"); + if (!string.IsNullOrEmpty(jsonConnStr)) + { + return jsonConnStr; + } + + return null; +} + +static string[] GetConfigPaths() +{ + var baseDir = AppContext.BaseDirectory; + var currentDir = Directory.GetCurrentDirectory(); + + return new[] + { + Path.Combine(baseDir, "..", "..", "configuration", "pathfinder.yml"), + Path.Combine(baseDir, "configuration", "pathfinder.yml"), + Path.Combine(currentDir, "configuration", "pathfinder.yml"), + Path.Combine(currentDir, "..", "..", "configuration", "pathfinder.yml"), + @"C:\dev\NosCore.PathFinder\configuration\pathfinder.yml" + }; +} + +record WsRequest(int MapId, short X, short Y, short MaxDistance = 22, double StopDistance = 0, string? Heuristic = null); + +class HeuristicProvider +{ + private readonly Dictionary _heuristics = new(StringComparer.OrdinalIgnoreCase) + { + ["octile"] = new OctileDistanceHeuristic(), + }; + + public IHeuristic Get(string? name) => + string.IsNullOrEmpty(name) || !_heuristics.TryGetValue(name, out var h) + ? _heuristics["octile"] + : h; + + public IEnumerable GetNames() => _heuristics.Keys; +} + +class PathfinderConfig +{ + public string? Language { get; set; } + public DatabaseConfig? Database { get; set; } +} + +class DatabaseConfig +{ + public string Host { get; set; } = "localhost"; + public int Port { get; set; } = 5432; + public string Database { get; set; } = "postgres"; + public string Username { get; set; } = "postgres"; + public string Password { get; set; } = ""; +} diff --git a/src/NosCore.PathFinder.Api/Resource/LocalizedResources.cs b/src/NosCore.PathFinder.Api/Resource/LocalizedResources.cs new file mode 100644 index 0000000..8c9b867 --- /dev/null +++ b/src/NosCore.PathFinder.Api/Resource/LocalizedResources.cs @@ -0,0 +1,11 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +namespace NosCore.PathFinder.Api.Resource; + +public class LocalizedResources +{ +} diff --git a/src/NosCore.PathFinder.Api/Resource/LocalizedResources.fr.resx b/src/NosCore.PathFinder.Api/Resource/LocalizedResources.fr.resx new file mode 100644 index 0000000..7b1f2e6 --- /dev/null +++ b/src/NosCore.PathFinder.Api/Resource/LocalizedResources.fr.resx @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Base de donnees configuree - chargement des cartes depuis PostgreSQL + + + Aucune base de donnees configuree - utilisation des cartes exemples uniquement + + + Repertoire de base: {0} + + + Repertoire courant: {0} + + + Recherche de pathfinder.yml... + + + Verification: {0} + + + Configuration de base de donnees chargee depuis: {0} + + + Echec de l'analyse de {0}: {1} + + + Utilisation de la chaine de connexion depuis appsettings.json + + + Aucune configuration de base de donnees trouvee + + + Charge {0} cartes, {1} monstres, {2} PNJ depuis la base de donnees + + + Echec du chargement de la carte {0} + + + Echec de connexion a la base de donnees - utilisation des cartes exemples uniquement + + + API PathFinder demarree sur {0} + + + Langue definie sur: {0} + + diff --git a/src/NosCore.PathFinder.Api/Resource/LocalizedResources.resx b/src/NosCore.PathFinder.Api/Resource/LocalizedResources.resx new file mode 100644 index 0000000..7f4767f --- /dev/null +++ b/src/NosCore.PathFinder.Api/Resource/LocalizedResources.resx @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Database configured - will load maps from PostgreSQL + + + No database configured - using sample maps only + + + Base directory: {0} + + + Current directory: {0} + + + Searching for pathfinder.yml... + + + Checking: {0} + + + Loaded database config from: {0} + + + Failed to parse {0}: {1} + + + Using connection string from appsettings.json + + + No database configuration found + + + Loaded {0} maps, {1} monsters, {2} NPCs from database + + + Failed to load map {0} + + + Failed to connect to database - using sample maps only + + + PathFinder API started on {0} + + + Language set to: {0} + + diff --git a/src/NosCore.PathFinder.Api/appsettings.json b/src/NosCore.PathFinder.Api/appsettings.json new file mode 100644 index 0000000..208400a --- /dev/null +++ b/src/NosCore.PathFinder.Api/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "NosCore": "Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=password" + } +} diff --git a/src/NosCore.PathFinder.Api/wwwroot/app.js b/src/NosCore.PathFinder.Api/wwwroot/app.js new file mode 100644 index 0000000..0b6d457 --- /dev/null +++ b/src/NosCore.PathFinder.Api/wwwroot/app.js @@ -0,0 +1,561 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +const canvas = document.getElementById('canvas'); +const ctx = canvas.getContext('2d'); + +let currentMap = null; +let mapData = null; +let flowFieldData = null; +let cellSize = 20; +let mode = 'flowfield'; +let showArrows = true; +let showEntities = true; +let maxDistance = 22; +let stopDistance = 0; +let ws = null; +let lastMousePos = { x: -1, y: -1 }; + +let entities = []; +let lastFrameTime = 0; +const MOVE_SPEED = 3; +const RETURN_SPEED = 2; + +let returnFlowFields = new Map(); + +let zoomLevel = 1; +let panX = 0; +let panY = 0; +let isDragging = false; +let dragStart = { x: 0, y: 0 }; +let gridBytes = null; +let wallCanvas = null; +let wallCtx = null; + +async function init() { + const maps = await fetch('/api/maps').then(r => r.json()); + const select = document.getElementById('mapSelect'); + + const dbMaps = maps.filter(m => m.source === 'database'); + const sampleMaps = maps.filter(m => m.source === 'sample'); + + if (dbMaps.length > 0) { + const dbGroup = document.createElement('optgroup'); + dbGroup.label = 'Database Maps'; + dbMaps.forEach(m => { + const opt = document.createElement('option'); + opt.value = m.id; + opt.textContent = `Map ${m.id} (${m.width}x${m.height})`; + dbGroup.appendChild(opt); + }); + select.appendChild(dbGroup); + } + + if (sampleMaps.length > 0) { + const sampleGroup = document.createElement('optgroup'); + sampleGroup.label = 'Sample Maps'; + sampleMaps.forEach(m => { + const opt = document.createElement('option'); + opt.value = m.id; + opt.textContent = `Sample ${Math.abs(m.id)} (${m.width}x${m.height})`; + sampleGroup.appendChild(opt); + }); + select.appendChild(sampleGroup); + } + + select.addEventListener('change', () => loadMap(parseInt(select.value))); + + document.querySelectorAll('.toggle').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.toggle').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + mode = btn.dataset.mode; + render(); + }); + }); + + document.getElementById('maxDistance').addEventListener('input', (e) => { + maxDistance = parseInt(e.target.value); + document.getElementById('distanceValue').textContent = maxDistance; + }); + + document.getElementById('stopDistance').addEventListener('input', (e) => { + stopDistance = parseFloat(e.target.value); + document.getElementById('stopDistanceValue').textContent = stopDistance; + }); + + document.getElementById('showArrows').addEventListener('change', (e) => { + showArrows = e.target.checked; + render(); + }); + + document.getElementById('showEntities').addEventListener('change', (e) => { + showEntities = e.target.checked; + render(); + }); + + canvas.addEventListener('mousemove', onMouseMove); + canvas.addEventListener('wheel', onWheel, { passive: false }); + canvas.addEventListener('mousedown', onMouseDown); + canvas.addEventListener('mouseup', onMouseUp); + canvas.addEventListener('mouseleave', onMouseUp); + + connectWebSocket(); + + if (maps.length > 0) { + loadMap(maps[0].id); + } + + setInterval(updateAggregateStats, 2000); + requestAnimationFrame(animate); +} + +function connectWebSocket() { + const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; + ws = new WebSocket(`${protocol}//${location.host}/ws`); + + ws.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data.type === 'flowfield') { + flowFieldData = data; + updatePerfStats(data.performance); + render(); + } + }; + + ws.onclose = () => setTimeout(connectWebSocket, 1000); +} + +async function loadMap(mapId) { + currentMap = mapId; + mapData = await fetch(`/api/maps/${mapId}`).then(r => r.json()); + flowFieldData = null; + + const container = canvas.parentElement; + const maxWidth = container.clientWidth - 40; + const maxHeight = container.clientHeight - 40; + + cellSize = Math.min( + Math.floor(maxWidth / mapData.width), + Math.floor(maxHeight / mapData.height), + 30 + ); + cellSize = Math.max(cellSize, 4); + + zoomLevel = 1; + panX = 0; + panY = 0; + + gridBytes = mapData.grid ? Uint8Array.from(atob(mapData.grid), c => c.charCodeAt(0)) : null; + + preRenderWalls(); + updateCanvasSize(); + initEntities(); + updateEntityCount(); + render(); +} + +function preRenderWalls() { + wallCanvas = document.createElement('canvas'); + wallCanvas.width = mapData.width * cellSize; + wallCanvas.height = mapData.height * cellSize; + wallCtx = wallCanvas.getContext('2d'); + + if (!gridBytes) return; + + wallCtx.fillStyle = '#21262d'; + for (let y = 0; y < mapData.height; y++) { + for (let x = 0; x < mapData.width; x++) { + const idx = y * mapData.width + x; + if (gridBytes[idx >> 3] & (1 << (idx & 7))) { + wallCtx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize); + } + } + } +} + +function updateCanvasSize() { + const container = canvas.parentElement; + const mapWidth = mapData.width * cellSize * zoomLevel; + const mapHeight = mapData.height * cellSize * zoomLevel; + + canvas.width = Math.min(mapWidth, container.clientWidth - 40); + canvas.height = Math.min(mapHeight, container.clientHeight - 40); +} + +function initEntities() { + entities = []; + returnFlowFields.clear(); + if (!mapData?.entities) return; + + let id = 0; + for (const e of mapData.entities) { + entities.push({ + id: id++, + type: e.type, + homeX: e.x, + homeY: e.y, + x: e.x, + y: e.y, + vnum: e.vNum, + needsReturnPath: false + }); + } +} + +function updateEntityCount() { + const monsterCount = mapData?.entities?.filter(e => e.type === 'monster').length || 0; + const npcCount = mapData?.entities?.filter(e => e.type === 'npc').length || 0; + document.getElementById('entityCount').textContent = `${monsterCount} monsters, ${npcCount} NPCs`; +} + +function animate(timestamp) { + const deltaTime = (timestamp - lastFrameTime) / 1000; + lastFrameTime = timestamp; + + if (mapData && entities.length > 0) { + updateEntities(deltaTime); + render(); + } + + requestAnimationFrame(animate); +} + +function isWalkable(x, y) { + if (x < 0 || y < 0 || x >= mapData.width || y >= mapData.height) return false; + if (!gridBytes) return true; + const ix = Math.floor(x); + const iy = Math.floor(y); + const idx = iy * mapData.width + ix; + return !(gridBytes[idx >> 3] & (1 << (idx & 7))); +} + +function updateEntities(deltaTime) { + const vectorMap = new Map(); + if (flowFieldData?.vectors) { + for (const v of flowFieldData.vectors) { + vectorMap.set(`${v.x},${v.y}`, { dx: v.dx, dy: v.dy }); + } + } + + for (const entity of entities) { + const cellX = Math.floor(entity.x); + const cellY = Math.floor(entity.y); + const key = `${cellX},${cellY}`; + const vector = vectorMap.get(key); + + if (vector) { + returnFlowFields.delete(entity.id); + entity.needsReturnPath = false; + + const newX = entity.x + vector.dx * MOVE_SPEED * deltaTime; + const newY = entity.y + vector.dy * MOVE_SPEED * deltaTime; + if (isWalkable(newX, newY)) { + entity.x = newX; + entity.y = newY; + } else if (isWalkable(newX, entity.y)) { + entity.x = newX; + } else if (isWalkable(entity.x, newY)) { + entity.y = newY; + } + } else { + const dx = entity.homeX - entity.x; + const dy = entity.homeY - entity.y; + const dist = Math.sqrt(dx * dx + dy * dy); + + if (dist < 0.1) { + entity.x = entity.homeX; + entity.y = entity.homeY; + returnFlowFields.delete(entity.id); + entity.needsReturnPath = false; + entity.stuckFrames = 0; + continue; + } + + const nx = dx / dist; + const ny = dy / dist; + const move = Math.min(dist, RETURN_SPEED * deltaTime); + const directX = entity.x + nx * move; + const directY = entity.y + ny * move; + + let moved = false; + if (isWalkable(directX, directY)) { + entity.x = directX; + entity.y = directY; + moved = true; + entity.stuckFrames = 0; + returnFlowFields.delete(entity.id); + entity.needsReturnPath = false; + } else if (isWalkable(directX, entity.y)) { + entity.x = directX; + moved = true; + entity.stuckFrames = 0; + } else if (isWalkable(entity.x, directY)) { + entity.y = directY; + moved = true; + entity.stuckFrames = 0; + } + + if (!moved) { + entity.stuckFrames = (entity.stuckFrames || 0) + 1; + + if (entity.stuckFrames > 5 && !entity.needsReturnPath) { + entity.needsReturnPath = true; + requestReturnPath(entity); + } + + const returnField = returnFlowFields.get(entity.id); + if (returnField) { + const returnVector = returnField.get(key); + if (returnVector) { + const pathX = entity.x + returnVector.dx * RETURN_SPEED * deltaTime; + const pathY = entity.y + returnVector.dy * RETURN_SPEED * deltaTime; + if (isWalkable(pathX, pathY)) { + entity.x = pathX; + entity.y = pathY; + } else if (isWalkable(pathX, entity.y)) { + entity.x = pathX; + } else if (isWalkable(entity.x, pathY)) { + entity.y = pathY; + } + } + } + } + } + } +} + +async function requestReturnPath(entity) { + try { + const dx = Math.abs(entity.x - entity.homeX); + const dy = Math.abs(entity.y - entity.homeY); + const pathDistance = Math.max(dx, dy) + 10; + const res = await fetch(`/api/maps/${currentMap}/flowfield?x=${entity.homeX}&y=${entity.homeY}&maxDistance=${pathDistance}&stopDistance=0`); + if (!res.ok) return; + + const data = await res.json(); + const vectorMap = new Map(); + for (const v of data.vectors) { + vectorMap.set(`${v.x},${v.y}`, { dx: v.dx, dy: v.dy }); + } + returnFlowFields.set(entity.id, vectorMap); + } catch (e) {} +} + +function onMouseMove(e) { + if (!mapData) return; + + const rect = canvas.getBoundingClientRect(); + + if (isDragging) { + panX += e.clientX - dragStart.x; + panY += e.clientY - dragStart.y; + dragStart = { x: e.clientX, y: e.clientY }; + render(); + return; + } + + const scale = cellSize * zoomLevel; + const x = Math.floor((e.clientX - rect.left - panX) / scale); + const y = Math.floor((e.clientY - rect.top - panY) / scale); + + if (x === lastMousePos.x && y === lastMousePos.y) return; + if (x < 0 || x >= mapData.width || y < 0 || y >= mapData.height) return; + + lastMousePos = { x, y }; + + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + mapId: currentMap, + x: x, + y: y, + maxDistance: maxDistance, + stopDistance: stopDistance + })); + } +} + +function onWheel(e) { + if (!mapData) return; + e.preventDefault(); + + const rect = canvas.getBoundingClientRect(); + const mouseX = e.clientX - rect.left; + const mouseY = e.clientY - rect.top; + + const worldX = (mouseX - panX) / (cellSize * zoomLevel); + const worldY = (mouseY - panY) / (cellSize * zoomLevel); + + const zoomFactor = e.deltaY < 0 ? 1.15 : 0.87; + const newZoom = Math.max(0.5, Math.min(5, zoomLevel * zoomFactor)); + + panX = mouseX - worldX * cellSize * newZoom; + panY = mouseY - worldY * cellSize * newZoom; + zoomLevel = newZoom; + + render(); +} + +function onMouseDown(e) { + if (e.button === 0 || e.button === 1) { + isDragging = true; + dragStart = { x: e.clientX, y: e.clientY }; + canvas.style.cursor = 'grabbing'; + e.preventDefault(); + } +} + +function onMouseUp() { + isDragging = false; + canvas.style.cursor = 'default'; +} + +function render() { + if (!mapData) return; + + const scale = cellSize * zoomLevel; + + ctx.fillStyle = '#0d1117'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.save(); + ctx.translate(panX, panY); + ctx.scale(zoomLevel, zoomLevel); + + if (wallCanvas) { + ctx.drawImage(wallCanvas, 0, 0); + } + + if (flowFieldData) { + const maxDist = Math.max(...flowFieldData.distances.map(d => d.d), 1); + + for (const cell of flowFieldData.distances) { + const intensity = 1 - (cell.d / maxDist); + const r = Math.floor(88 + 100 * intensity); + const g = Math.floor(166 + 60 * intensity); + const b = Math.floor(255 * intensity); + ctx.fillStyle = `rgba(${r},${g},${b},0.6)`; + ctx.fillRect(cell.x * cellSize, cell.y * cellSize, cellSize, cellSize); + } + + if (showArrows && mode === 'flowfield') { + ctx.strokeStyle = 'rgba(255,255,255,0.8)'; + ctx.fillStyle = 'rgba(255,255,255,0.8)'; + ctx.lineWidth = 1 / zoomLevel; + + for (const v of flowFieldData.vectors) { + drawArrow(v.x, v.y, v.dx, v.dy); + } + } + + ctx.fillStyle = '#58a6ff'; + ctx.beginPath(); + ctx.arc( + flowFieldData.origin.x * cellSize + cellSize / 2, + flowFieldData.origin.y * cellSize + cellSize / 2, + cellSize / 2.5, + 0, Math.PI * 2 + ); + ctx.fill(); + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 2 / zoomLevel; + ctx.stroke(); + } + + if (showEntities) { + for (const e of entities) { + if (e.type === 'monster') { + drawEntity(e.x, e.y, '#f85149', '#ff7b72'); + } else { + drawEntity(e.x, e.y, '#d29922', '#e3b341'); + } + } + } + + ctx.strokeStyle = 'rgba(255,255,255,0.05)'; + ctx.lineWidth = 0.5 / zoomLevel; + for (let x = 0; x <= mapData.width; x++) { + ctx.beginPath(); + ctx.moveTo(x * cellSize, 0); + ctx.lineTo(x * cellSize, mapData.height * cellSize); + ctx.stroke(); + } + for (let y = 0; y <= mapData.height; y++) { + ctx.beginPath(); + ctx.moveTo(0, y * cellSize); + ctx.lineTo(mapData.width * cellSize, y * cellSize); + ctx.stroke(); + } + + ctx.restore(); +} + +function drawEntity(x, y, fillColor, strokeColor) { + ctx.fillStyle = fillColor; + ctx.beginPath(); + ctx.arc( + x * cellSize + cellSize / 2, + y * cellSize + cellSize / 2, + cellSize / 3, + 0, Math.PI * 2 + ); + ctx.fill(); + ctx.strokeStyle = strokeColor; + ctx.lineWidth = 1.5 / zoomLevel; + ctx.stroke(); +} + +function drawArrow(x, y, dx, dy) { + const cx = x * cellSize + cellSize / 2; + const cy = y * cellSize + cellSize / 2; + const len = cellSize * 0.35; + const headLen = cellSize * 0.15; + + const ex = cx + dx * len; + const ey = cy + dy * len; + + ctx.beginPath(); + ctx.moveTo(cx, cy); + ctx.lineTo(ex, ey); + ctx.stroke(); + + const angle = Math.atan2(dy, dx); + ctx.beginPath(); + ctx.moveTo(ex, ey); + ctx.lineTo(ex - headLen * Math.cos(angle - 0.5), ey - headLen * Math.sin(angle - 0.5)); + ctx.lineTo(ex - headLen * Math.cos(angle + 0.5), ey - headLen * Math.sin(angle + 0.5)); + ctx.closePath(); + ctx.fill(); +} + +function updatePerfStats(perf) { + document.getElementById('lastMs').textContent = perf.elapsedMs.toFixed(2) + ' ms'; + document.getElementById('vectorCount').textContent = perf.vectorCount.toLocaleString(); + + const cellsPerSec = perf.elapsedMs > 0 ? Math.round(perf.vectorCount / perf.elapsedMs * 1000) : 0; + document.getElementById('cellsPerSec').textContent = cellsPerSec.toLocaleString(); + + const barWidth = Math.min(100, perf.elapsedMs * 10); + document.getElementById('perfBar').style.width = barWidth + '%'; +} + +async function updateAggregateStats() { + try { + const stats = await fetch('/api/performance').then(r => r.json()); + document.getElementById('totalReqs').textContent = stats.totalRequests.toLocaleString(); + + const ff = stats.flowField; + if (ff.count > 0) { + document.getElementById('avgMs').textContent = ff.avgMs.toFixed(2) + ' ms'; + document.getElementById('p95Ms').textContent = ff.p95Ms.toFixed(2) + ' ms'; + } + + document.getElementById('cpuPercent').textContent = stats.cpuPercent.toFixed(1) + '%'; + document.getElementById('memoryMb').textContent = stats.memoryMb.toFixed(1) + ' MB'; + } catch (e) {} +} + +init(); diff --git a/src/NosCore.PathFinder.Api/wwwroot/index.html b/src/NosCore.PathFinder.Api/wwwroot/index.html new file mode 100644 index 0000000..831e243 --- /dev/null +++ b/src/NosCore.PathFinder.Api/wwwroot/index.html @@ -0,0 +1,313 @@ + + + + + + NosCore PathFinder Visualizer + + + +
+ + +
+
+ +
+
+
Wall
+
Player (Origin)
+
Monster
+
NPC
+
Distance gradient
+
Flow direction
+
+
+
+ + + + diff --git a/src/NosCore.PathFinder.Gui/Configuration/PathfinderGuiConfiguration.cs b/src/NosCore.PathFinder.Gui/Configuration/PathfinderGuiConfiguration.cs deleted file mode 100644 index 604b4b6..0000000 --- a/src/NosCore.PathFinder.Gui/Configuration/PathfinderGuiConfiguration.cs +++ /dev/null @@ -1,17 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -using System.ComponentModel.DataAnnotations; -using NosCore.Shared.Configuration; - -namespace NosCore.PathFinder.Gui.Configuration -{ - public class PathfinderGuiConfiguration : LanguageConfiguration - { - [Required] - public SqlConnectionConfiguration? Database { get; set; } - } -} \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/Database/DataAccessHelper.cs b/src/NosCore.PathFinder.Gui/Database/DataAccessHelper.cs deleted file mode 100644 index 8f5fd2a..0000000 --- a/src/NosCore.PathFinder.Gui/Database/DataAccessHelper.cs +++ /dev/null @@ -1,47 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -using System; -using Microsoft.EntityFrameworkCore; -using NosCore.Dao.Interfaces; -using NosCore.PathFinder.Gui.I18N; -using Serilog; - -namespace NosCore.PathFinder.Gui.Database -{ - public class DataAccessHelper - { - private static readonly ILogger Logger = Shared.I18N.Logger.GetLoggerConfiguration().CreateLogger(); - - private DbContextOptions? _option; - - /// - /// Creates new instance of database context. - /// - public DbContext CreateContext() - { - return new NosCoreContext(_option); - } - - public void Initialize(DbContextOptions option) - { - _option = option; - using var context = CreateContext(); - try - { - context.Database.Migrate(); - context.Database.GetDbConnection().Open(); - Logger.Information(LogLanguage.Instance.GetMessageFromKey(LogLanguageKey.DATABASE_INITIALIZED)); - } - catch (Exception ex) - { - Logger.Error("Database Error", ex); - Logger.Error(LogLanguage.Instance.GetMessageFromKey(LogLanguageKey.DATABASE_NOT_UPTODATE)); - throw; - } - } - } -} \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/Database/Map.cs b/src/NosCore.PathFinder.Gui/Database/Map.cs deleted file mode 100644 index 80040f6..0000000 --- a/src/NosCore.PathFinder.Gui/Database/Map.cs +++ /dev/null @@ -1,31 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace NosCore.PathFinder.Gui.Database -{ - public class Map - { - public virtual ICollection MapMonster { get; set; } - - public virtual ICollection MapNpc { get; set; } - - public Map() - { - MapMonster = new HashSet(); - MapNpc = new HashSet(); - } - - public byte[] Data { get; set; } = null!; - - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.None)] - public short MapId { get; set; } - } -} diff --git a/src/NosCore.PathFinder.Gui/Database/MapMonsters.cs b/src/NosCore.PathFinder.Gui/Database/MapMonsters.cs deleted file mode 100644 index e4ba303..0000000 --- a/src/NosCore.PathFinder.Gui/Database/MapMonsters.cs +++ /dev/null @@ -1,26 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace NosCore.PathFinder.Gui.Database -{ - public class MapMonster - { - public virtual Map Map { get; set; } = null!; - - public short MapId { get; set; } - - [DatabaseGenerated(DatabaseGeneratedOption.None)] - [Key] - public int MapMonsterId { get; set; } - - public short MapX { get; set; } - - public short MapY { get; set; } - } -} diff --git a/src/NosCore.PathFinder.Gui/Database/MapNpc.cs b/src/NosCore.PathFinder.Gui/Database/MapNpc.cs deleted file mode 100644 index d52952e..0000000 --- a/src/NosCore.PathFinder.Gui/Database/MapNpc.cs +++ /dev/null @@ -1,26 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace NosCore.PathFinder.Gui.Database -{ - public class MapNpc - { - public virtual Map Map { get; set; } = null!; - - public short MapId { get; set; } - - [DatabaseGenerated(DatabaseGeneratedOption.None)] - [Key] - public int MapNpcId { get; set; } - - public short MapX { get; set; } - - public short MapY { get; set; } - } -} diff --git a/src/NosCore.PathFinder.Gui/Database/NosCoreContext.cs b/src/NosCore.PathFinder.Gui/Database/NosCoreContext.cs deleted file mode 100644 index e2ae8ce..0000000 --- a/src/NosCore.PathFinder.Gui/Database/NosCoreContext.cs +++ /dev/null @@ -1,40 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -using Microsoft.EntityFrameworkCore; -using NosCore.Dao.Extensions; - -namespace NosCore.PathFinder.Gui.Database -{ - public class NosCoreContext : DbContext - { - public NosCoreContext(DbContextOptions? options) : base(options) - { - } - - public virtual DbSet? Map { get; set; } = null!; - - public virtual DbSet? MapMonster { get; set; } = null!; - - public virtual DbSet? MapNpc { get; set; } = null!; - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - // remove automatic pluralization - modelBuilder.RemovePluralizingTableNameConvention(); - - modelBuilder.Entity() - .HasMany(e => e.MapMonster) - .WithOne(e => e.Map) - .OnDelete(DeleteBehavior.Restrict); - - modelBuilder.Entity() - .HasMany(e => e.MapMonster) - .WithOne(e => e.Map) - .OnDelete(DeleteBehavior.Restrict); - } - } -} \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/Dtos/MapDto.cs b/src/NosCore.PathFinder.Gui/Dtos/MapDto.cs deleted file mode 100644 index 0dbc209..0000000 --- a/src/NosCore.PathFinder.Gui/Dtos/MapDto.cs +++ /dev/null @@ -1,74 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using NosCore.PathFinder.Brushfire; -using NosCore.PathFinder.Gui.GuiObject; -using NosCore.PathFinder.Interfaces; -using NosCore.Shared.Helpers; - -namespace NosCore.PathFinder.Gui.Dtos -{ - public class MapDto : IMapGrid - { - public byte[] Data { get; set; } = null!; - - public short MapId { get; set; } - - private short _xLength; - - private short _yLength; - - public short Width - { - get - { - if (_xLength == 0) - { - _xLength = BitConverter.ToInt16(Data.AsSpan().Slice(0, 2).ToArray(), 0); - } - - return _xLength; - } - } - - public short Height - { - get - { - if (_yLength == 0) - { - _yLength = BitConverter.ToInt16(Data.AsSpan().Slice(2, 2).ToArray(), 0); - } - - return _yLength; - } - } - - public ConcurrentDictionary Players { get; set; } = - new ConcurrentDictionary(); - - public byte this[short x, short y] => Data.AsSpan().Slice(4 + y * Width + x, 1)[0]; - - public bool IsWalkable(short mapX, short mapY) - { - if ((mapX >= Width) || (mapX < 0) || (mapY >= Height) || (mapY < 0)) - { - return false; - } - - return IsWalkable(this[mapX, mapY]); - } - - private static bool IsWalkable(byte value) - { - return (value == 0) || (value == 2) || ((value >= 16) && (value <= 19)); - } - } -} diff --git a/src/NosCore.PathFinder.Gui/Dtos/MapMonsterDto.cs b/src/NosCore.PathFinder.Gui/Dtos/MapMonsterDto.cs deleted file mode 100644 index 6e88fe7..0000000 --- a/src/NosCore.PathFinder.Gui/Dtos/MapMonsterDto.cs +++ /dev/null @@ -1,21 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -namespace NosCore.PathFinder.Gui.Dtos -{ - public class MapMonsterDto - { - public int Speed { get; set; } - - public short MapId { get; set; } - - public int MapMonsterId { get; set; } - - public short MapX { get; set; } - - public short MapY { get; set; } - } -} diff --git a/src/NosCore.PathFinder.Gui/Dtos/MapNpcDto.cs b/src/NosCore.PathFinder.Gui/Dtos/MapNpcDto.cs deleted file mode 100644 index 0d9ba6c..0000000 --- a/src/NosCore.PathFinder.Gui/Dtos/MapNpcDto.cs +++ /dev/null @@ -1,21 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -namespace NosCore.PathFinder.Gui.Dtos -{ - public class MapNpcDto - { - public int Speed { get; set; } - - public short MapId { get; set; } - - public int MapNpcId { get; set; } - - public short MapX { get; set; } - - public short MapY { get; set; } - } -} diff --git a/src/NosCore.PathFinder.Gui/GuiObject/CharacterGo.cs b/src/NosCore.PathFinder.Gui/GuiObject/CharacterGo.cs deleted file mode 100644 index 9c334f2..0000000 --- a/src/NosCore.PathFinder.Gui/GuiObject/CharacterGo.cs +++ /dev/null @@ -1,40 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -using NosCore.PathFinder.Gui.Dtos; -using NosCore.Shared.Enumerations; - -namespace NosCore.PathFinder.Gui.GuiObject -{ - public class CharacterGo : IAliveEntity - { - public long VisualId { get; set; } - - public short MapX { get; set; } - - public short MapY { get; set; } - - public int Speed { get; set; } - - public long? TargetVisualId { get; set; } - - public VisualType? TargetVisualType { get; set; } - - public short PositionX - { - get => MapX; - set => MapX = value; - } - - public short PositionY - { - get => MapY; - set => MapY = value; - } - - public MapDto Map { get; set; } = default!; - } -} diff --git a/src/NosCore.PathFinder.Gui/GuiObject/IMovableEntity.cs b/src/NosCore.PathFinder.Gui/GuiObject/IMovableEntity.cs deleted file mode 100644 index 4586e8c..0000000 --- a/src/NosCore.PathFinder.Gui/GuiObject/IMovableEntity.cs +++ /dev/null @@ -1,183 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using NosCore.PathFinder.Brushfire; -using NosCore.PathFinder.Gui.Dtos; -using NosCore.PathFinder.Interfaces; -using NosCore.PathFinder.Pathfinder; -using NosCore.Shared.Enumerations; -using NosCore.Shared.Helpers; - -namespace NosCore.PathFinder.Gui.GuiObject -{ - public interface IMovableEntity : IAliveEntity - { - public const int RefreshRate = 200; - DateTime NextMove { get; set; } - } - - public interface IAliveEntity : IVisualEntity - { - long VisualId { get; } - - short MapX { get; set; } - - short MapY { get; set; } - - int Speed { get; set; } - - long? TargetVisualId { get; set; } - - public VisualType? TargetVisualType { get; set; } - } - - public interface IVisualEntity - { - short PositionX { get; set; } - - short PositionY { get; set; } - - MapDto Map { get; set; } - } - - public static class MovableEntityExtension - { - static IEnumerable<(short X, short Y)?> GetCellsInRadius(short firstX, short firstY, byte xradius, byte yradius) - { - for (var y = -yradius; y <= yradius; y++) - { - var projectedY = (short)Math.Clamp(y + firstY, 0, short.MaxValue); - for (var x = -xradius; x <= xradius; x++) - { - if ((x != firstX) || (y != firstY)) - { - yield return ((short)Math.Clamp(x + firstX, 0, short.MaxValue), projectedY); - } - } - } - } - - public static async Task MoveAsync(this IMovableEntity nonPlayableEntity, IHeuristic distanceCalculator) - { - var cellPerSec = 2.5 * nonPlayableEntity.Speed; - var mapX = nonPlayableEntity.PositionX; - var mapY = nonPlayableEntity.PositionY; - - if (nonPlayableEntity.TargetVisualId == null && nonPlayableEntity.TargetVisualType != VisualType.Map) - { - nonPlayableEntity.NextMove = DateTime.Now.AddMilliseconds(RandomHelper.Instance.RandomNumber(IMovableEntity.RefreshRate, 2500 + IMovableEntity.RefreshRate)); - - var freeCell = GetCellsInRadius(mapX, mapY, - (byte)RandomHelper.Instance.RandomNumber(0, 3), - (byte)RandomHelper.Instance.RandomNumber(0, 3)).OrderBy(_ => RandomHelper.Instance.RandomNumber(0, int.MaxValue)) - .FirstOrDefault(c => - { - var fromGrid = (c!.Value.X, c!.Value.Y); - while (fromGrid.X != mapX || fromGrid.Y != mapY) - { - var dX = mapX - fromGrid.X; - var dY = mapY - fromGrid.Y; - - var nDx = 0; - var nDy = 0; - if (dX != 0) - { - nDx = (dX / Math.Abs(dX)); - } - if (dY != 0) - { - nDy = (dY / Math.Abs(dY)); - } - - if (!nonPlayableEntity.Map.IsWalkable(fromGrid.X, fromGrid.Y)) - { - return false; - } - fromGrid.X += (short)nDx; - fromGrid.Y += (short)nDy; - } - - return true; - }); - if (freeCell == null) - { - return 0; - } - - mapX = freeCell.Value.X; - mapY = freeCell.Value.Y; - } - else - { - IPathfinder pathfinder = new JumpPointSearchPathfinder(nonPlayableEntity.Map, distanceCalculator); - List<(short X, short Y)>? path = null; - if (nonPlayableEntity.TargetVisualId != null && nonPlayableEntity.Map.Players.TryGetValue((long)nonPlayableEntity.TargetVisualId, out var target) && distanceCalculator.GetDistance((target.PositionX, target.PositionY), (nonPlayableEntity.PositionX, nonPlayableEntity.PositionY)) < 20) - { - if (path?.LastOrDefault() != (target.PositionX, target.PositionY)) - { - var goalPathFinder = new GoalBasedPathfinder(nonPlayableEntity.Map, distanceCalculator); - path = goalPathFinder.FindPath((nonPlayableEntity.PositionX, nonPlayableEntity.PositionY), - (target.PositionX, target.PositionY)).ToList(); - } - } - else if (nonPlayableEntity.TargetVisualType != VisualType.Map) - { - var targetFound = false; - for (var i = 0; i < 10; i++) - { - if (nonPlayableEntity.TargetVisualId != null && nonPlayableEntity.Map.Players.TryGetValue((long)nonPlayableEntity.TargetVisualId, out target) && distanceCalculator.GetDistance((target.PositionX, target.PositionY), - (nonPlayableEntity.PositionX, nonPlayableEntity.PositionY)) < 20) - { - targetFound = true; - break; - } - await Task.Delay(500); - } - - if (targetFound == false) - { - - nonPlayableEntity.TargetVisualType = (nonPlayableEntity.MapX, nonPlayableEntity.MapY) != - (nonPlayableEntity.PositionX, nonPlayableEntity.PositionY) ? VisualType.Map : (VisualType?)null; - - nonPlayableEntity.TargetVisualId = null; - } - } - else - { - path = pathfinder.FindPath((nonPlayableEntity.PositionX, nonPlayableEntity.PositionY), - (nonPlayableEntity.MapX, nonPlayableEntity.MapY)).ToList(); - - if (path.Count <= cellPerSec && path.LastOrDefault() == (nonPlayableEntity.MapX, nonPlayableEntity.MapY)) - { - nonPlayableEntity.TargetVisualType = null; - } - } - - - if (path?.Count > 1) - { - var refreshRate = TimeSpan.FromMilliseconds(IMovableEntity.RefreshRate).TotalSeconds; - var cellPerRefresh = (int)(cellPerSec * refreshRate); - var (x, y) = path.Count > cellPerRefresh ? path.Skip(cellPerRefresh).First() : path.SkipLast(1).Last(); - mapX = x; - mapY = y; - } - } - - var distance = distanceCalculator.GetDistance((nonPlayableEntity.PositionX, nonPlayableEntity.PositionY), (mapX, mapY)); - nonPlayableEntity.NextMove = DateTime.Now.AddMilliseconds(distance / cellPerSec); - await Task.Delay(TimeSpan.FromSeconds(distance / cellPerSec)); - nonPlayableEntity.PositionX = mapX; - nonPlayableEntity.PositionY = mapY; - return (int)TimeSpan.FromSeconds(distance / cellPerSec).TotalMilliseconds; - } - } -} \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/GuiObject/MapMonsterGo.cs b/src/NosCore.PathFinder.Gui/GuiObject/MapMonsterGo.cs deleted file mode 100644 index ca20763..0000000 --- a/src/NosCore.PathFinder.Gui/GuiObject/MapMonsterGo.cs +++ /dev/null @@ -1,52 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using NosCore.PathFinder.Gui.Dtos; -using NosCore.PathFinder.Heuristic; -using NosCore.Shared.Enumerations; -using Serilog; - -namespace NosCore.PathFinder.Gui.GuiObject -{ - public class MapMonsterGo : MapMonsterDto, IMovableEntity - { - private static readonly ILogger Logger = Shared.I18N.Logger.GetLoggerConfiguration().CreateLogger(); - public long VisualId => MapMonsterId; - public short PositionX { get; set; } - - public short PositionY { get; set; } - - public DateTime NextMove { get; set; } - - public long? TargetVisualId { get; set; } - - public VisualType? TargetVisualType { get; set; } - - public MapDto Map { get; set; } = null!; - - public async Task StartLife(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - try - { - var secondsWalking = await this.MoveAsync(new OctileDistanceHeuristic()); - await Task.Delay( - secondsWalking < IMovableEntity.RefreshRate - ? IMovableEntity.RefreshRate - secondsWalking - : secondsWalking, cancellationToken); - } - catch (Exception ex) - { - Logger.Error(ex, ex.Message); - } - } - } - } -} diff --git a/src/NosCore.PathFinder.Gui/GuiObject/MapNpcGo.cs b/src/NosCore.PathFinder.Gui/GuiObject/MapNpcGo.cs deleted file mode 100644 index a3680eb..0000000 --- a/src/NosCore.PathFinder.Gui/GuiObject/MapNpcGo.cs +++ /dev/null @@ -1,57 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -using System; -using System.Threading; -using System.Threading.Tasks; -using NosCore.PathFinder.Gui.Dtos; -using NosCore.PathFinder.Heuristic; -using NosCore.Shared.Enumerations; -using Serilog; - -namespace NosCore.PathFinder.Gui.GuiObject -{ - public class MapNpcGo : MapNpcDto, IMovableEntity - { - private static readonly ILogger Logger = Shared.I18N.Logger.GetLoggerConfiguration().CreateLogger(); - public long VisualId => MapNpcId; - - public short PositionX { get; set; } - - public short PositionY { get; set; } - - public DateTime NextMove { get; set; } - - public MapDto Map - { - get; - set; - } = null!; - - public long? TargetVisualId { get; set; } - - public VisualType? TargetVisualType { get; set; } - - public async Task StartLife(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - try - { - var secondsWalking = await this.MoveAsync(new OctileDistanceHeuristic()); - await Task.Delay( - secondsWalking < IMovableEntity.RefreshRate - ? IMovableEntity.RefreshRate - secondsWalking - : secondsWalking, cancellationToken); - } - catch (Exception ex) - { - Logger.Error(ex, ex.Message); - } - } - } - } -} diff --git a/src/NosCore.PathFinder.Gui/GuiWindow.cs b/src/NosCore.PathFinder.Gui/GuiWindow.cs deleted file mode 100644 index 80531a8..0000000 --- a/src/NosCore.PathFinder.Gui/GuiWindow.cs +++ /dev/null @@ -1,249 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Mapster; -using NosCore.PathFinder.Gui.Database; -using Serilog; -using NosCore.Dao; -using NosCore.PathFinder.Brushfire; -using NosCore.PathFinder.Gui.Dtos; -using NosCore.PathFinder.Gui.GuiObject; -using NosCore.PathFinder.Heuristic; -using NosCore.Shared.Enumerations; -using OpenTK; -using OpenTK.Graphics; -using OpenTK.Graphics.OpenGL; -using OpenTK.Input; -using Color = OpenTK.Color; - -namespace NosCore.PathFinder.Gui -{ - public class GuiWindow : GameWindow - { - private static readonly ILogger Logger = Shared.I18N.Logger.GetLoggerConfiguration().CreateLogger(); - private readonly MapDto _map; - - private readonly List _monsters; - private readonly List _npcs; - private readonly int _originalCellSize; - - private float _cellSize; - private readonly CharacterGo _mouseCharacter; - private readonly int _originalWidth; - - private readonly Vector2[] _wallPixels; - - private int _vertexBufferObject; - private Dictionary? _brushFirePixels; - - public GuiWindow(MapDto map, int width, int height, string title, DataAccessHelper dbContextBuilder) - : base(width, (height < width / map.Width * map.Height) ? width / map.Width * map.Height : height, - GraphicsMode.Default, title) - { - var dbContextBuilder1 = dbContextBuilder; - var mapMonsterDao = new Dao(Logger, dbContextBuilder1.CreateContext); - var mapNpcDao = new Dao(Logger, dbContextBuilder1.CreateContext); - _originalWidth = Width; - _originalCellSize = Width / map.Width; - _cellSize = Width / map.Width; - - _monsters = mapMonsterDao.Where(s => s.MapId == map.MapId)?.Adapt>() ?? - new List(); - _map = map; - _mouseCharacter = new CharacterGo() - { - VisualId = 1 - }; - _map.Players.TryAdd(1, _mouseCharacter); - - foreach (var mapMonster in _monsters) - { - mapMonster.PositionX = mapMonster.MapX; - mapMonster.PositionY = mapMonster.MapY; - mapMonster.Speed = 10; - mapMonster.Map = _map; - } - - _npcs = mapNpcDao.Where(s => s.MapId == map.MapId)?.Adapt>() ?? new List(); - foreach (var mapNpc in _npcs) - { - mapNpc.PositionX = mapNpc.MapX; - mapNpc.PositionY = mapNpc.MapY; - mapNpc.Speed = 10; - mapNpc.Map = _map; - } - - Parallel.ForEach(_monsters, monster => _ = monster.StartLife(CancellationToken.None)); - Parallel.ForEach(_npcs, npc => _ = npc.StartLife(CancellationToken.None)); - - var wallpixels = new List(); - for (short y = 0; y < _map.Height; y++) - { - for (short x = 0; x < _map.Width; x++) - { - if (_map[x, y] > 0) - { - wallpixels.Add(GenerateSquare(x, y)); - } - } - } - - _wallPixels = wallpixels.SelectMany(s => s).ToArray(); - } - - protected override void OnUpdateFrame(FrameEventArgs e) - { - base.OnUpdateFrame(e); - - var input = Keyboard.GetState(); - - if (input.IsKeyDown(Key.Escape)) - { - Exit(); - } - } - - protected override void OnResize(EventArgs e) - { - _cellSize = (float)_originalCellSize * Width / _originalWidth; - GL.Viewport(0, 0, Width, Height); - base.OnResize(e); - } - - protected override void OnMouseMove(MouseMoveEventArgs e) - { - base.OnMouseMove(e); - - var mapX = (short)(e.X / _cellSize); - var mapY = (short)(e.Y / _cellSize); - - if (mapX != _mouseCharacter.MapX || _mouseCharacter.MapY != mapY) - { - _mouseCharacter.MapX = mapX; - _mouseCharacter.MapY = mapY; - var distance = new OctileDistanceHeuristic(); - var brushfire = _map.LoadBrushFire((_mouseCharacter.MapX, _mouseCharacter.MapY), distance); - - _brushFirePixels = brushfire.Grid.Values.Where(s => s?.Value != null).GroupBy(s => (int)s!.Value!) - .ToDictionary(s => - { - var alpha = 255 - (s.Key * 10); - if (alpha < 0) - { - alpha = 0; - } - return Color.FromArgb((int)(alpha), 0, 255, 0); - }, s => s!.ToList().SelectMany(s => GenerateSquare(s!.Position.X, s.Position.Y)).ToArray()); - - foreach (var monster in _monsters.Where(s => - distance.GetDistance((mapX, mapY), (s.PositionX, s.PositionY)) < 5)) - { - monster.TargetVisualId = 1; - monster.TargetVisualType = VisualType.Player; - } - } - } - - protected override void OnLoad(EventArgs e) - { - base.OnLoad(e); - GL.ClearColor(Color.LightSkyBlue.A, Color.LightSkyBlue.R, Color.LightSkyBlue.G, Color.LightSkyBlue.B); - var world = Matrix4.CreateOrthographicOffCenter(0, ClientRectangle.Width, ClientRectangle.Height, 0, 0, 1); - GL.LoadMatrix(ref world);//deprecated - GL.EnableVertexAttribArray(0); - GL.Enable(EnableCap.Blend); - GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); - _vertexBufferObject = GL.GenBuffer(); - } - - protected override void OnUnload(EventArgs e) - { - GL.BindBuffer(BufferTarget.ArrayBuffer, 0); - GL.DeleteBuffer(_vertexBufferObject); - base.OnUnload(e); - } - - protected override void OnRenderFrame(FrameEventArgs e) - { - base.OnRenderFrame(e); - GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); - - DrawShapes(_wallPixels, Color.Blue, PrimitiveType.Quads); - foreach (var pixel in _brushFirePixels ?? new Dictionary()) - { - DrawShapes(pixel.Value, pixel.Key, PrimitiveType.Quads); - } - - var circle = GenerateDisk(_mouseCharacter.MapX, _mouseCharacter.MapY); - DrawShapes(circle, Color.BlueViolet, PrimitiveType.TriangleFan); - - var monstersDisk = _monsters.Where(s => s.TargetVisualId != null).SelectMany(s => GenerateDisk(s.PositionX, s.PositionY)).ToArray(); - DrawShapes(monstersDisk, Color.Red, PrimitiveType.TriangleFan); - - var npcDisk = _npcs.Where(s => s.TargetVisualId != null).SelectMany(s => GenerateDisk(s.PositionX, s.PositionY)).ToArray(); - DrawShapes(npcDisk, Color.Yellow, PrimitiveType.TriangleFan); - - var monstersCircle = _monsters.Where(s => s.TargetVisualId == null).SelectMany(s => GenerateCircle(s.PositionX, s.PositionY)).ToArray(); - DrawShapes(monstersCircle, Color.Red, PrimitiveType.LineLoop); - - var npcCircle = _npcs.Where(s => s.TargetVisualId == null).SelectMany(s => GenerateCircle(s.PositionX, s.PositionY)).ToArray(); - DrawShapes(npcCircle, Color.Yellow, PrimitiveType.LineLoop); - - SwapBuffers(); - } - - private Vector2[] GenerateSquare(short x, short y) - { - return new[] - { - new Vector2((float)(x * _cellSize), (float)(y * _cellSize)), - new Vector2((float)(_cellSize * (x + 1)), (float)(y * _cellSize)), - new Vector2((float)(_cellSize * (x + 1)), (float)(_cellSize * (y + 1))), - new Vector2((float)(x * _cellSize),(float)( _cellSize * (y + 1))) - }; - } - - private Vector2[] GenerateDisk(short x, short y) - { - return Enumerable.Range(0, 36).Select(i => new Vector2((float)((x + Math.Cos(i)) * _cellSize), - (float)((y + Math.Sin(i)) * _cellSize))).ToArray(); - } - - private Vector2[] GenerateCircle(short x, short y) - { - return Enumerable.Range(0, 36).Select(i => - { - var theta = 3.1415926f * i / 18; - return new Vector2((float)((x + Math.Cos(theta)) * _cellSize), - (float)((y + Math.Sin(theta)) * _cellSize)); - }).ToArray(); - } - - private void DrawShapes(Vector2[] vector, Color color, PrimitiveType type) - { - var shapeSize = type == PrimitiveType.Quads ? 4 : type == PrimitiveType.LineLoop ? 36 : 36; - var count = vector.Length / shapeSize; - var counts = Enumerable.Repeat(shapeSize, count).ToArray(); - var first = counts.Select((s, i) => s * i).ToArray(); - - - GL.BindBuffer(BufferTarget.ArrayBuffer, _vertexBufferObject); - GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(Vector2.SizeInBytes * vector.Length), vector, BufferUsageHint.StaticDraw); - - GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, Vector2.SizeInBytes, 0); - GL.Color4(color); //deprecated - GL.MultiDrawArrays(type, first, counts, count); - - GL.BindBuffer(BufferTarget.ArrayBuffer, 0); - } - } -} \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/I18N/LogLanguage.cs b/src/NosCore.PathFinder.Gui/I18N/LogLanguage.cs deleted file mode 100644 index 34def5b..0000000 --- a/src/NosCore.PathFinder.Gui/I18N/LogLanguage.cs +++ /dev/null @@ -1,49 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -using System.Globalization; -using System.Resources; -using NosCore.Shared.Enumerations; - -namespace NosCore.PathFinder.Gui.I18N -{ - public sealed class LogLanguage - { - private static LogLanguage? _instance; - - private static readonly CultureInfo ResourceCulture = new CultureInfo(Language.ToString()); - - private readonly ResourceManager _manager; - - private LogLanguage() - { - var assem = typeof(LogLanguageKey).Assembly; - _manager = new ResourceManager( - assem.GetName().Name + ".Resource.LocalizedResources", - assem); - } - - public static RegionType Language { get; set; } - - public static LogLanguage Instance => _instance ??= new LogLanguage(); - - public string GetMessageFromKey(LogLanguageKey messageKey) - { - return GetMessageFromKey(messageKey, null); - } - - public string GetMessageFromKey(LogLanguageKey messageKey, string? culture) - { - var cult = culture != null ? new CultureInfo(culture) : ResourceCulture; - var resourceMessage = (_manager != null) - ? _manager.GetResourceSet(cult, true, - cult.TwoLetterISOLanguageName == default(RegionType).ToString().ToLower(cult)) - ?.GetString(messageKey.ToString()) : string.Empty; - - return !string.IsNullOrEmpty(resourceMessage) ? resourceMessage : $"#<{messageKey}>"; - } - } -} \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/I18N/LogLanguageKey.cs b/src/NosCore.PathFinder.Gui/I18N/LogLanguageKey.cs deleted file mode 100644 index a339a22..0000000 --- a/src/NosCore.PathFinder.Gui/I18N/LogLanguageKey.cs +++ /dev/null @@ -1,19 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -using System.Diagnostics.CodeAnalysis; - -namespace NosCore.PathFinder.Gui.I18N -{ - [SuppressMessage("ReSharper", "InconsistentNaming")] - public enum LogLanguageKey - { - WRONG_SELECTED_MAPID, - SELECT_MAPID, - DATABASE_INITIALIZED, - DATABASE_NOT_UPTODATE - } -} \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/NosCore.PathFinder.Gui.csproj b/src/NosCore.PathFinder.Gui/NosCore.PathFinder.Gui.csproj deleted file mode 100644 index ca74c23..0000000 --- a/src/NosCore.PathFinder.Gui/NosCore.PathFinder.Gui.csproj +++ /dev/null @@ -1,58 +0,0 @@ - - - - Exe - net7.0 - true - latest - enable - - - - ../../build - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - PreserveNewest - - - - - - - - - - True - True - LocalizedResources.resx - - - - - - PublicResXFileCodeGenerator - LocalizedResources.Designer.cs - - - - \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/PathfinderGui.cs b/src/NosCore.PathFinder.Gui/PathfinderGui.cs deleted file mode 100644 index f8b9c4b..0000000 --- a/src/NosCore.PathFinder.Gui/PathfinderGui.cs +++ /dev/null @@ -1,68 +0,0 @@ -// __ _ __ __ ___ __ ___ ___ -// | \| |/__\ /' _/ / _//__\| _ \ __| -// | | ' | \/ |`._`.| \_| \/ | v / _| -// |_|\__|\__/ |___/ \__/\__/|_|_\___| -// ----------------------------------- - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using NosCore.PathFinder.Gui.Configuration; -using NosCore.PathFinder.Gui.Database; -using NosCore.PathFinder.Gui.I18N; -using NosCore.Dao; -using NosCore.PathFinder.Gui.Dtos; -using NosCore.Shared.Configuration; - -namespace NosCore.PathFinder.Gui -{ - public static class PathFinderGui - { - private const string Title = "NosCore - Pathfinder GUI"; - private const string ConsoleText = "PATHFINDER GUI - NosCoreIO"; - private static readonly PathfinderGuiConfiguration PathfinderGuiConfiguration = new PathfinderGuiConfiguration(); - private static readonly Dictionary GuiWindows = new Dictionary(); - private static readonly DataAccessHelper DbContextBuilder = new DataAccessHelper(); - - public static async Task Main(string[] args) - { - try { Console.Title = Title; } catch (PlatformNotSupportedException) { } - ConfiguratorBuilder.InitializeConfiguration(args, new[] { "pathfinder.yml", "logger.yml" }).Bind(PathfinderGuiConfiguration); - Shared.I18N.Logger.PrintHeader(ConsoleText); - var logger = Shared.I18N.Logger.GetLoggerConfiguration().CreateLogger(); - LogLanguage.Language = PathfinderGuiConfiguration.Language; - var optionsBuilder = new DbContextOptionsBuilder(); - optionsBuilder.UseNpgsql(PathfinderGuiConfiguration.Database!.ConnectionString); - DbContextBuilder.Initialize(optionsBuilder.Options); - var mapDao = new Dao(logger, DbContextBuilder.CreateContext); - - while (true) - { - logger.Information(LogLanguage.Instance.GetMessageFromKey(LogLanguageKey.SELECT_MAPID)); - var input = Console.ReadLine(); - if ((input == null) || !short.TryParse(input, out var askMapId)) - { - logger.Error(LogLanguage.Instance.GetMessageFromKey(LogLanguageKey.WRONG_SELECTED_MAPID)); - continue; - } - var map = await mapDao.FirstOrDefaultAsync(m => m.MapId == askMapId).ConfigureAwait(false); - - if ((!(map?.Width > 0)) || (map.Height <= 0)) - { - continue; - } - - if (GuiWindows.ContainsKey(map.MapId) && GuiWindows[map.MapId]!.Exists) - { - GuiWindows[map.MapId]!.Close(); - } - - GuiWindows[map.MapId] = new GuiWindow(map,1024, 768, - $"NosCore Pathfinder GUI - Map {map.MapId}", DbContextBuilder); - GuiWindows[map.MapId]!.Run(30); - } - } - } -} \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.Designer.cs b/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.Designer.cs deleted file mode 100644 index f7b3624..0000000 --- a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace NosCore.PathFinder.Gui.Resource { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class LocalizedResources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal LocalizedResources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("NosCore.PathFinder.Gui.Resource.LocalizedResources", typeof(LocalizedResources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to Database has been initialized.. - /// - public static string DATABASE_INITIALIZED { - get { - return ResourceManager.GetString("DATABASE_INITIALIZED", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Database may not be up to date. Please consider updating your database.. - /// - public static string DATABASE_NOT_UPTODATE { - get { - return ResourceManager.GetString("DATABASE_NOT_UPTODATE", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Please, Select a MapId:. - /// - public static string SELECT_MAPID { - get { - return ResourceManager.GetString("SELECT_MAPID", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Wrong MapId!. - /// - public static string WRONG_SELECTED_MAPID { - get { - return ResourceManager.GetString("WRONG_SELECTED_MAPID", resourceCulture); - } - } - } -} diff --git a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.cs.resx b/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.cs.resx deleted file mode 100644 index 78b0804..0000000 --- a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.cs.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Databáze byla Initializována - - - Databáze nemusí být aktuální. Zvažte aktualizaci - - - Prosím vybere ID Mapy - - - Špatně vybrané ID Mapy - - \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.de.resx b/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.de.resx deleted file mode 100644 index 28b9ff5..0000000 --- a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.de.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Datenbank wurde initialisiert. - - - Datenbank befindet sich nicht auf dem neusten Stand. - - - Bitte wähle eine MapId - - - Falsche MapId! - - \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.es.resx b/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.es.resx deleted file mode 100644 index 91457dd..0000000 --- a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.es.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - La base de datos ha sido inicializada. - - - La base de datos puede ser que no este actualizada. Por favor, considere actualizarla. - - - Por favor, seleccione una MapId: - - - ¡La Id del Mapa (IdMap) es incorrecta! - - \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.fr.resx b/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.fr.resx deleted file mode 100644 index e20010b..0000000 --- a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.fr.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - La base de donnée a été initialisée. - - - La base de donnée n'est pas a jour. - - - Veuillez séléctionner un MapID. - - - Mauvais MapID. - - \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.it.resx b/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.it.resx deleted file mode 100644 index e777d20..0000000 --- a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.it.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Il Database è stato avviato! - - - Il database non sembra aggiornato. Per favore considera di aggiornare il tuo database. - - - Seleziona un MapId: - - - MapId errato! - - \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.pl.resx b/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.pl.resx deleted file mode 100644 index 9c59ada..0000000 --- a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.pl.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Baza danych została zainicjowana. - - - Baza danych może nie być aktualna. Rozważ aktualizację bazy danych. - - - Proszę wybrać MapId: - - - Błędny MapId! - - \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.resx b/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.resx deleted file mode 100644 index 87fb79c..0000000 --- a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Database has been initialized. - - - Database may not be up to date. Please consider updating your database. - - - Please, Select a MapId: - - - Wrong MapId! - - \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.ru.resx b/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.ru.resx deleted file mode 100644 index 4b4e083..0000000 --- a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.ru.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - База данных инициализирована. - - - База данных могла устареть. Пожалуйста, проверьте обновления для БД. - - - Пожалуйста, выберите ID локации: - - - Некорректный MapId! - - \ No newline at end of file diff --git a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.tr.resx b/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.tr.resx deleted file mode 100644 index 2be5473..0000000 --- a/src/NosCore.PathFinder.Gui/Resource/LocalizedResources.tr.resx +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Veritabanı başarıyla yüklendi. - - - Veritabanı güncel olmayabilir. Lütfen sağlayıcıyla konuşun. - - - Lütfen bir harita kodu gir: - - - Hatalı Harita Kodu! - - \ No newline at end of file diff --git a/src/NosCore.PathFinder/Brushfire/BrushFire.cs b/src/NosCore.PathFinder/Brushfire/BrushFire.cs index cfcc9dd..4deeca1 100644 --- a/src/NosCore.PathFinder/Brushfire/BrushFire.cs +++ b/src/NosCore.PathFinder/Brushfire/BrushFire.cs @@ -10,24 +10,23 @@ namespace NosCore.PathFinder.Brushfire { public readonly struct BrushFire { - - public BrushFire((short X, short Y) origin, Dictionary<(short X, short Y), Node?> brushFireGrid, short width, + public BrushFire((short X, short Y) origin, Dictionary<(short X, short Y), double> distances, short width, short length) { Origin = origin; - Grid = brushFireGrid; + Distances = distances; Length = length; Width = width; } public (short X, short Y) Origin { get; } - public Dictionary<(short X, short Y), Node?> Grid { get; } + public Dictionary<(short X, short Y), double> Distances { get; } public short Length { get; } public short Width { get; } - public double? this[short x, short y] => Grid.ContainsKey((x, y)) ? Grid[(x, y)]?.Value : null; + public double? this[short x, short y] => Distances.TryGetValue((x, y), out var d) ? d : null; } } diff --git a/src/NosCore.PathFinder/Brushfire/FlowField.cs b/src/NosCore.PathFinder/Brushfire/FlowField.cs new file mode 100644 index 0000000..901b4d4 --- /dev/null +++ b/src/NosCore.PathFinder/Brushfire/FlowField.cs @@ -0,0 +1,31 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using System.Collections.Generic; + +namespace NosCore.PathFinder.Brushfire +{ + public readonly struct FlowField + { + public FlowField((short X, short Y) origin, Dictionary<(short X, short Y), (float X, float Y)> vectors, short width, short height) + { + Origin = origin; + Vectors = vectors; + Width = width; + Height = height; + } + + public (short X, short Y) Origin { get; } + + public Dictionary<(short X, short Y), (float X, float Y)> Vectors { get; } + + public short Width { get; } + + public short Height { get; } + + public (float X, float Y)? this[short x, short y] => Vectors.TryGetValue((x, y), out var vector) ? vector : null; + } +} diff --git a/src/NosCore.PathFinder/Brushfire/IMapGridExtension.cs b/src/NosCore.PathFinder/Brushfire/IMapGridExtension.cs index 3ba20e3..cf72425 100644 --- a/src/NosCore.PathFinder/Brushfire/IMapGridExtension.cs +++ b/src/NosCore.PathFinder/Brushfire/IMapGridExtension.cs @@ -4,14 +4,16 @@ // |_|\__|\__/ |___/ \__/\__/|_|_\___| // ----------------------------------- +using System; using System.Collections.Generic; -using System.Linq; using NosCore.PathFinder.Interfaces; namespace NosCore.PathFinder.Brushfire { public static class IMapGridExtension { + private static readonly float Sqrt2Inv = 1f / (float)Math.Sqrt(2); + private static readonly List<(short X, short Y)> Neighbours = new List<(short, short)> { (-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), @@ -20,15 +22,18 @@ public static class IMapGridExtension public static IEnumerable<(short X, short Y)> GetNeighbors(this IMapGrid grid, (short X, short Y) cell, bool includeWalls = false) { - return Neighbours.Where(delta => + for (int i = 0; i < Neighbours.Count; i++) { + var delta = Neighbours[i]; var currentX = (short)(cell.X + delta.X); var currentY = (short)(cell.Y + delta.Y); - return currentX >= 0 && currentX < grid.Width && - currentY >= 0 && currentY < grid.Height && - (includeWalls || grid.IsWalkable(currentX, currentY) - ); - }).Select(delta => ((short)(cell.X + delta.X), (short)(cell.Y + delta.Y))); + if (currentX >= 0 && currentX < grid.Width && + currentY >= 0 && currentY < grid.Height && + (includeWalls || grid.IsWalkable(currentX, currentY))) + { + yield return (currentX, currentY); + } + } } public static BrushFire LoadBrushFire(this IMapGrid mapGrid, (short X, short Y) user, IHeuristic heuristic, short maxDistance = 22) @@ -36,53 +41,93 @@ public static BrushFire LoadBrushFire(this IMapGrid mapGrid, (short X, short Y) if (user.X < 0 || user.X >= mapGrid.Width || user.Y < 0 || user.Y >= mapGrid.Height) { - return new BrushFire(user, new Dictionary<(short X, short Y), Node?>(), mapGrid.Width, mapGrid.Height); + return new BrushFire(user, new Dictionary<(short X, short Y), double>(), mapGrid.Width, mapGrid.Height); } - var path = new PriorityQueue(); - var cellGrid = new Dictionary<(short X, short Y), Node?>(); - var grid = new Node?[mapGrid.Width, mapGrid.Height]; - grid[user.X, user.Y] = new Node(user, mapGrid[user.X, user.Y]) + var queue = new PriorityQueue<(short X, short Y, double D), double>(); + var distances = new Dictionary<(short X, short Y), double>(); + + queue.Enqueue((user.X, user.Y, 0), 0); + distances[user] = 0; + + while (queue.Count > 0) { - Closed = true - }; - path.Enqueue(grid[user.X, user.Y]!, 0); - cellGrid[user] = new Node(user, null); + var cell = queue.Dequeue(); + var cellPos = (cell.X, cell.Y); + + if (distances.TryGetValue(cellPos, out var existingDist) && existingDist < cell.D) + { + continue; + } - // while the open list is not empty - while (path.Count > 0) + foreach (var neighborPos in mapGrid.GetNeighbors(cellPos)) + { + var distance = heuristic.GetDistance(neighborPos, cellPos) + cell.D; + if (distance > maxDistance) + { + continue; + } + + if (!distances.TryGetValue(neighborPos, out var neighborDist) || distance < neighborDist) + { + distances[neighborPos] = distance; + queue.Enqueue((neighborPos.X, neighborPos.Y, distance), distance); + } + } + } + return new BrushFire(user, distances, mapGrid.Width, mapGrid.Height); + } + + public static FlowField GetFlowField(this BrushFire brushFire, IMapGrid mapGrid, double stopDistance = 0) + { + var vectors = new Dictionary<(short X, short Y), (float X, float Y)>(); + + foreach (var kvp in brushFire.Distances) { - // pop the position of Cell which has the minimum `f` value. - var cell = path.Dequeue(); - cellGrid[cell.Position] ??= new Node(cell.Position, mapGrid[cell.Position.X, cell.Position.Y]); - grid[cell.Position.X, cell.Position.Y] ??= new Node(cell.Position, mapGrid[cell.Position.X, cell.Position.Y]); - grid[cell.Position.X, cell.Position.Y]!.Closed = true; + var pos = kvp.Key; + var currentDistance = kvp.Value; - // get neigbours of the current Cell if the neighbor has not been inspected yet, or can be reached with - var neighbors = mapGrid.GetNeighbors(cell.Position).Select(s => grid[s.X, s.Y] ?? new Node(s, mapGrid[s.X, s.Y])).Where(neighbor => !neighbor.Closed).ToList(); + if (pos == brushFire.Origin || currentDistance <= stopDistance) + { + continue; + } - for (int i = 0, l = neighbors.Count; i < l; ++i) + var bestNeighbor = pos; + var bestDistance = currentDistance; + + foreach (var neighbor in mapGrid.GetNeighbors(pos)) { - if (Equals(neighbors[i]!.F, 0d)) + if (neighbor == brushFire.Origin) { - var distance = heuristic.GetDistance(neighbors[i]!.Position, cell.Position) + cell.F; - if (distance > maxDistance) - { - //too far count as a wall - neighbors[i]!.Value = null; - continue; - } - - cellGrid[neighbors[i]!.Position] = new Node(neighbors[i]!.Position, distance); - neighbors[i]!.F = distance; - grid[neighbors[i]!.Position.X, neighbors[i]!.Position.Y] = neighbors[i]; + bestDistance = 0; + bestNeighbor = neighbor; + break; } - path.Enqueue(neighbors[i]!, neighbors[i]!.F); - neighbors[i]!.Closed = true; + if (brushFire.Distances.TryGetValue(neighbor, out var neighborDistance) && neighborDistance < bestDistance) + { + bestDistance = neighborDistance; + bestNeighbor = neighbor; + } + } + + if (bestNeighbor != pos) + { + var dx = bestNeighbor.X - pos.X; + var dy = bestNeighbor.Y - pos.Y; + + if (dx != 0 && dy != 0) + { + vectors[pos] = (dx * Sqrt2Inv, dy * Sqrt2Inv); + } + else + { + vectors[pos] = (dx, dy); + } } } - return new BrushFire(user, cellGrid, mapGrid.Width, mapGrid.Height); + + return new FlowField(brushFire.Origin, vectors, brushFire.Width, brushFire.Length); } } diff --git a/src/NosCore.PathFinder/NosCore.PathFinder.csproj b/src/NosCore.PathFinder/NosCore.PathFinder.csproj index 1ae42b2..7604033 100644 --- a/src/NosCore.PathFinder/NosCore.PathFinder.csproj +++ b/src/NosCore.PathFinder/NosCore.PathFinder.csproj @@ -1,7 +1,7 @@  - net7.0 + net10.0 latest favicon.ico true @@ -12,7 +12,7 @@ https://github.com/NosCoreIO/NosCore.PathFinder.git nostale, noscore, nostale private server source, nostale emulator - 2.0.1 + 2.1.0 false NosCore's PathFinder @@ -29,10 +29,14 @@ - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/src/NosCore.PathFinder/Pathfinder/GoalBasedPathfinder.cs b/src/NosCore.PathFinder/Pathfinder/GoalBasedPathfinder.cs index 5fc6e7b..1204b70 100644 --- a/src/NosCore.PathFinder/Pathfinder/GoalBasedPathfinder.cs +++ b/src/NosCore.PathFinder/Pathfinder/GoalBasedPathfinder.cs @@ -1,4 +1,4 @@ -// __ _ __ __ ___ __ ___ ___ +// __ _ __ __ ___ __ ___ ___ // | \| |/__\ /' _/ / _//__\| _ \ __| // | | ' | \/ |`._`.| \_| \/ | v / _| // |_|\__|\__/ |___/ \__/\__/|_|_\___| @@ -27,68 +27,65 @@ public GoalBasedPathfinder(IMapGrid mapGrid, IHeuristic heuristic) public GoalBasedPathfinder(IMapGrid mapGrid, IHeuristic heuristic, BrushFire brushfire) : this(mapGrid, heuristic) { - CacheBrushFire(brushfire, brushfire.Origin); + BrushFirecache.Set(brushfire.Origin, brushfire, DateTimeOffset.Now.AddSeconds(10)); } - - private BrushFire CacheBrushFire(BrushFire brushFire, (short X, short Y) start) + public IEnumerable<(short X, short Y)> FindPath((short X, short Y) start, (short X, short Y) end) { - Node? GetParent((short X, short Y) currentnode) + var list = new List<(short X, short Y)>(); + + if (!_mapGrid.IsWalkable(start.X, start.Y) || !_mapGrid.IsWalkable(end.X, end.Y)) { - var neighbor = _mapGrid.GetNeighbors(currentnode).Select(s => new Node((s.X, s.Y), brushFire.Grid.ContainsKey((s.X, s.Y)) ? brushFire.Grid[(s.X, s.Y)]?.Value ?? 0 : 0)).OrderBy(s => s.Value).FirstOrDefault(); - if (!(neighbor is { } neighborCell)) - { - return null; - } + return list; + } - if (!brushFire.Grid.ContainsKey((neighborCell.Position.X, neighborCell.Position.Y))) - { - brushFire.Grid.Add((neighborCell.Position.X, neighborCell.Position.Y), new Node(neighborCell.Position, null)); - } + if (!BrushFirecache.TryGetValue(end, out BrushFire brushFire)) + { + brushFire = _mapGrid.LoadBrushFire(end, _heuristic); + BrushFirecache.Set(end, brushFire, DateTimeOffset.Now.AddSeconds(10)); + } + + if (!brushFire.Distances.ContainsKey(start)) + { + return list; + } - brushFire.Grid[(neighborCell.Position.X, neighborCell.Position.Y)] ??= new Node(neighborCell.Position, null); + var current = start; + var visited = new HashSet<(short X, short Y)> { current }; - if (neighborCell.Value > 0 && brushFire.Grid[(neighborCell.Position.X, neighborCell.Position.Y)]!.Closed == false) + while (current != end) + { + var bestNeighbor = current; + var bestDistance = brushFire.Distances.TryGetValue(current, out var currentDist) ? currentDist : double.MaxValue; + + foreach (var neighbor in _mapGrid.GetNeighbors(current)) { - var parent = GetParent(neighborCell.Position); - if (parent != null) + if (visited.Contains(neighbor)) { - brushFire.Grid[(neighborCell.Position.X, neighborCell.Position.Y)]!.Parent ??= parent; - brushFire.Grid[(parent.Position.X, parent.Position.Y)] = parent; + continue; } - } - - brushFire.Grid[(neighborCell.Position.X, neighborCell.Position.Y)]!.Closed = true; - return brushFire.Grid[(neighborCell.Position.X, neighborCell.Position.Y)]; - } - - brushFire.Grid[(start.X, start.Y)] = new Node((start.X, start.Y), brushFire.Grid[(start.X, start.Y)]?.Value ?? 0) { Parent = GetParent((start.X, start.Y)), Closed = true }; - BrushFirecache.Set(brushFire.Origin, brushFire, DateTimeOffset.Now.AddSeconds(10)); - return brushFire; - } - - public IEnumerable<(short X, short Y)> FindPath((short X, short Y) start, (short X, short Y) end) - { - List<(short X, short Y)> list = new(); - BrushFirecache.TryGetValue(end, out BrushFire? brushFireOut); + if (neighbor == end) + { + bestNeighbor = neighbor; + break; + } - if (!_mapGrid.IsWalkable(start.X, start.Y) || !_mapGrid.IsWalkable(end.X, end.Y) || (brushFireOut != null && !brushFireOut.Value.Grid.ContainsKey((start.X, start.Y)))) - { - return list; - } + if (brushFire.Distances.TryGetValue(neighbor, out var neighborDist) && neighborDist < bestDistance) + { + bestDistance = neighborDist; + bestNeighbor = neighbor; + } + } - if (brushFireOut?.Grid[(start.X, start.Y)]?.Parent == null) - { - var brushFire = brushFireOut ?? _mapGrid.LoadBrushFire(end, _heuristic); - brushFireOut = CacheBrushFire(brushFire, start); - } + if (bestNeighbor == current) + { + break; + } - if (!(brushFireOut?.Grid[(start.X, start.Y)] is { } currentnode)) return list; - while (currentnode.Parent != null && currentnode.Parent.Position != (start)) - { - list.Add(currentnode.Parent.Position); - currentnode = (Node)currentnode.Parent; + current = bestNeighbor; + visited.Add(current); + list.Add(current); } return list; diff --git a/test/NosCore.PathFinder.Benchmark/NosCore.PathFinder.Benchmark/NosCore.PathFinder.Benchmark.csproj b/test/NosCore.PathFinder.Benchmark/NosCore.PathFinder.Benchmark/NosCore.PathFinder.Benchmark.csproj index 5d489e5..8980ad1 100644 --- a/test/NosCore.PathFinder.Benchmark/NosCore.PathFinder.Benchmark/NosCore.PathFinder.Benchmark.csproj +++ b/test/NosCore.PathFinder.Benchmark/NosCore.PathFinder.Benchmark/NosCore.PathFinder.Benchmark.csproj @@ -1,12 +1,12 @@ Exe - net7.0 + net10.0 enable - + diff --git a/test/NosCore.PathFinder.Tests/BrushFireTests.cs b/test/NosCore.PathFinder.Tests/BrushFireTests.cs index 4534c38..3571823 100644 --- a/test/NosCore.PathFinder.Tests/BrushFireTests.cs +++ b/test/NosCore.PathFinder.Tests/BrushFireTests.cs @@ -1,14 +1,18 @@ -// __ _ __ __ ___ __ ___ ___ +// __ _ __ __ ___ __ ___ ___ // | \| |/__\ /' _/ / _//__\| _ \ __| // | | ' | \/ |`._`.| \_| \/ | v / _| // |_|\__|\__/ |___/ \__/\__/|_|_\___| // ----------------------------------- using System.Collections.Generic; -using System.Drawing; using Microsoft.VisualStudio.TestTools.UnitTesting; using NosCore.PathFinder.Brushfire; using NosCore.PathFinder.Heuristic; +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace NosCore.PathFinder.Tests { @@ -23,36 +27,51 @@ public void Test_BrushFire() { (short X, short Y) characterPosition = (6, 10); var brushFire = _map.LoadBrushFire(characterPosition, new OctileDistanceHeuristic()); - var bitmap = new Bitmap(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); - var listPixel = new List(); - TestHelper.DrawMap(_map, TestHelper.Scale, listPixel, bitmap, (0, 0), characterPosition); - using var graphics = Graphics.FromImage(bitmap); + using var image = new Image(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); + var listPixel = new List(); + TestHelper.DrawMap(_map, TestHelper.Scale, listPixel, image, (0, 0), characterPosition); + var font = TestHelper.GetFont(); - - for (short y = 0; y < _map.Height; y++) + image.Mutate(ctx => { - for (short x = 0; x < _map.Width; x++) + for (short y = 0; y < _map.Height; y++) { - var rectangle = new Rectangle(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); - if ((x, y) != characterPosition) + for (short x = 0; x < _map.Width; x++) { - if (brushFire[x, y] != null) - { - graphics.FillRectangle(new Pen(Color.White).Brush, rectangle); - var color = Color.FromArgb((int)((brushFire[x, y] * 12 > 255 ? 255 : (brushFire[x, y] ?? 0) * 12)), 0, 0, 255); - graphics.DrawString(brushFire[x, y]?.ToString("N0"), new Font("Arial", 16), Brushes.Black, rectangle, TestHelper.StringFormat); - graphics.FillRectangle(new Pen(color).Brush, rectangle); - listPixel.Add(color); - } - else + var rect = new RectangleF(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); + if ((x, y) != characterPosition) { - graphics.DrawString("∞", new Font("Arial", 16), Brushes.White, rectangle, TestHelper.StringFormat); + if (brushFire[x, y] != null) + { + ctx.Fill(Color.White, rect); + var alpha = (byte)((brushFire[x, y] * 12 > 255 ? 255 : (brushFire[x, y] ?? 0) * 12)); + var color = new Rgba32(0, 0, 255, alpha); + var textOptions = new RichTextOptions(font) + { + Origin = new PointF(x * TestHelper.Scale + TestHelper.Scale / 2f, y * TestHelper.Scale + TestHelper.Scale / 2f), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + ctx.DrawText(textOptions, brushFire[x, y]?.ToString("N0") ?? "", Color.Black); + ctx.Fill(color, rect); + listPixel.Add(color); + } + else + { + var textOptions = new RichTextOptions(font) + { + Origin = new PointF(x * TestHelper.Scale + TestHelper.Scale / 2f, y * TestHelper.Scale + TestHelper.Scale / 2f), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + ctx.DrawText(textOptions, "∞", Color.White); + } } } } - } + }); - TestHelper.VerifyFile("brushfire.png", bitmap, listPixel, "Brushfire"); + TestHelper.VerifyFile("brushfire.png", image, listPixel, "Brushfire"); } } } diff --git a/test/NosCore.PathFinder.Tests/FlowFieldTests.cs b/test/NosCore.PathFinder.Tests/FlowFieldTests.cs new file mode 100644 index 0000000..e4f286d --- /dev/null +++ b/test/NosCore.PathFinder.Tests/FlowFieldTests.cs @@ -0,0 +1,183 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NosCore.PathFinder.Brushfire; +using NosCore.PathFinder.Heuristic; +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace NosCore.PathFinder.Tests +{ + [TestClass] + public class FlowFieldTests + { + private readonly TestMap _map = TestHelper.SimpleMap; + + [TestMethod] + public void Test_FlowField() + { + (short X, short Y) characterPosition = (6, 10); + var brushFire = _map.LoadBrushFire(characterPosition, new OctileDistanceHeuristic()); + var flowField = brushFire.GetFlowField(_map); + + using var image = new Image(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); + var listPixel = new List(); + TestHelper.DrawMap(_map, TestHelper.Scale, listPixel, image, (0, 0), characterPosition); + + image.Mutate(ctx => + { + for (short y = 0; y < _map.Height; y++) + { + for (short x = 0; x < _map.Width; x++) + { + var rect = new RectangleF(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); + if ((x, y) != characterPosition) + { + if (brushFire[x, y] != null) + { + ctx.Fill(Color.White, rect); + var alpha = (byte)((brushFire[x, y] * 12 > 255 ? 255 : (brushFire[x, y] ?? 0) * 12)); + var color = new Rgba32(0, 0, 255, alpha); + ctx.Fill(color, rect); + listPixel.Add(color); + } + } + } + } + }); + + for (short y = 0; y < _map.Height; y++) + { + for (short x = 0; x < _map.Width; x++) + { + if ((x, y) != characterPosition && brushFire[x, y] != null) + { + var vector = flowField[x, y]; + if (vector != null) + { + TestHelper.DrawArrow(image, x, y, vector.Value.X, vector.Value.Y, TestHelper.Scale, Color.White.ToPixel()); + } + } + } + } + + TestHelper.VerifyFile("flow-field.png", image, listPixel, "Flow Field (Vector Field Pathfinding)"); + } + + [TestMethod] + public void Test_FlowField_MonsterPath() + { + (short X, short Y) characterPosition = (6, 10); + (short X, short Y) monsterPosition = (15, 16); + + var brushFire = _map.LoadBrushFire(characterPosition, new OctileDistanceHeuristic()); + var flowField = brushFire.GetFlowField(_map); + + var path = TraceFlowFieldPath(flowField, monsterPosition, characterPosition); + + using var image = new Image(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); + var listPixel = new List(); + TestHelper.DrawMap(_map, TestHelper.Scale, listPixel, image, monsterPosition, characterPosition); + var font = TestHelper.GetFont(); + + image.Mutate(ctx => + { + for (short y = 0; y < _map.Height; y++) + { + for (short x = 0; x < _map.Width; x++) + { + var rect = new RectangleF(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); + if ((x, y) != characterPosition && (x, y) != monsterPosition) + { + if (brushFire[x, y] != null) + { + ctx.Fill(Color.White, rect); + var alpha = (byte)((brushFire[x, y] * 12 > 255 ? 255 : (brushFire[x, y] ?? 0) * 12)); + var color = new Rgba32(0, 0, 255, alpha); + ctx.Fill(color, rect); + listPixel.Add(color); + } + } + } + } + }); + + for (short y = 0; y < _map.Height; y++) + { + for (short x = 0; x < _map.Width; x++) + { + if ((x, y) != characterPosition && (x, y) != monsterPosition && brushFire[x, y] != null) + { + var vector = flowField[x, y]; + if (vector != null) + { + TestHelper.DrawArrow(image, x, y, vector.Value.X, vector.Value.Y, TestHelper.Scale, Color.White.ToPixel()); + } + } + } + } + + var pathArray = path.ToArray(); + image.Mutate(ctx => + { + for (var i = 0; i < pathArray.Length; i++) + { + var (x, y) = pathArray[i]; + if ((x, y) != monsterPosition && (x, y) != characterPosition) + { + var rect = new RectangleF(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); + var color = Color.LightPink; + ctx.Fill(color, rect); + var textOptions = new RichTextOptions(font) + { + Origin = new PointF(x * TestHelper.Scale + TestHelper.Scale / 2f, y * TestHelper.Scale + TestHelper.Scale / 2f), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + ctx.DrawText(textOptions, i.ToString(), Color.Black); + listPixel.Add(color.ToPixel()); + } + } + }); + + TestHelper.VerifyFile("flow-field-path.png", image, listPixel, "Flow Field Path (Monster following vectors to Player)"); + } + + private static List<(short X, short Y)> TraceFlowFieldPath(FlowField flowField, (short X, short Y) start, (short X, short Y) target, int maxSteps = 100) + { + var path = new List<(short X, short Y)>(); + var current = start; + + for (var step = 0; step < maxSteps; step++) + { + var vector = flowField[current.X, current.Y]; + if (vector == null) + break; + + var nextX = (short)(current.X + Math.Sign(vector.Value.X)); + var nextY = (short)(current.Y + Math.Sign(vector.Value.Y)); + + if ((nextX, nextY) == current) + break; + + current = (nextX, nextY); + path.Add(current); + + if (current == target) + break; + } + + return path; + } + } +} diff --git a/test/NosCore.PathFinder.Tests/GoalBasedPathfinderTests.cs b/test/NosCore.PathFinder.Tests/GoalBasedPathfinderTests.cs index 8da429a..0f47b9a 100644 --- a/test/NosCore.PathFinder.Tests/GoalBasedPathfinderTests.cs +++ b/test/NosCore.PathFinder.Tests/GoalBasedPathfinderTests.cs @@ -1,4 +1,4 @@ -// __ _ __ __ ___ __ ___ ___ +// __ _ __ __ ___ __ ___ ___ // | \| |/__\ /' _/ / _//__\| _ \ __| // | | ' | \/ |`._`.| \_| \/ | v / _| // |_|\__|\__/ |___/ \__/\__/|_|_\___| @@ -6,13 +6,17 @@ using System; using System.Collections.Generic; -using System.Drawing; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using NosCore.PathFinder.Brushfire; using NosCore.PathFinder.Heuristic; using NosCore.PathFinder.Interfaces; using NosCore.PathFinder.Pathfinder; +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace NosCore.PathFinder.Tests { @@ -35,53 +39,75 @@ public GoalBasedPathfinderTests() [TestMethod] public void Test_GoalBasedPathfinder() { - var bitmap = new Bitmap(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); + using var image = new Image(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); (short X, short Y) target = (15, 16); - var listPixel = new List(); - TestHelper.DrawMap(_map, TestHelper.Scale, listPixel, bitmap, target, _characterPosition); - using var graphics = Graphics.FromImage(bitmap); + var listPixel = new List(); + TestHelper.DrawMap(_map, TestHelper.Scale, listPixel, image, target, _characterPosition); + var font = TestHelper.GetFont(); - - for (short y = 0; y < _map.Height; y++) + image.Mutate(ctx => { - for (short x = 0; x < _map.Width; x++) + for (short y = 0; y < _map.Height; y++) { - var rectangle = new Rectangle(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); - if ((x, y) != target && (x, y) != _characterPosition) + for (short x = 0; x < _map.Width; x++) { - if (_brushFire[x, y] != null) - { - graphics.FillRectangle(new Pen(Color.White).Brush, rectangle); - var color = Color.FromArgb((int)((_brushFire[x, y] * 12 > 255 ? 255 : (_brushFire[x, y] ?? 0) * 12)), 0, 0, 255); - graphics.DrawString(_brushFire[x, y]?.ToString("N0"), new Font("Arial", 16), Brushes.Black, rectangle, TestHelper.StringFormat); - graphics.FillRectangle(new Pen(color).Brush, rectangle); - listPixel.Add(color); - } - else + var rect = new RectangleF(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); + if ((x, y) != target && (x, y) != _characterPosition) { - graphics.DrawString("∞", new Font("Arial", 16), Brushes.White, rectangle, TestHelper.StringFormat); + if (_brushFire[x, y] != null) + { + ctx.Fill(Color.White, rect); + var alpha = (byte)((_brushFire[x, y] * 12 > 255 ? 255 : (_brushFire[x, y] ?? 0) * 12)); + var color = new Rgba32(0, 0, 255, alpha); + var textOptions = new RichTextOptions(font) + { + Origin = new PointF(x * TestHelper.Scale + TestHelper.Scale / 2f, y * TestHelper.Scale + TestHelper.Scale / 2f), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + ctx.DrawText(textOptions, _brushFire[x, y]?.ToString("N0") ?? "", Color.Black); + ctx.Fill(color, rect); + listPixel.Add(color); + } + else + { + var textOptions = new RichTextOptions(font) + { + Origin = new PointF(x * TestHelper.Scale + TestHelper.Scale / 2f, y * TestHelper.Scale + TestHelper.Scale / 2f), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + ctx.DrawText(textOptions, "∞", Color.White); + } } } } - } + }); var path = _goalPathfinder.FindPath(target, _characterPosition).ToList(); - foreach (var (x, y) in path) + image.Mutate(ctx => { - if ((x, y) != target && (x, y) != _characterPosition) + foreach (var (x, y) in path) { - var rectangle = new Rectangle(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, - TestHelper.Scale); - var color = Color.LightPink; - graphics.FillRectangle(new Pen(color).Brush, rectangle); - graphics.DrawString(Array.IndexOf(path.ToArray(), (x, y)).ToString(), new Font("Arial", 16), - Brushes.Black, rectangle, TestHelper.StringFormat); - listPixel.Add(color); + if ((x, y) != target && (x, y) != _characterPosition) + { + var rect = new RectangleF(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); + var color = Color.LightPink; + ctx.Fill(color, rect); + var textOptions = new RichTextOptions(font) + { + Origin = new PointF(x * TestHelper.Scale + TestHelper.Scale / 2f, y * TestHelper.Scale + TestHelper.Scale / 2f), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + ctx.DrawText(textOptions, Array.IndexOf(path.ToArray(), (x, y)).ToString(), Color.Black); + listPixel.Add(color.ToPixel()); + } } - } + }); - TestHelper.VerifyFile("goal-based-pathfinder.png", bitmap, listPixel, "Goal Based Pathfinder"); + TestHelper.VerifyFile("goal-based-pathfinder.png", image, listPixel, "Goal Based Pathfinder"); } diff --git a/test/NosCore.PathFinder.Tests/JumpPointSearchPathfinderTests.cs b/test/NosCore.PathFinder.Tests/JumpPointSearchPathfinderTests.cs index 41c6fcb..e6b28cd 100644 --- a/test/NosCore.PathFinder.Tests/JumpPointSearchPathfinderTests.cs +++ b/test/NosCore.PathFinder.Tests/JumpPointSearchPathfinderTests.cs @@ -1,4 +1,4 @@ -// __ _ __ __ ___ __ ___ ___ +// __ _ __ __ ___ __ ___ ___ // | \| |/__\ /' _/ / _//__\| _ \ __| // | | ' | \/ |`._`.| \_| \/ | v / _| // |_|\__|\__/ |___/ \__/\__/|_|_\___| @@ -6,11 +6,15 @@ using System; using System.Collections.Generic; -using System.Drawing; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using NosCore.PathFinder.Heuristic; using NosCore.PathFinder.Pathfinder; +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace NosCore.PathFinder.Tests { @@ -31,33 +35,42 @@ public JumpPointSearchPathfinderTests() [TestMethod] public void Test_JumpPointSearchPathfinder() { - var bitmap = new Bitmap(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); + using var image = new Image(_map.Width * TestHelper.Scale, _map.Height * TestHelper.Scale); (short X, short Y) target = (15, 16); - var listPixel = new List(); - TestHelper.DrawMap(_map, TestHelper.Scale, listPixel, bitmap, target, _characterPosition); - using var graphics = Graphics.FromImage(bitmap); + var listPixel = new List(); + TestHelper.DrawMap(_map, TestHelper.Scale, listPixel, image, target, _characterPosition); + var font = TestHelper.GetFont(); var jumps = _jumpPointSearchPathfinder.GetJumpList(target, _characterPosition).ToList(); var path = _jumpPointSearchPathfinder.FindPath(target, _characterPosition).ToList(); - foreach (var (x, y) in path) + + image.Mutate(ctx => { - if ((x, y) != target && (x, y) != _characterPosition) + foreach (var (x, y) in path) { - var rectangle = new Rectangle(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, - TestHelper.Scale); - var color = Color.LightPink; - if (jumps.Contains((x, y))) + if ((x, y) != target && (x, y) != _characterPosition) { - color = Color.DeepPink; - } + var rect = new RectangleF(x * TestHelper.Scale, y * TestHelper.Scale, TestHelper.Scale, TestHelper.Scale); + var color = Color.LightPink; + if (jumps.Contains((x, y))) + { + color = Color.DeepPink; + } - graphics.FillRectangle(new Pen(color).Brush, rectangle); - graphics.DrawString(Array.IndexOf(path.ToArray(), (x, y)).ToString(), new Font("Arial", 16), - Brushes.Black, rectangle, TestHelper.StringFormat); - listPixel.Add(color); + ctx.Fill(color, rect); + var textOptions = new RichTextOptions(font) + { + Origin = new PointF(x * TestHelper.Scale + TestHelper.Scale / 2f, y * TestHelper.Scale + TestHelper.Scale / 2f), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + ctx.DrawText(textOptions, Array.IndexOf(path.ToArray(), (x, y)).ToString(), Color.Black); + listPixel.Add(color.ToPixel()); + } } - } - TestHelper.VerifyFile("jump-point-search-pathfinder.png", bitmap, listPixel, "Jump Point Search Pathfinder (break at walls)"); + }); + + TestHelper.VerifyFile("jump-point-search-pathfinder.png", image, listPixel, "Jump Point Search Pathfinder (break at walls)"); } } } diff --git a/test/NosCore.PathFinder.Tests/NosCore.PathFinder.Tests.csproj b/test/NosCore.PathFinder.Tests/NosCore.PathFinder.Tests.csproj index 066585c..f4343cb 100644 --- a/test/NosCore.PathFinder.Tests/NosCore.PathFinder.Tests.csproj +++ b/test/NosCore.PathFinder.Tests/NosCore.PathFinder.Tests.csproj @@ -1,19 +1,21 @@  - net7.0 + net10.0 enable false - - - - - - - + + + + + + + + + diff --git a/test/NosCore.PathFinder.Tests/TestHelper.cs b/test/NosCore.PathFinder.Tests/TestHelper.cs index f638f30..6d8c103 100644 --- a/test/NosCore.PathFinder.Tests/TestHelper.cs +++ b/test/NosCore.PathFinder.Tests/TestHelper.cs @@ -1,33 +1,37 @@ -// __ _ __ __ ___ __ ___ ___ +// __ _ __ __ ___ __ ___ ___ // | \| |/__\ /' _/ / _//__\| _ \ __| // | | ' | \/ |`._`.| \_| \/ | v / _| // |_|\__|\__/ |___/ \__/\__/|_|_\___| // ----------------------------------- +using System; using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Security.Cryptography; using System.Text; using ApprovalTests; using ApprovalTests.Writers; +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace NosCore.PathFinder.Tests { public static class TestHelper { - public static void VerifyFile(string linearPathfinderPng, Bitmap bitmap, List listPixel, string desc) + public static void VerifyFile(string linearPathfinderPng, Image image, List listPixel, string desc) { var filepath = Path.GetFullPath($"../../../../../documentation/{linearPathfinderPng}"); - bitmap.Save(filepath, ImageFormat.Png); + image.SaveAsPng(filepath); var builder = new StringBuilder(); builder.AppendLine("# NosCore.Pathfinder's Documentation"); builder.AppendLine($"## {desc}"); builder.AppendLine($"- Filename: {linearPathfinderPng}"); - var pixels = string.Join("", listPixel.SelectMany(s => s.Name)); + var pixels = string.Join("", listPixel.SelectMany(s => $"{s.R:X2}{s.G:X2}{s.B:X2}{s.A:X2}")); var checksum = string.Join("", SHA256.Create() @@ -37,13 +41,32 @@ public static void VerifyFile(string linearPathfinderPng, Bitmap bitmap, List listPixel, Bitmap bitmap, (short X, short Y) monster, (short X, short Y) character) + public static void DrawMap(TestMap map, int scale, List listPixel, Image image, (short X, short Y) monster, (short X, short Y) character) { - using var graphics = Graphics.FromImage(bitmap); - for (short y = 0; y < map.Height; y++) + var font = GetFont(); + image.Mutate(ctx => { - for (short x = 0; x < map.Width; x++) + for (short y = 0; y < map.Height; y++) { - var rectangle = new Rectangle(x * scale, y * scale, scale, scale); - var color = Color.Blue; - string? text = null; - if (!map.IsWalkable(x, y)) + for (short x = 0; x < map.Width; x++) { - color = Color.Black; - } + var rect = new RectangleF(x * scale, y * scale, scale, scale); + var color = Color.Blue; + string? text = null; + if (!map.IsWalkable(x, y)) + { + color = Color.Black; + } - if (character == (x, y)) - { - text = "P"; - color = Color.Green; - } + if (character == (x, y)) + { + text = "P"; + color = Color.Green; + } - if (monster != default && monster == (x, y)) - { - text = "M"; - color = Color.DarkRed; - } - graphics.FillRectangle(new Pen(color).Brush, rectangle); - graphics.DrawString(text, new Font("Arial", 16), Brushes.Black, rectangle, StringFormat); + if (monster != default && monster == (x, y)) + { + text = "M"; + color = Color.DarkRed; + } + ctx.Fill(color, rect); + if (text != null && font != null) + { + var textOptions = new RichTextOptions(font) + { + Origin = new PointF(x * scale + scale / 2f, y * scale + scale / 2f), + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + ctx.DrawText(textOptions, text, Color.Black); + } - listPixel.Add(color); + listPixel.Add(color.ToPixel()); + } } - } + }); + } + + public static void DrawArrow(Image image, int cellX, int cellY, float dirX, float dirY, int scale, Rgba32 color) + { + var centerX = cellX * scale + scale / 2f; + var centerY = cellY * scale + scale / 2f; + + var arrowLength = scale * 0.35f; + var headLength = scale * 0.15f; + + var endX = centerX + dirX * arrowLength; + var endY = centerY + dirY * arrowLength; + + image.Mutate(ctx => + { + ctx.DrawLine(color, 2f, new PointF(centerX, centerY), new PointF(endX, endY)); + + var angle = (float)Math.Atan2(dirY, dirX); + var head1X = endX - headLength * (float)Math.Cos(angle - 0.5f); + var head1Y = endY - headLength * (float)Math.Sin(angle - 0.5f); + var head2X = endX - headLength * (float)Math.Cos(angle + 0.5f); + var head2Y = endY - headLength * (float)Math.Sin(angle + 0.5f); + + var headPoints = new PointF[] { new(endX, endY), new(head1X, head1Y), new(head2X, head2Y) }; + ctx.FillPolygon(color, headPoints); + }); } } }