Enhance README with updated instructions for world generation and menu navigation; implement world capacity management in the API with new configuration options; improve client interface with a full-screen menu and world list display; add theme toggle functionality and refine styling for better user experience.
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\TheLivingWorld.Core\TheLivingWorld.Core.csproj" />
|
||||
<ProjectReference Include="..\..\src\TheLivingWorld.Osm\TheLivingWorld.Osm.csproj" />
|
||||
<ProjectReference Include="..\..\src\TheLivingWorld.Api\TheLivingWorld.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,113 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TheLivingWorld.Api.Generation;
|
||||
using TheLivingWorld.Api.Storage;
|
||||
using TheLivingWorld.Core.Contracts;
|
||||
using TheLivingWorld.Core.Export;
|
||||
using TheLivingWorld.Osm;
|
||||
using TheLivingWorld.Osm.Import;
|
||||
using TheLivingWorld.Osm.Overpass;
|
||||
|
||||
namespace TheLivingWorld.Tests;
|
||||
|
||||
public sealed class WorldGenerationServiceTests : IDisposable
|
||||
{
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), $"tlw-gen-{Guid.NewGuid():n}");
|
||||
private readonly WorldStore _store;
|
||||
|
||||
public WorldGenerationServiceTests()
|
||||
{
|
||||
_store = new WorldStore(
|
||||
Options.Create(new WorldStorageOptions { RootDirectory = _root }),
|
||||
NullLogger<WorldStore>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_rejects_creation_when_the_slot_budget_is_full()
|
||||
{
|
||||
await _store.SaveSummaryAsync(Summary("taken-aaaaaaaa", "Taken"));
|
||||
|
||||
using var service = CreateService(maxConcurrentWorlds: 1);
|
||||
|
||||
var failure = await Assert.ThrowsAsync<WorldCapacityExceededException>(() =>
|
||||
service.StartAsync(new CreateWorldRequest
|
||||
{
|
||||
Name = "Overflow",
|
||||
Latitude = 31.8966010,
|
||||
Longitude = -100.4858591,
|
||||
SizeKm = 10,
|
||||
}, CancellationToken.None));
|
||||
|
||||
Assert.Equal(1, failure.MaxConcurrentWorlds);
|
||||
Assert.Equal(1, await _store.CountAsync());
|
||||
Assert.Equal("Taken", Assert.Single(await _store.ListAsync()).Name);
|
||||
}
|
||||
|
||||
private WorldGenerationService CreateService(int maxConcurrentWorlds)
|
||||
{
|
||||
// The capacity check runs before any Overpass work, so this generator is never invoked by the
|
||||
// rejection test. The acceptance test only asserts the pending summary was written.
|
||||
var generator = new OsmWorldGenerator(
|
||||
new OverpassClient(
|
||||
new HttpClient(new UnreachableHandler()),
|
||||
Options.Create(new OsmOptions
|
||||
{
|
||||
Endpoints = ["https://unreachable.example/api"],
|
||||
CacheDirectory = Path.Combine(_root, "osm-cache"),
|
||||
MaxAttemptsPerEndpoint = 1,
|
||||
RequestTimeoutSeconds = 1,
|
||||
}),
|
||||
NullLogger<OverpassClient>.Instance),
|
||||
new OsmWorldBuilder(NullLogger<OsmWorldBuilder>.Instance),
|
||||
NullLogger<OsmWorldGenerator>.Instance);
|
||||
|
||||
return new WorldGenerationService(
|
||||
generator,
|
||||
_store,
|
||||
new ChunkExporter(),
|
||||
Options.Create(new WorldStorageOptions
|
||||
{
|
||||
RootDirectory = _root,
|
||||
MaxConcurrentWorlds = maxConcurrentWorlds,
|
||||
}),
|
||||
new NeverStoppingLifetime(),
|
||||
NullLogger<WorldGenerationService>.Instance);
|
||||
}
|
||||
|
||||
private static WorldSummaryDto Summary(string id, string name) => new()
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
Latitude = 31.9,
|
||||
Longitude = -100.5,
|
||||
SizeMeters = 10_000,
|
||||
Status = WorldStatus.Ready,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_root)) Directory.Delete(_root, recursive: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private sealed class NeverStoppingLifetime : IHostApplicationLifetime
|
||||
{
|
||||
public CancellationToken ApplicationStarted => CancellationToken.None;
|
||||
public CancellationToken ApplicationStopped => CancellationToken.None;
|
||||
public CancellationToken ApplicationStopping => CancellationToken.None;
|
||||
|
||||
public void StopApplication()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class UnreachableHandler : HttpMessageHandler
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken) =>
|
||||
throw new InvalidOperationException("Overpass must not be contacted by these tests.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TheLivingWorld.Api.Storage;
|
||||
using TheLivingWorld.Core.Contracts;
|
||||
|
||||
namespace TheLivingWorld.Tests;
|
||||
|
||||
public sealed class WorldStoreTests : IDisposable
|
||||
{
|
||||
private readonly string _root = Path.Combine(Path.GetTempPath(), $"tlw-worlds-{Guid.NewGuid():n}");
|
||||
private readonly WorldStore _store;
|
||||
|
||||
public WorldStoreTests()
|
||||
{
|
||||
_store = new WorldStore(
|
||||
Options.Create(new WorldStorageOptions { RootDirectory = _root }),
|
||||
NullLogger<WorldStore>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CountAsync_is_zero_when_the_root_is_missing()
|
||||
{
|
||||
Assert.Equal(0, await _store.CountAsync());
|
||||
Assert.Empty(await _store.ListAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CountAsync_and_ListAsync_track_saved_summaries()
|
||||
{
|
||||
await _store.SaveSummaryAsync(Summary("alpha-11111111", "Alpha"));
|
||||
await _store.SaveSummaryAsync(Summary("bravo-22222222", "Bravo"));
|
||||
|
||||
Assert.Equal(2, await _store.CountAsync());
|
||||
|
||||
var listed = await _store.ListAsync();
|
||||
Assert.Equal(2, listed.Count);
|
||||
Assert.Equal(["Bravo", "Alpha"], listed.Select(world => world.Name).ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CountAsync_ignores_directories_without_readable_state()
|
||||
{
|
||||
await _store.SaveSummaryAsync(Summary("alpha-11111111", "Alpha"));
|
||||
Directory.CreateDirectory(Path.Combine(_root, "not-a-valid-id!!!"));
|
||||
Directory.CreateDirectory(Path.Combine(_root, "orphan-33333333"));
|
||||
|
||||
Assert.Equal(1, await _store.CountAsync());
|
||||
}
|
||||
|
||||
private static WorldSummaryDto Summary(string id, string name) => new()
|
||||
{
|
||||
Id = id,
|
||||
Name = name,
|
||||
Latitude = 31.9,
|
||||
Longitude = -100.5,
|
||||
SizeMeters = 10_000,
|
||||
Status = WorldStatus.Ready,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(_root)) Directory.Delete(_root, recursive: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user