style: primary constructors for position state types
Use C# primary constructors and camelCase private fields per csharp-style. Refactor InMemoryPositionStateStore seeding into CreateInitialMap; align Postgres integration tests and PositionStateApiTests with factory/httpClient naming.pull/15/head
parent
e2d64a7a57
commit
d20b3931af
|
|
@ -9,7 +9,7 @@ namespace NeonSprawl.Server.Tests.Game.PositionState;
|
|||
public class PositionStateApiTests(InMemoryWebApplicationFactory factory)
|
||||
: IClassFixture<InMemoryWebApplicationFactory>
|
||||
{
|
||||
private readonly HttpClient _client = factory.CreateClient();
|
||||
private readonly HttpClient httpClient = factory.CreateClient();
|
||||
|
||||
[Fact]
|
||||
public async Task GetPosition_ShouldReturnVersionedPayloadAtOrigin_WhenDevPlayerRequested()
|
||||
|
|
@ -18,7 +18,7 @@ public class PositionStateApiTests(InMemoryWebApplicationFactory factory)
|
|||
const string url = "/game/players/dev-local-1/position";
|
||||
|
||||
// Act
|
||||
var response = await _client.GetAsync(url);
|
||||
var response = await httpClient.GetAsync(url);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
|
@ -39,7 +39,7 @@ public class PositionStateApiTests(InMemoryWebApplicationFactory factory)
|
|||
const string url = "/game/players/DEV-LOCAL-1/position";
|
||||
|
||||
// Act
|
||||
var response = await _client.GetAsync(url);
|
||||
var response = await httpClient.GetAsync(url);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
|
|
@ -57,7 +57,7 @@ public class PositionStateApiTests(InMemoryWebApplicationFactory factory)
|
|||
const string url = "/game/players/unknown-player/position";
|
||||
|
||||
// Act
|
||||
var response = await _client.GetAsync(url);
|
||||
var response = await httpClient.GetAsync(url);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
|
|
|
|||
|
|
@ -10,21 +10,14 @@ namespace NeonSprawl.Server.Tests.Game.PositionState;
|
|||
|
||||
/// <summary>NS-17: real PostgreSQL + HTTP; collection serializes tests that share the database.</summary>
|
||||
[Collection("Postgres integration")]
|
||||
public sealed class PostgresPositionStateIntegrationTests
|
||||
public sealed class PostgresPositionStateIntegrationTests(PostgresWebApplicationFactory factory)
|
||||
{
|
||||
private readonly PostgresWebApplicationFactory _factory;
|
||||
|
||||
public PostgresPositionStateIntegrationTests(PostgresWebApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[RequirePostgresFact]
|
||||
public async Task PostMove_ThenGet_ShouldReflectPersistedPositionAndSequence()
|
||||
{
|
||||
// Arrange
|
||||
await ResetPlayerPositionTableAsync();
|
||||
using var client = _factory.CreateClient();
|
||||
using var client = factory.CreateClient();
|
||||
var cmd = new MoveCommandRequest
|
||||
{
|
||||
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
|
||||
|
|
@ -68,7 +61,7 @@ public sealed class PostgresPositionStateIntegrationTests
|
|||
PositionStateResponse? persisted = null;
|
||||
|
||||
// Act
|
||||
using (var first = _factory.CreateClient())
|
||||
using (var first = factory.CreateClient())
|
||||
{
|
||||
var post = await first.PostAsJsonAsync("/game/players/dev-local-1/move", cmd);
|
||||
postStatus = post.StatusCode;
|
||||
|
|
@ -94,7 +87,7 @@ public sealed class PostgresPositionStateIntegrationTests
|
|||
{
|
||||
// Arrange
|
||||
await ResetPlayerPositionTableAsync();
|
||||
using var client = _factory.CreateClient();
|
||||
using var client = factory.CreateClient();
|
||||
var cmd = new MoveCommandRequest
|
||||
{
|
||||
SchemaVersion = MoveCommandRequest.CurrentSchemaVersion,
|
||||
|
|
@ -117,8 +110,8 @@ public sealed class PostgresPositionStateIntegrationTests
|
|||
throw new InvalidOperationException("ConnectionStrings__NeonSprawl is not set.");
|
||||
}
|
||||
|
||||
_ = _factory.Services;
|
||||
var options = _factory.Services.GetRequiredService<IOptions<GamePositionOptions>>().Value;
|
||||
_ = factory.Services;
|
||||
var options = factory.Services.GetRequiredService<IOptions<GamePositionOptions>>().Value;
|
||||
|
||||
var ddlPath = Path.Combine(AppContext.BaseDirectory, "db", "migrations", "V001__player_position.sql");
|
||||
if (!File.Exists(ddlPath))
|
||||
|
|
|
|||
|
|
@ -4,21 +4,22 @@ using Microsoft.Extensions.Options;
|
|||
namespace NeonSprawl.Server.Game.PositionState;
|
||||
|
||||
/// <summary>Thread-safe in-memory positions; seeds the configured dev player on construction.</summary>
|
||||
public sealed class InMemoryPositionStateStore : IPositionStateStore
|
||||
public sealed class InMemoryPositionStateStore(IOptions<GamePositionOptions> options) : IPositionStateStore
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, PositionSnapshot> _positions = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ConcurrentDictionary<string, PositionSnapshot> positions = CreateInitialMap(options.Value);
|
||||
|
||||
public InMemoryPositionStateStore(IOptions<GamePositionOptions> options)
|
||||
private static ConcurrentDictionary<string, PositionSnapshot> CreateInitialMap(GamePositionOptions o)
|
||||
{
|
||||
var o = options.Value;
|
||||
var id = o.DevPlayerId.Trim();
|
||||
if (id.Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Game:DevPlayerId must be non-empty.");
|
||||
}
|
||||
|
||||
var map = new ConcurrentDictionary<string, PositionSnapshot>(StringComparer.OrdinalIgnoreCase);
|
||||
var p = o.DefaultPosition ?? new DefaultPositionOptions();
|
||||
_positions[id] = new PositionSnapshot(p.X, p.Y, p.Z, 0);
|
||||
map[id] = new PositionSnapshot(p.X, p.Y, p.Z, 0);
|
||||
return map;
|
||||
}
|
||||
|
||||
public bool TryGetPosition(string playerId, out PositionSnapshot snapshot)
|
||||
|
|
@ -26,11 +27,11 @@ public sealed class InMemoryPositionStateStore : IPositionStateStore
|
|||
var key = playerId?.Trim();
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
return _positions.TryGetValue(key, out snapshot);
|
||||
return positions.TryGetValue(key, out snapshot);
|
||||
}
|
||||
|
||||
snapshot = default;
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
public bool TryApplyMoveTarget(string playerId, double x, double y, double z, out PositionSnapshot snapshot)
|
||||
|
|
@ -44,17 +45,18 @@ public sealed class InMemoryPositionStateStore : IPositionStateStore
|
|||
|
||||
while (true)
|
||||
{
|
||||
if (!_positions.TryGetValue(key, out var previous))
|
||||
if (!positions.TryGetValue(key, out var previous))
|
||||
{
|
||||
snapshot = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var next = new PositionSnapshot(x, y, z, previous.Sequence + 1);
|
||||
if (!_positions.TryUpdate(key, next, previous))
|
||||
if (!positions.TryUpdate(key, next, previous))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
snapshot = next;
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,29 +3,26 @@ using Microsoft.Extensions.Options;
|
|||
namespace NeonSprawl.Server.Game.PositionState;
|
||||
|
||||
/// <summary>Runs DDL and dev-player seed once when the host starts (PostgreSQL path only).</summary>
|
||||
internal sealed class PostgresDevPlayerSeedHostedService : IHostedService
|
||||
internal sealed class PostgresDevPlayerSeedHostedService(
|
||||
Npgsql.NpgsqlDataSource dataSource,
|
||||
IOptions<GamePositionOptions> positionOptions) : IHostedService
|
||||
{
|
||||
private readonly Npgsql.NpgsqlDataSource _dataSource;
|
||||
private readonly GamePositionOptions _options;
|
||||
private readonly GamePositionOptions gameOptions = RequireDevPlayer(positionOptions.Value);
|
||||
|
||||
public PostgresDevPlayerSeedHostedService(
|
||||
Npgsql.NpgsqlDataSource dataSource,
|
||||
IOptions<GamePositionOptions> options)
|
||||
private static GamePositionOptions RequireDevPlayer(GamePositionOptions options)
|
||||
{
|
||||
_dataSource = dataSource;
|
||||
var o = options.Value;
|
||||
if (string.IsNullOrWhiteSpace(o.DevPlayerId))
|
||||
if (string.IsNullOrWhiteSpace(options.DevPlayerId))
|
||||
{
|
||||
throw new InvalidOperationException("Game:DevPlayerId must be non-empty.");
|
||||
}
|
||||
|
||||
_options = o;
|
||||
return options;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
PostgresPositionBootstrap.EnsureSchema(_dataSource);
|
||||
PostgresPositionBootstrap.SeedDevPlayer(_dataSource, _options);
|
||||
PostgresPositionBootstrap.EnsureSchema(dataSource);
|
||||
PostgresPositionBootstrap.SeedDevPlayer(dataSource, gameOptions);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,20 +3,20 @@ using Microsoft.Extensions.Options;
|
|||
namespace NeonSprawl.Server.Game.PositionState;
|
||||
|
||||
/// <summary>PostgreSQL-backed <see cref="IPositionStateStore"/> (NS-17). Player ids are stored normalized: invariant lower + trim for case-insensitive parity with <see cref="InMemoryPositionStateStore"/>.</summary>
|
||||
public sealed class PostgresPositionStateStore : IPositionStateStore
|
||||
public sealed class PostgresPositionStateStore(
|
||||
Npgsql.NpgsqlDataSource dataSource,
|
||||
IOptions<GamePositionOptions> positionOptions) : IPositionStateStore
|
||||
{
|
||||
private readonly Npgsql.NpgsqlDataSource _dataSource;
|
||||
private readonly GamePositionOptions _options;
|
||||
private readonly GamePositionOptions gameOptions = RequireValidOptions(positionOptions.Value);
|
||||
|
||||
public PostgresPositionStateStore(Npgsql.NpgsqlDataSource dataSource, IOptions<GamePositionOptions> options)
|
||||
private static GamePositionOptions RequireValidOptions(GamePositionOptions options)
|
||||
{
|
||||
_dataSource = dataSource;
|
||||
_options = options.Value;
|
||||
var id = _options.DevPlayerId.Trim();
|
||||
if (id.Length == 0)
|
||||
if (options.DevPlayerId.Trim().Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Game:DevPlayerId must be non-empty.");
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public bool TryGetPosition(string playerId, out PositionSnapshot snapshot)
|
||||
|
|
@ -28,9 +28,9 @@ public sealed class PostgresPositionStateStore : IPositionStateStore
|
|||
return false;
|
||||
}
|
||||
|
||||
PostgresPositionBootstrap.EnsureSchema(_dataSource);
|
||||
PostgresPositionBootstrap.EnsureSchema(dataSource);
|
||||
|
||||
using var conn = _dataSource.OpenConnection();
|
||||
using var conn = dataSource.OpenConnection();
|
||||
using var cmd = new Npgsql.NpgsqlCommand(
|
||||
"SELECT pos_x, pos_y, pos_z, sequence FROM player_position WHERE player_id = @pid;",
|
||||
conn);
|
||||
|
|
@ -55,9 +55,9 @@ public sealed class PostgresPositionStateStore : IPositionStateStore
|
|||
return false;
|
||||
}
|
||||
|
||||
PostgresPositionBootstrap.EnsureSchema(_dataSource);
|
||||
PostgresPositionBootstrap.EnsureSchema(dataSource);
|
||||
|
||||
using var conn = _dataSource.OpenConnection();
|
||||
using var conn = dataSource.OpenConnection();
|
||||
using var cmd = new Npgsql.NpgsqlCommand(
|
||||
"""
|
||||
UPDATE player_position
|
||||
|
|
|
|||
Loading…
Reference in New Issue