NEO-146: apply C# collection and using style fixes

Replace .ToArray() with collection spread, simplify single-statement
await using blocks, use Lock for Postgres schema gates, and document
IDE0063/IDE0305/IDE0330/CA1859 conventions in csharp-style.md.
pull/187/head
VinPropane 2026-06-24 20:41:25 -04:00
parent 1922900477
commit b455ff5ad5
59 changed files with 329 additions and 368 deletions

View File

@ -54,7 +54,61 @@ if (string.IsNullOrEmpty(key))
- **File-scoped namespaces** (`namespace X;`) for new single-namespace files when the SDK/version allows.
- **Pattern matching / nullability:** prefer modern C# features where they simplify code; honor **nullable reference type** annotations when the project enables them.
## Members
## Collection expressions (IDE0305)
- Prefer **collection spread** (`[.. source]`) over **`.ToArray()`** / **`.ToList()`** when materializing a sequence into a new array or list for return values, record `with` copies, or local snapshots.
- Give an **explicit target type** when the compiler cannot infer one (CS9176), e.g. `string[] ids = [.. query.Select(static x => x.Id)];`.
- In **assertions**, avoid passing a bare spread directly to APIs with span overloads (e.g. `Assert.Equal`) — assign to a typed local first:
```csharp
// Prefer
return [.. query];
GrantedItems = [.. row.GrantedItems],
// When type inference fails
string[] expectedOrder = [.. registry.GetDefinitionsInIdOrder().Select(static d => d.Id)];
string[] actualOrder = [.. body.Quests.Select(static row => row.QuestId)];
Assert.Equal(expectedOrder, actualOrder);
// Avoid (ambiguous Assert overloads; unnecessary allocation helper)
Assert.Equal(expectedOrder, [.. body.Quests.Select(static row => row.QuestId)]);
return query.ToArray();
```
## `using` / `await using` (IDE0063)
- When a **`using` or `await using` block contains exactly one statement**, prefer the declaration form without braces: `await using var conn = …;` then the single statement on following lines **only if** it is still logically one resource scope (typical for `NpgsqlConnection` + one command in integration-test reset helpers).
- **Keep braced `using (var …) { … }`** when the block has **multiple statements** (Arrange + Act + Assert inside one scope, seed loops, or any sequence that shares locals).
- **Do not flatten** multi-statement blocks into `using var` at method scope when inner scopes reuse names like `store`, `registry`, or `scope` — that causes **CS0136** name clashes. Use distinct names (`seedStore`, `actScope`, `readStore`) or keep the braced block.
```csharp
// Prefer — single disposable, single follow-on statement
await using var conn = new NpgsqlConnection(cs);
await conn.OpenAsync();
// Prefer — multiple statements in one scope
using (var firstScope = Factory.Services.CreateScope())
{
var store = firstScope.ServiceProvider.GetRequiredService<IExampleStore>();
Assert.True(store.TryAppend(row));
}
// Avoid — flattens to method scope and collides with a later `var store`
using var firstScope = Factory.Services.CreateScope();
var store = firstScope.ServiceProvider.GetRequiredService<IExampleStore>();
Assert.True(store.TryAppend(row));
// … later …
var store = secondScope.ServiceProvider.GetRequiredService<IExampleStore>(); // CS0136
```
## Postgres schema gates (IDE0330)
- In `*Bootstrap.cs` types, use **`System.Threading.Lock`** (not `object`) for static schema-initialization gates: `private static readonly Lock SchemaGate = new();`.
## Concrete return types (CA1859)
- When a private helper always returns a materialized array, prefer the **concrete array type** as the return type (e.g. `RewardItemGrantApplied[]`) instead of `IReadOnlyList<T>` if callers only need the array and analyzers suggest it.
- Prefer **expression-bodied** members only when they stay one clear idea.
- **LINQ:** favor readability over micro-chains; break complex queries across lines.

View File

@ -37,13 +37,12 @@ public sealed class HotbarLoadoutPersistenceIntegrationTests(PostgresIntegration
HttpStatusCode postStatus;
// Act
using (var firstClient = Factory.CreateClient())
{
var post = await firstClient.PostAsJsonAsync(
"/game/players/dev-local-1/hotbar-loadout",
update);
postStatus = post.StatusCode;
}
using var firstClient = Factory.CreateClient();
var post = await firstClient.PostAsJsonAsync(
"/game/players/dev-local-1/hotbar-loadout",
update);
postStatus = post.StatusCode;
// Assert
await using var secondFactory = new PostgresWebApplicationFactory();
@ -73,14 +72,10 @@ public sealed class HotbarLoadoutPersistenceIntegrationTests(PostgresIntegration
var ddl = await File.ReadAllTextAsync(ddlPath);
await using var conn = new NpgsqlConnection(cs);
await conn.OpenAsync();
await using (var apply = new NpgsqlCommand(ddl, conn))
{
await apply.ExecuteNonQueryAsync();
}
await using var apply = new NpgsqlCommand(ddl, conn);
await apply.ExecuteNonQueryAsync();
await using (var truncate = new NpgsqlCommand("TRUNCATE player_hotbar_loadout;", conn))
{
await truncate.ExecuteNonQueryAsync();
}
await using var truncate = new NpgsqlCommand("TRUNCATE player_hotbar_loadout;", conn);
await truncate.ExecuteNonQueryAsync();
}
}

View File

@ -138,14 +138,12 @@ public sealed class ContractInstancePersistenceIntegrationTests(PostgresIntegrat
}
ContractInstanceState readBack;
await using (var secondFactory = new PostgresWebApplicationFactory())
{
using var secondScope = secondFactory.Services.CreateScope();
var store = secondScope.ServiceProvider.GetRequiredService<IContractInstanceStore>();
readBack = store.TryGet(PlayerId, InstanceId, out var snapshot)
? snapshot
: null!;
}
await using var secondFactory = new PostgresWebApplicationFactory();
using var secondScope = secondFactory.Services.CreateScope();
var readStore = secondScope.ServiceProvider.GetRequiredService<IContractInstanceStore>();
readBack = readStore.TryGet(PlayerId, InstanceId, out var snapshot)
? snapshot
: null!;
// Assert
Assert.NotNull(readBack);
@ -176,21 +174,15 @@ public sealed class ContractInstancePersistenceIntegrationTests(PostgresIntegrat
var instanceDdl = await File.ReadAllTextAsync(instanceDdlPath);
await using var conn = new NpgsqlConnection(cs);
await conn.OpenAsync();
await using (var applyPosition = new NpgsqlCommand(positionDdl, conn))
{
await applyPosition.ExecuteNonQueryAsync();
}
await using var applyPosition = new NpgsqlCommand(positionDdl, conn);
await applyPosition.ExecuteNonQueryAsync();
await using (var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn))
{
await truncate.ExecuteNonQueryAsync();
}
await using var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn);
await truncate.ExecuteNonQueryAsync();
PostgresPositionBootstrap.SeedDevPlayer(conn, options);
await using (var applyInstance = new NpgsqlCommand(instanceDdl, conn))
{
await applyInstance.ExecuteNonQueryAsync();
}
await using var applyInstance = new NpgsqlCommand(instanceDdl, conn);
await applyInstance.ExecuteNonQueryAsync();
}
}

View File

@ -54,14 +54,12 @@ public sealed class ContractOutcomePersistenceIntegrationTests(PostgresIntegrati
}
ContractOutcomeRow readBack;
await using (var secondFactory = new PostgresWebApplicationFactory())
{
using var secondScope = secondFactory.Services.CreateScope();
var outcomeStore = secondScope.ServiceProvider.GetRequiredService<IContractOutcomeStore>();
readBack = outcomeStore.TryGetByIdempotencyKey(idempotencyKey, out var row)
? row
: null!;
}
await using var secondFactory = new PostgresWebApplicationFactory();
using var secondScope = secondFactory.Services.CreateScope();
var readOutcomeStore = secondScope.ServiceProvider.GetRequiredService<IContractOutcomeStore>();
readBack = readOutcomeStore.TryGetByIdempotencyKey(idempotencyKey, out var row)
? row
: null!;
// Assert
Assert.NotNull(readBack);
@ -125,11 +123,10 @@ public sealed class ContractOutcomePersistenceIntegrationTests(PostgresIntegrati
RecordedAt);
// Act
bool appended;
using (var scope = Factory.Services.CreateScope())
{
var outcomeStore = scope.ServiceProvider.GetRequiredService<IContractOutcomeStore>();
appended = outcomeStore.TryAppend(outcomeRow);
}
using var scope = Factory.Services.CreateScope();
var outcomeStore = scope.ServiceProvider.GetRequiredService<IContractOutcomeStore>();
appended = outcomeStore.TryAppend(outcomeRow);
// Assert
Assert.False(appended);
}
@ -158,26 +155,18 @@ public sealed class ContractOutcomePersistenceIntegrationTests(PostgresIntegrati
var outcomeDdl = await File.ReadAllTextAsync(outcomeDdlPath);
await using var conn = new NpgsqlConnection(cs);
await conn.OpenAsync();
await using (var applyPosition = new NpgsqlCommand(positionDdl, conn))
{
await applyPosition.ExecuteNonQueryAsync();
}
await using var applyPosition = new NpgsqlCommand(positionDdl, conn);
await applyPosition.ExecuteNonQueryAsync();
await using (var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn))
{
await truncate.ExecuteNonQueryAsync();
}
await using var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn);
await truncate.ExecuteNonQueryAsync();
PostgresPositionBootstrap.SeedDevPlayer(conn, options);
await using (var applyInstance = new NpgsqlCommand(instanceDdl, conn))
{
await applyInstance.ExecuteNonQueryAsync();
}
await using var applyInstance = new NpgsqlCommand(instanceDdl, conn);
await applyInstance.ExecuteNonQueryAsync();
await using (var applyOutcome = new NpgsqlCommand(outcomeDdl, conn))
{
await applyOutcome.ExecuteNonQueryAsync();
}
await using var applyOutcome = new NpgsqlCommand(outcomeDdl, conn);
await applyOutcome.ExecuteNonQueryAsync();
}
}

View File

@ -31,12 +31,10 @@ public sealed class FactionStandingPersistenceIntegrationTests(PostgresIntegrati
}
FactionStandingReadOutcome readBack;
await using (var secondFactory = new PostgresWebApplicationFactory())
{
using var secondScope = secondFactory.Services.CreateScope();
var store = secondScope.ServiceProvider.GetRequiredService<IFactionStandingStore>();
readBack = store.TryGetStanding(PlayerId, GridFactionId);
}
await using var secondFactory = new PostgresWebApplicationFactory();
using var secondScope = secondFactory.Services.CreateScope();
var readStore = secondScope.ServiceProvider.GetRequiredService<IFactionStandingStore>();
readBack = readStore.TryGetStanding(PlayerId, GridFactionId);
// Assert
Assert.True(writeOutcome.Success);
@ -69,11 +67,10 @@ public sealed class FactionStandingPersistenceIntegrationTests(PostgresIntegrati
// Act
FactionStandingReadOutcome readBack;
using (var scope = Factory.Services.CreateScope())
{
var store = scope.ServiceProvider.GetRequiredService<IFactionStandingStore>();
readBack = store.TryGetStanding(PlayerId, GridFactionId);
}
using var scope = Factory.Services.CreateScope();
var store = scope.ServiceProvider.GetRequiredService<IFactionStandingStore>();
readBack = store.TryGetStanding(PlayerId, GridFactionId);
// Assert
Assert.True(readBack.Success);
@ -102,21 +99,15 @@ public sealed class FactionStandingPersistenceIntegrationTests(PostgresIntegrati
var standingDdl = await File.ReadAllTextAsync(standingDdlPath);
await using var conn = new NpgsqlConnection(cs);
await conn.OpenAsync();
await using (var applyPosition = new NpgsqlCommand(positionDdl, conn))
{
await applyPosition.ExecuteNonQueryAsync();
}
await using var applyPosition = new NpgsqlCommand(positionDdl, conn);
await applyPosition.ExecuteNonQueryAsync();
await using (var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn))
{
await truncate.ExecuteNonQueryAsync();
}
await using var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn);
await truncate.ExecuteNonQueryAsync();
PostgresPositionBootstrap.SeedDevPlayer(conn, options);
await using (var applyStanding = new NpgsqlCommand(standingDdl, conn))
{
await applyStanding.ExecuteNonQueryAsync();
}
await using var applyStanding = new NpgsqlCommand(standingDdl, conn);
await applyStanding.ExecuteNonQueryAsync();
}
}

View File

@ -85,7 +85,7 @@ public sealed class InMemoryReputationDeltaStoreTests
// Act
var rows = store.GetDeltasForPlayer(PlayerId);
// Assert
Assert.Equal(["delta-a", "delta-m", "delta-z"], rows.Select(static r => r.Id).ToArray());
Assert.Equal(["delta-a", "delta-m", "delta-z"], [.. rows.Select(static r => r.Id)]);
}
[Fact]

View File

@ -40,12 +40,10 @@ public sealed class ReputationDeltaPersistenceIntegrationTests(PostgresIntegrati
}
IReadOnlyList<ReputationDeltaRow> readBack;
await using (var secondFactory = new PostgresWebApplicationFactory())
{
using var secondScope = secondFactory.Services.CreateScope();
var store = secondScope.ServiceProvider.GetRequiredService<IReputationDeltaStore>();
readBack = store.GetDeltasForPlayer(PlayerId);
}
await using var secondFactory = new PostgresWebApplicationFactory();
using var secondScope = secondFactory.Services.CreateScope();
var readStore = secondScope.ServiceProvider.GetRequiredService<IReputationDeltaStore>();
readBack = readStore.GetDeltasForPlayer(PlayerId);
// Assert
var persisted = Assert.Single(readBack);
@ -77,21 +75,15 @@ public sealed class ReputationDeltaPersistenceIntegrationTests(PostgresIntegrati
var auditDdl = await File.ReadAllTextAsync(auditDdlPath);
await using var conn = new NpgsqlConnection(cs);
await conn.OpenAsync();
await using (var applyPosition = new NpgsqlCommand(positionDdl, conn))
{
await applyPosition.ExecuteNonQueryAsync();
}
await using var applyPosition = new NpgsqlCommand(positionDdl, conn);
await applyPosition.ExecuteNonQueryAsync();
await using (var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn))
{
await truncate.ExecuteNonQueryAsync();
}
await using var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn);
await truncate.ExecuteNonQueryAsync();
PostgresPositionBootstrap.SeedDevPlayer(conn, options);
await using (var applyAudit = new NpgsqlCommand(auditDdl, conn))
{
await applyAudit.ExecuteNonQueryAsync();
}
await using var applyAudit = new NpgsqlCommand(auditDdl, conn);
await applyAudit.ExecuteNonQueryAsync();
}
}

View File

@ -48,11 +48,11 @@ public sealed class ResourceNodeInstancePersistenceIntegrationTests(PostgresInte
await ResetInstanceTableAsync();
using (var seedScope = Factory.Services.CreateScope())
{
var registry = seedScope.ServiceProvider.GetRequiredService<IResourceNodeDefinitionRegistry>();
var store = seedScope.ServiceProvider.GetRequiredService<IResourceNodeInstanceStore>();
var seedRegistry = seedScope.ServiceProvider.GetRequiredService<IResourceNodeDefinitionRegistry>();
var seedStore = seedScope.ServiceProvider.GetRequiredService<IResourceNodeInstanceStore>();
for (var i = 0; i < 10; i++)
{
_ = ResourceNodeInstanceOperations.TryCommitSuccessfulGatherDecrement(AlphaId, registry, store);
_ = ResourceNodeInstanceOperations.TryCommitSuccessfulGatherDecrement(AlphaId, seedRegistry, seedStore);
}
}
@ -60,13 +60,11 @@ public sealed class ResourceNodeInstancePersistenceIntegrationTests(PostgresInte
ResourceNodeInstanceMutationOutcome denyOutcome;
ResourceNodeInstanceSnapshot persisted;
await using var secondFactory = new PostgresWebApplicationFactory();
using (var scope = secondFactory.Services.CreateScope())
{
var registry = scope.ServiceProvider.GetRequiredService<IResourceNodeDefinitionRegistry>();
var store = scope.ServiceProvider.GetRequiredService<IResourceNodeInstanceStore>();
denyOutcome = ResourceNodeInstanceOperations.TryCommitSuccessfulGatherDecrement(AlphaId, registry, store);
store.TryGetRemainingGathers(AlphaId, out persisted);
}
using var actScope = secondFactory.Services.CreateScope();
var actRegistry = actScope.ServiceProvider.GetRequiredService<IResourceNodeDefinitionRegistry>();
var actStore = actScope.ServiceProvider.GetRequiredService<IResourceNodeInstanceStore>();
denyOutcome = ResourceNodeInstanceOperations.TryCommitSuccessfulGatherDecrement(AlphaId, actRegistry, actStore);
actStore.TryGetRemainingGathers(AlphaId, out persisted);
// Assert
Assert.Equal(ResourceNodeInstanceMutationKind.Denied, denyOutcome.Kind);
@ -92,14 +90,10 @@ public sealed class ResourceNodeInstancePersistenceIntegrationTests(PostgresInte
var ddl = await File.ReadAllTextAsync(ddlPath);
await using var conn = new NpgsqlConnection(cs);
await conn.OpenAsync();
await using (var apply = new NpgsqlCommand(ddl, conn))
{
await apply.ExecuteNonQueryAsync();
}
await using var apply = new NpgsqlCommand(ddl, conn);
await apply.ExecuteNonQueryAsync();
await using (var truncate = new NpgsqlCommand("TRUNCATE resource_node_instance;", conn))
{
await truncate.ExecuteNonQueryAsync();
}
await using var truncate = new NpgsqlCommand("TRUNCATE resource_node_instance;", conn);
await truncate.ExecuteNonQueryAsync();
}
}

View File

@ -97,26 +97,18 @@ public sealed class GigProgressionGrantPersistenceIntegrationTests(PostgresInteg
var gigDdl = await File.ReadAllTextAsync(gigDdlPath);
await using var conn = new NpgsqlConnection(cs);
await conn.OpenAsync();
await using (var applyPosition = new NpgsqlCommand(positionDdl, conn))
{
await applyPosition.ExecuteNonQueryAsync();
}
await using var applyPosition = new NpgsqlCommand(positionDdl, conn);
await applyPosition.ExecuteNonQueryAsync();
await using (var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn))
{
await truncate.ExecuteNonQueryAsync();
}
await using var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn);
await truncate.ExecuteNonQueryAsync();
PostgresPositionBootstrap.SeedDevPlayer(conn, options);
await using (var applyGig = new NpgsqlCommand(gigDdl, conn))
{
await applyGig.ExecuteNonQueryAsync();
}
await using var applyGig = new NpgsqlCommand(gigDdl, conn);
await applyGig.ExecuteNonQueryAsync();
await using (var truncateGig = new NpgsqlCommand("TRUNCATE player_gig_progression;", conn))
{
await truncateGig.ExecuteNonQueryAsync();
}
await using var truncateGig = new NpgsqlCommand("TRUNCATE player_gig_progression;", conn);
await truncateGig.ExecuteNonQueryAsync();
}
}

View File

@ -54,31 +54,31 @@ public sealed class PlayerInventoryPersistenceIntegrationTests(PostgresIntegrati
await ResetInventoryTableAsync();
using (var seedScope = Factory.Services.CreateScope())
{
var registry = seedScope.ServiceProvider.GetRequiredService<IItemDefinitionRegistry>();
var store = seedScope.ServiceProvider.GetRequiredService<IPlayerInventoryStore>();
var seedRegistry = seedScope.ServiceProvider.GetRequiredService<IItemDefinitionRegistry>();
var seedStore = seedScope.ServiceProvider.GetRequiredService<IPlayerInventoryStore>();
for (var i = 0; i < PlayerInventorySnapshot.BagSlotCount; i++)
{
_ = PlayerInventoryOperations.TryAddStack(
"dev-local-1",
"survey_drone_kit",
quantity: 1,
registry,
store);
seedRegistry,
seedStore);
}
}
// Act
PlayerInventoryMutationOutcome denyOutcome;
using (var scope = Factory.Services.CreateScope())
using (var actScope = Factory.Services.CreateScope())
{
var registry = scope.ServiceProvider.GetRequiredService<IItemDefinitionRegistry>();
var store = scope.ServiceProvider.GetRequiredService<IPlayerInventoryStore>();
var actRegistry = actScope.ServiceProvider.GetRequiredService<IItemDefinitionRegistry>();
var actStore = actScope.ServiceProvider.GetRequiredService<IPlayerInventoryStore>();
denyOutcome = PlayerInventoryOperations.TryAddStack(
"dev-local-1",
"scrap_metal_bulk",
quantity: 1,
registry,
store);
actRegistry,
actStore);
}
// Act — verify on fresh host
@ -120,27 +120,19 @@ public sealed class PlayerInventoryPersistenceIntegrationTests(PostgresIntegrati
var inventoryDdl = await File.ReadAllTextAsync(inventoryDdlPath);
await using var conn = new NpgsqlConnection(cs);
await conn.OpenAsync();
await using (var applyPosition = new NpgsqlCommand(positionDdl, conn))
{
await applyPosition.ExecuteNonQueryAsync();
}
await using var applyPosition = new NpgsqlCommand(positionDdl, conn);
await applyPosition.ExecuteNonQueryAsync();
await using (var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn))
{
await truncate.ExecuteNonQueryAsync();
}
await using var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn);
await truncate.ExecuteNonQueryAsync();
PostgresPositionBootstrap.SeedDevPlayer(conn, options);
await using (var applyInventory = new NpgsqlCommand(inventoryDdl, conn))
{
await applyInventory.ExecuteNonQueryAsync();
}
await using var applyInventory = new NpgsqlCommand(inventoryDdl, conn);
await applyInventory.ExecuteNonQueryAsync();
await using (var truncateInventory = new NpgsqlCommand("TRUNCATE player_inventory;", conn))
{
await truncateInventory.ExecuteNonQueryAsync();
}
await using var truncateInventory = new NpgsqlCommand("TRUNCATE player_inventory;", conn);
await truncateInventory.ExecuteNonQueryAsync();
}
private static int OccupiedBagSlotCount(PlayerInventorySnapshot snapshot)

View File

@ -89,7 +89,7 @@ public class MasteryCatalogRegistryTests
// Act
var tracks = registry.GetTracksInSkillIdOrder();
// Assert
Assert.Equal(["intrusion", "refine", "salvage"], tracks.Select(t => t.SkillId).ToArray());
Assert.Equal(["intrusion", "refine", "salvage"], [.. tracks.Select(t => t.SkillId)]);
}
[Fact]
@ -111,7 +111,7 @@ public class MasteryCatalogRegistryTests
// Act
var ordered = registry.GetPerksInIdOrder();
// Assert
Assert.Equal(["a_perk", "m_perk", "z_perk"], ordered.Select(p => p.Id).ToArray());
Assert.Equal(["a_perk", "m_perk", "z_perk"], [.. ordered.Select(p => p.Id)]);
}
[Fact]

View File

@ -158,10 +158,9 @@ public sealed class PerkStateApiTests
// Assert
Assert.Equal(HttpStatusCode.OK, post.StatusCode);
var json = await post.Content.ReadAsStringAsync();
using (var doc = JsonDocument.Parse(json))
{
Assert.False(doc.RootElement.TryGetProperty("reasonCode", out _));
}
using var doc = JsonDocument.Parse(json);
Assert.False(doc.RootElement.TryGetProperty("reasonCode", out _));
var envelope = JsonSerializer.Deserialize<PerkBranchSelectResponse>(json);
Assert.NotNull(envelope);

View File

@ -20,23 +20,20 @@ public sealed class PerkStatePersistenceIntegrationTests(PostgresIntegrationHarn
// Arrange
await ResetPerkTablesAsync();
const string playerId = "dev-local-1";
using (var scope = Factory.Services.CreateScope())
{
var xpStore = scope.ServiceProvider.GetRequiredService<IPlayerSkillProgressionStore>();
var engine = scope.ServiceProvider.GetRequiredService<PerkUnlockEngine>();
Assert.True(xpStore.TryApplyXpDelta(playerId, "salvage", 450, out _, out _));
Assert.True(engine.TrySelectBranch(playerId, "salvage", 1, "scrap_efficiency").Success);
_ = engine.ReevaluateAfterLevelUp(playerId, "salvage", newLevel: 4);
}
using var scope = Factory.Services.CreateScope();
var xpStore = scope.ServiceProvider.GetRequiredService<IPlayerSkillProgressionStore>();
var engine = scope.ServiceProvider.GetRequiredService<PerkUnlockEngine>();
Assert.True(xpStore.TryApplyXpDelta(playerId, "salvage", 450, out _, out _));
Assert.True(engine.TrySelectBranch(playerId, "salvage", 1, "scrap_efficiency").Success);
_ = engine.ReevaluateAfterLevelUp(playerId, "salvage", newLevel: 4);
// Act
PerkStateSnapshot snapshot;
await using (var secondFactory = new PostgresWebApplicationFactory())
{
using var scope = secondFactory.Services.CreateScope();
var perkStore = scope.ServiceProvider.GetRequiredService<IPlayerPerkStateStore>();
snapshot = perkStore.GetSnapshot(playerId);
}
await using var secondFactory = new PostgresWebApplicationFactory();
using var secondScope = secondFactory.Services.CreateScope();
var perkStore = secondScope.ServiceProvider.GetRequiredService<IPlayerPerkStateStore>();
snapshot = perkStore.GetSnapshot(playerId);
// Assert
Assert.Equal("scrap_efficiency", snapshot.BranchPicksBySkillId["salvage"][1]);
@ -65,21 +62,15 @@ public sealed class PerkStatePersistenceIntegrationTests(PostgresIntegrationHarn
var perkDdl = await File.ReadAllTextAsync(perkDdlPath);
await using var conn = new NpgsqlConnection(cs);
await conn.OpenAsync();
await using (var applyPosition = new NpgsqlCommand(positionDdl, conn))
{
await applyPosition.ExecuteNonQueryAsync();
}
await using var applyPosition = new NpgsqlCommand(positionDdl, conn);
await applyPosition.ExecuteNonQueryAsync();
await using (var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn))
{
await truncate.ExecuteNonQueryAsync();
}
await using var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn);
await truncate.ExecuteNonQueryAsync();
PostgresPositionBootstrap.SeedDevPlayer(conn, options);
await using (var applyPerk = new NpgsqlCommand(perkDdl, conn))
{
await applyPerk.ExecuteNonQueryAsync();
}
await using var applyPerk = new NpgsqlCommand(perkDdl, conn);
await applyPerk.ExecuteNonQueryAsync();
}
}

View File

@ -62,12 +62,11 @@ public sealed class PostgresPositionStateIntegrationTests(PostgresIntegrationHar
PositionStateResponse? persisted = null;
// Act
using (var first = Factory.CreateClient())
{
var post = await first.PostAsJsonAsync("/game/players/dev-local-1/move", cmd);
postStatus = post.StatusCode;
persisted = await post.Content.ReadFromJsonAsync<PositionStateResponse>();
}
using var first = Factory.CreateClient();
var post = await first.PostAsJsonAsync("/game/players/dev-local-1/move", cmd);
postStatus = post.StatusCode;
persisted = await post.Content.ReadFromJsonAsync<PositionStateResponse>();
// Assert
await using var secondFactory = new PostgresWebApplicationFactory();
@ -122,15 +121,11 @@ public sealed class PostgresPositionStateIntegrationTests(PostgresIntegrationHar
var ddl = await File.ReadAllTextAsync(ddlPath);
await using var conn = new NpgsqlConnection(cs);
await conn.OpenAsync();
await using (var apply = new NpgsqlCommand(ddl, conn))
{
await apply.ExecuteNonQueryAsync();
}
await using var apply = new NpgsqlCommand(ddl, conn);
await apply.ExecuteNonQueryAsync();
await using (var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn))
{
await truncate.ExecuteNonQueryAsync();
}
await using var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn);
await truncate.ExecuteNonQueryAsync();
PostgresPositionBootstrap.SeedDevPlayer(conn, options);
}

View File

@ -70,17 +70,15 @@ public sealed class PlayerQuestProgressPersistenceIntegrationTests(PostgresInteg
// Act — read back through a fresh host
QuestStepState readBack;
await using (var secondFactory = new PostgresWebApplicationFactory())
{
using var secondScope = secondFactory.Services.CreateScope();
var store = secondScope.ServiceProvider.GetRequiredService<IPlayerQuestStateStore>();
readBack = store.TryGetProgress(
PlayerId,
PrototypeE7M1QuestCatalogRules.GatherIntroQuestId,
out var snapshot)
? snapshot
: null!;
}
await using var secondFactory = new PostgresWebApplicationFactory();
using var secondScope = secondFactory.Services.CreateScope();
var readStore = secondScope.ServiceProvider.GetRequiredService<IPlayerQuestStateStore>();
readBack = readStore.TryGetProgress(
PlayerId,
PrototypeE7M1QuestCatalogRules.GatherIntroQuestId,
out var snapshot)
? snapshot
: null!;
// Assert
Assert.NotNull(readBack);
@ -108,12 +106,10 @@ public sealed class PlayerQuestProgressPersistenceIntegrationTests(PostgresInteg
}
var readBackExists = false;
await using (var secondFactory = new PostgresWebApplicationFactory())
{
using var secondScope = secondFactory.Services.CreateScope();
var store = secondScope.ServiceProvider.GetRequiredService<IPlayerQuestStateStore>();
readBackExists = store.TryGetProgress(PlayerId, questId, out _);
}
await using var secondFactory = new PostgresWebApplicationFactory();
using var secondScope = secondFactory.Services.CreateScope();
var readStore = secondScope.ServiceProvider.GetRequiredService<IPlayerQuestStateStore>();
readBackExists = readStore.TryGetProgress(PlayerId, questId, out _);
// Assert
Assert.False(readBackExists);
@ -141,21 +137,15 @@ public sealed class PlayerQuestProgressPersistenceIntegrationTests(PostgresInteg
var questDdl = await File.ReadAllTextAsync(questDdlPath);
await using var conn = new NpgsqlConnection(cs);
await conn.OpenAsync();
await using (var applyPosition = new NpgsqlCommand(positionDdl, conn))
{
await applyPosition.ExecuteNonQueryAsync();
}
await using var applyPosition = new NpgsqlCommand(positionDdl, conn);
await applyPosition.ExecuteNonQueryAsync();
await using (var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn))
{
await truncate.ExecuteNonQueryAsync();
}
await using var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn);
await truncate.ExecuteNonQueryAsync();
PostgresPositionBootstrap.SeedDevPlayer(conn, options);
await using (var applyQuest = new NpgsqlCommand(questDdl, conn))
{
await applyQuest.ExecuteNonQueryAsync();
}
await using var applyQuest = new NpgsqlCommand(questDdl, conn);
await applyQuest.ExecuteNonQueryAsync();
}
}

View File

@ -24,8 +24,8 @@ public class QuestDefinitionsWorldApiTests
Assert.NotNull(body.Quests);
Assert.Equal(PrototypeE7M1QuestCatalogRules.ExpectedQuestIds.Count, body.Quests.Count);
Assert.Equal(
PrototypeE7M1QuestCatalogRules.ExpectedQuestIds.Order(StringComparer.Ordinal).ToArray(),
body.Quests.Select(q => q.Id).ToArray());
[.. PrototypeE7M1QuestCatalogRules.ExpectedQuestIds.Order(StringComparer.Ordinal)],
[.. body.Quests.Select(q => q.Id)]);
var gatherIntro = body.Quests.Single(q => q.Id == "prototype_quest_gather_intro");
Assert.Equal("Intro: Salvage Run", gatherIntro.DisplayName);

View File

@ -63,7 +63,7 @@ public sealed class QuestProgressApiTests
await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient();
var registry = factory.Services.GetRequiredService<IQuestDefinitionRegistry>();
var expectedQuestOrder = registry.GetDefinitionsInIdOrder().Select(static d => d.Id).ToArray();
string[] expectedQuestOrder = [.. registry.GetDefinitionsInIdOrder().Select(static d => d.Id)];
// Act
var response = await client.GetAsync($"/game/players/{PlayerId}/quest-progress");
@ -75,7 +75,8 @@ public sealed class QuestProgressApiTests
Assert.Equal(QuestProgressListResponse.CurrentSchemaVersion, body!.SchemaVersion);
Assert.Equal(PlayerId, body.PlayerId);
Assert.Equal(expectedQuestOrder.Length, body.Quests.Count);
Assert.Equal(expectedQuestOrder, body.Quests.Select(static row => row.QuestId).ToArray());
string[] actualQuestOrder = [.. body.Quests.Select(static row => row.QuestId)];
Assert.Equal(expectedQuestOrder, actualQuestOrder);
foreach (var row in body.Quests)
{
Assert.Equal(QuestProgressApi.StatusNotStarted, row.Status);

View File

@ -29,12 +29,11 @@ public sealed class SkillProgressionGrantPersistenceIntegrationTests(PostgresInt
HttpResponseMessage postResponse;
// Act — write grant through first host
using (var firstClient = Factory.CreateClient())
{
postResponse = await firstClient.PostAsJsonAsync(
"/game/players/dev-local-1/skill-progression",
grant);
}
using var firstClient = Factory.CreateClient();
postResponse = await firstClient.PostAsJsonAsync(
"/game/players/dev-local-1/skill-progression",
grant);
// Act — read back through a fresh host (forces DB round-trip / new DI scope)
await using var secondFactory = new PostgresWebApplicationFactory();
@ -78,26 +77,18 @@ public sealed class SkillProgressionGrantPersistenceIntegrationTests(PostgresInt
var progressionDdl = await File.ReadAllTextAsync(progressionDdlPath);
await using var conn = new NpgsqlConnection(cs);
await conn.OpenAsync();
await using (var applyPosition = new NpgsqlCommand(positionDdl, conn))
{
await applyPosition.ExecuteNonQueryAsync();
}
await using var applyPosition = new NpgsqlCommand(positionDdl, conn);
await applyPosition.ExecuteNonQueryAsync();
await using (var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn))
{
await truncate.ExecuteNonQueryAsync();
}
await using var truncate = new NpgsqlCommand("TRUNCATE player_position CASCADE;", conn);
await truncate.ExecuteNonQueryAsync();
PostgresPositionBootstrap.SeedDevPlayer(conn, options);
await using (var applyProgression = new NpgsqlCommand(progressionDdl, conn))
{
await applyProgression.ExecuteNonQueryAsync();
}
await using var applyProgression = new NpgsqlCommand(progressionDdl, conn);
await applyProgression.ExecuteNonQueryAsync();
await using (var truncateProgression = new NpgsqlCommand("TRUNCATE player_skill_progression;", conn))
{
await truncateProgression.ExecuteNonQueryAsync();
}
await using var truncateProgression = new NpgsqlCommand("TRUNCATE player_skill_progression;", conn);
await truncateProgression.ExecuteNonQueryAsync();
}
}

View File

@ -1,10 +1,12 @@
using System.Threading;
namespace NeonSprawl.Server.Game.AbilityInput;
/// <summary>Applies NEO-29 hotbar loadout table DDL once per process.</summary>
public static class PostgresHotbarLoadoutBootstrap
{
private static readonly string DdlRelativePath = Path.Combine("db", "migrations", "V002__player_hotbar_loadout.sql");
private static readonly object SchemaGate = new();
private static readonly Lock SchemaGate = new();
private static int _schemaReady;
public static void EnsureSchema(Npgsql.NpgsqlDataSource dataSource)

View File

@ -26,9 +26,8 @@ public static class AbilityDefinitionCatalogLoader
if (errors.Count > 0)
ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(abilitiesDirectory, "*_abilities.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)
.ToArray();
string[] jsonFiles = [.. Directory.GetFiles(abilitiesDirectory, "*_abilities.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)];
if (jsonFiles.Length == 0)
errors.Add($"error: no *_abilities.json files under {abilitiesDirectory}");

View File

@ -43,7 +43,7 @@ public sealed class AbilityDefinitionRegistry(AbilityDefinitionCatalog catalog)
private static IReadOnlyList<AbilityDefRow> BuildDefinitionsInIdOrder(AbilityDefinitionCatalog catalog)
{
var ids = catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal).ToArray();
string[] ids = [.. catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal)];
var list = new List<AbilityDefRow>(ids.Length);
foreach (var id in ids)
{

View File

@ -62,9 +62,8 @@ public static class ContractTemplateCatalogLoader
if (errors.Count > 0)
ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(contractsDirectory, "*_contract_templates.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)
.ToArray();
string[] jsonFiles = [.. Directory.GetFiles(contractsDirectory, "*_contract_templates.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)];
if (jsonFiles.Length == 0)
errors.Add($"error: no *_contract_templates.json files under {contractsDirectory}");

View File

@ -27,7 +27,7 @@ public sealed class ContractTemplateRegistry(ContractTemplateCatalog catalog) :
/// <inheritdoc />
public IReadOnlyList<ContractTemplateRow> GetDefinitionsInIdOrder()
{
var ids = catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal).ToArray();
string[] ids = [.. catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal)];
var list = new List<ContractTemplateRow>(ids.Length);
foreach (var id in ids)
list.Add(catalog.ById[id]);

View File

@ -73,7 +73,7 @@ public sealed class InMemoryContractOutcomeStore : IContractOutcomeStore
query = query.Take(limit.Value);
}
return query.ToArray();
return [..query];
}
/// <inheritdoc />
@ -85,10 +85,9 @@ public sealed class InMemoryContractOutcomeStore : IContractOutcomeStore
return false;
}
var idsToRemove = rowsById.Values
string[] idsToRemove = [.. rowsById.Values
.Where(row => string.Equals(row.ContractInstanceId, instanceId, StringComparison.Ordinal))
.Select(static row => row.Id)
.ToArray();
.Select(static row => row.Id)];
foreach (var id in idsToRemove)
{
@ -124,9 +123,9 @@ public sealed class InMemoryContractOutcomeStore : IContractOutcomeStore
PlayerId = playerId,
ContractInstanceId = instanceId,
IdempotencyKey = idempotencyKey,
GrantedItems = row.GrantedItems.ToArray(),
GrantedSkillXp = row.GrantedSkillXp.ToArray(),
GrantedReputation = row.GrantedReputation.ToArray(),
GrantedItems = [..row.GrantedItems],
GrantedSkillXp = [..row.GrantedSkillXp],
GrantedReputation = [..row.GrantedReputation],
};
return true;
}

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Contracts;
/// <summary>Applies NEO-146 contract instance table DDL once per process.</summary>
@ -5,7 +7,7 @@ public static class PostgresContractInstanceBootstrap
{
private static readonly string DdlRelativePath = Path.Combine("db", "migrations", "V011__contract_instance.sql");
private static readonly object SchemaGate = new();
private static readonly Lock SchemaGate = new();
private static int _schemaReady;

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Contracts;
/// <summary>Applies NEO-146 contract outcome table DDL once per process.</summary>
@ -5,7 +7,7 @@ public static class PostgresContractOutcomeBootstrap
{
private static readonly string DdlRelativePath = Path.Combine("db", "migrations", "V012__contract_outcome.sql");
private static readonly object SchemaGate = new();
private static readonly Lock SchemaGate = new();
private static int _schemaReady;

View File

@ -162,19 +162,19 @@ public sealed class PostgresContractOutcomeStore(Npgsql.NpgsqlDataSource dataSou
private static string SerializeGrants<T>(IReadOnlyList<T> grants) =>
JsonSerializer.Serialize(grants, JsonOptions);
private static IReadOnlyList<RewardItemGrantApplied> DeserializeItems(string json)
private static RewardItemGrantApplied[] DeserializeItems(string json)
{
var parsed = JsonSerializer.Deserialize<RewardItemGrantApplied[]>(json, JsonOptions);
return parsed ?? [];
}
private static IReadOnlyList<RewardSkillXpGrantApplied> DeserializeSkillXp(string json)
private static RewardSkillXpGrantApplied[] DeserializeSkillXp(string json)
{
var parsed = JsonSerializer.Deserialize<RewardSkillXpGrantApplied[]>(json, JsonOptions);
return parsed ?? [];
}
private static IReadOnlyList<RewardReputationGrantApplied> DeserializeReputation(string json)
private static RewardReputationGrantApplied[] DeserializeReputation(string json)
{
var parsed = JsonSerializer.Deserialize<RewardReputationGrantApplied[]>(json, JsonOptions);
return parsed ?? [];
@ -201,9 +201,9 @@ public sealed class PostgresContractOutcomeStore(Npgsql.NpgsqlDataSource dataSou
PlayerId = playerId,
ContractInstanceId = instanceId,
IdempotencyKey = idempotencyKey,
GrantedItems = row.GrantedItems.ToArray(),
GrantedSkillXp = row.GrantedSkillXp.ToArray(),
GrantedReputation = row.GrantedReputation.ToArray(),
GrantedItems = [..row.GrantedItems],
GrantedSkillXp = [..row.GrantedSkillXp],
GrantedReputation = [..row.GrantedReputation],
};
return true;
}

View File

@ -36,9 +36,8 @@ public static class RecipeDefinitionCatalogLoader
if (errors.Count > 0)
ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(recipesDirectory, "*_recipes.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)
.ToArray();
string[] jsonFiles = [.. Directory.GetFiles(recipesDirectory, "*_recipes.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)];
if (jsonFiles.Length == 0)
errors.Add($"error: no *_recipes.json files under {recipesDirectory}");

View File

@ -24,7 +24,7 @@ public sealed class RecipeDefinitionRegistry(RecipeDefinitionCatalog catalog) :
private static IReadOnlyList<RecipeDefRow> BuildDefinitionsInIdOrder(RecipeDefinitionCatalog catalog)
{
var ids = catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal).ToArray();
string[] ids = [.. catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal)];
var list = new List<RecipeDefRow>(ids.Length);
foreach (var id in ids)
{

View File

@ -30,9 +30,8 @@ public static class EncounterDefinitionCatalogLoader
if (errors.Count > 0)
ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(encountersDirectory, "*_encounters.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)
.ToArray();
string[] jsonFiles = [.. Directory.GetFiles(encountersDirectory, "*_encounters.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)];
if (jsonFiles.Length == 0)
errors.Add($"error: no *_encounters.json files under {encountersDirectory}");

View File

@ -43,7 +43,7 @@ public sealed class EncounterDefinitionRegistry(EncounterDefinitionCatalog catal
private static IReadOnlyList<EncounterDefRow> BuildDefinitionsInIdOrder(EncounterDefinitionCatalog catalog)
{
var ids = catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal).ToArray();
string[] ids = [.. catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal)];
var list = new List<EncounterDefRow>(ids.Length);
foreach (var id in ids)
{

View File

@ -27,7 +27,7 @@ public sealed class InMemoryEncounterCompleteEventStore : IEncounterCompleteEven
eventsByKey[key] = completeEvent with
{
GrantedItems = completeEvent.GrantedItems.ToArray(),
GrantedItems = [..completeEvent.GrantedItems],
};
return true;
}

View File

@ -35,9 +35,8 @@ public static class RewardTableDefinitionCatalogLoader
if (errors.Count > 0)
ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(rewardTablesDirectory, "*_reward_tables.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)
.ToArray();
string[] jsonFiles = [.. Directory.GetFiles(rewardTablesDirectory, "*_reward_tables.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)];
if (jsonFiles.Length == 0)
errors.Add($"error: no *_reward_tables.json files under {rewardTablesDirectory}");

View File

@ -43,7 +43,7 @@ public sealed class RewardTableDefinitionRegistry(RewardTableDefinitionCatalog c
private static IReadOnlyList<RewardTableDefRow> BuildDefinitionsInIdOrder(RewardTableDefinitionCatalog catalog)
{
var ids = catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal).ToArray();
string[] ids = [.. catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal)];
var list = new List<RewardTableDefRow>(ids.Length);
foreach (var id in ids)
{

View File

@ -26,9 +26,8 @@ public static class FactionDefinitionCatalogLoader
if (errors.Count > 0)
ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(factionsDirectory, "*_factions.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)
.ToArray();
string[] jsonFiles = [.. Directory.GetFiles(factionsDirectory, "*_factions.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)];
if (jsonFiles.Length == 0)
errors.Add($"error: no *_factions.json files under {factionsDirectory}");

View File

@ -27,7 +27,7 @@ public sealed class FactionDefinitionRegistry(FactionDefinitionCatalog catalog)
/// <inheritdoc />
public IReadOnlyList<FactionDefRow> GetDefinitionsInIdOrder()
{
var ids = catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal).ToArray();
string[] ids = [.. catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal)];
var list = new List<FactionDefRow>(ids.Length);
foreach (var id in ids)
{

View File

@ -58,7 +58,7 @@ public sealed class InMemoryReputationDeltaStore : IReputationDeltaStore
query = query.Take(limit.Value);
}
return query.ToArray();
return [..query];
}
/// <inheritdoc />
@ -71,13 +71,12 @@ public sealed class InMemoryReputationDeltaStore : IReputationDeltaStore
return false;
}
var idsToRemove = rowsById.Values
string[] idsToRemove = [.. rowsById.Values
.Where(row =>
string.Equals(row.PlayerId, player, StringComparison.Ordinal) &&
string.Equals(row.SourceKind, ReputationDeltaSourceKinds.QuestCompletion, StringComparison.Ordinal) &&
string.Equals(row.SourceId, sourceId, StringComparison.Ordinal))
.Select(static row => row.Id)
.ToArray();
.Select(static row => row.Id)];
foreach (var id in idsToRemove)
{

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Factions;
/// <summary>Applies NEO-135 faction standing table DDL once per process.</summary>
@ -5,7 +7,7 @@ public static class PostgresPlayerFactionStandingBootstrap
{
private static readonly string DdlRelativePath = Path.Combine("db", "migrations", "V009__player_faction_standing.sql");
private static readonly object SchemaGate = new();
private static readonly Lock SchemaGate = new();
private static int _schemaReady;

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Factions;
/// <summary>Applies NEO-135 reputation delta audit table DDL once per process.</summary>
@ -5,7 +7,7 @@ public static class PostgresReputationDeltaBootstrap
{
private static readonly string DdlRelativePath = Path.Combine("db", "migrations", "V010__reputation_delta_audit.sql");
private static readonly object SchemaGate = new();
private static readonly Lock SchemaGate = new();
private static int _schemaReady;

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Gathering;
/// <summary>Applies NEO-61 resource-node instance table DDL once per process.</summary>
@ -5,7 +7,7 @@ public static class PostgresResourceNodeInstanceBootstrap
{
private static readonly string DdlRelativePath = Path.Combine("db", "migrations", "V006__resource_node_instance.sql");
private static readonly object SchemaGate = new();
private static readonly Lock SchemaGate = new();
private static int _schemaReady;

View File

@ -35,15 +35,13 @@ public static class ResourceNodeCatalogLoader
if (errors.Count > 0)
ThrowIfAny(errors);
var nodeJsonFiles = Directory.GetFiles(resourceNodesDirectory, "*_resource_nodes.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)
.ToArray();
string[] nodeJsonFiles = [.. Directory.GetFiles(resourceNodesDirectory, "*_resource_nodes.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)];
if (nodeJsonFiles.Length == 0)
errors.Add($"error: no *_resource_nodes.json files under {resourceNodesDirectory}");
var yieldJsonFiles = Directory.GetFiles(resourceNodesDirectory, "*_resource_yields.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)
.ToArray();
string[] yieldJsonFiles = [.. Directory.GetFiles(resourceNodesDirectory, "*_resource_yields.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)];
if (yieldJsonFiles.Length == 0)
errors.Add($"error: no *_resource_yields.json files under {resourceNodesDirectory}");

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Gigs;
/// <summary>Applies NEO-44 gig progression table DDL once per process.</summary>
@ -5,7 +7,7 @@ public static class PostgresGigProgressionBootstrap
{
private static readonly string DdlRelativePath = Path.Combine("db", "migrations", "V007__player_gig_progression.sql");
private static readonly object SchemaGate = new();
private static readonly Lock SchemaGate = new();
private static int _schemaReady;

View File

@ -26,9 +26,8 @@ public static class ItemDefinitionCatalogLoader
if (errors.Count > 0)
ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(itemsDirectory, "*_items.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)
.ToArray();
string[] jsonFiles = [.. Directory.GetFiles(itemsDirectory, "*_items.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)];
if (jsonFiles.Length == 0)
errors.Add($"error: no *_items.json files under {itemsDirectory}");

View File

@ -27,7 +27,7 @@ public sealed class ItemDefinitionRegistry(ItemDefinitionCatalog catalog) : IIte
/// <inheritdoc />
public IReadOnlyList<ItemDefRow> GetDefinitionsInIdOrder()
{
var ids = catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal).ToArray();
string[] ids = [.. catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal)];
var list = new List<ItemDefRow>(ids.Length);
foreach (var id in ids)
{

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Items;
/// <summary>Applies NEO-54 inventory table DDL once per process.</summary>
@ -5,7 +7,7 @@ public static class PostgresPlayerInventoryBootstrap
{
private static readonly string DdlRelativePath = Path.Combine("db", "migrations", "V005__player_inventory.sql");
private static readonly object SchemaGate = new();
private static readonly Lock SchemaGate = new();
private static int _schemaReady;

View File

@ -29,9 +29,8 @@ public static class MasteryCatalogLoader
if (errors.Count > 0)
ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(masteryDirectory, "*_mastery.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)
.ToArray();
string[] jsonFiles = [.. Directory.GetFiles(masteryDirectory, "*_mastery.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)];
if (jsonFiles.Length == 0)
errors.Add($"error: no *_mastery.json files under {masteryDirectory}");

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Mastery;
/// <summary>Applies NEO-47 perk state table DDL once per process.</summary>
@ -5,7 +7,7 @@ public static class PostgresPerkStateBootstrap
{
private static readonly string DdlRelativePath = Path.Combine("db", "migrations", "V004__player_perk_state.sql");
private static readonly object SchemaGate = new();
private static readonly Lock SchemaGate = new();
private static int _schemaReady;

View File

@ -26,9 +26,8 @@ public static class NpcBehaviorDefinitionCatalogLoader
if (errors.Count > 0)
ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(npcBehaviorsDirectory, "*_npc_behaviors.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)
.ToArray();
string[] jsonFiles = [.. Directory.GetFiles(npcBehaviorsDirectory, "*_npc_behaviors.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)];
if (jsonFiles.Length == 0)
errors.Add($"error: no *_npc_behaviors.json files under {npcBehaviorsDirectory}");

View File

@ -43,7 +43,7 @@ public sealed class NpcBehaviorDefinitionRegistry(NpcBehaviorDefinitionCatalog c
private static IReadOnlyList<NpcBehaviorDefRow> BuildDefinitionsInIdOrder(NpcBehaviorDefinitionCatalog catalog)
{
var ids = catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal).ToArray();
string[] ids = [.. catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal)];
var list = new List<NpcBehaviorDefRow>(ids.Length);
foreach (var id in ids)
{

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.PositionState;
/// <summary>
@ -8,7 +10,7 @@ public static class PostgresPositionBootstrap
{
private static readonly string DdlRelativePath = Path.Combine("db", "migrations", "V001__player_position.sql");
private static readonly object SchemaGate = new();
private static readonly Lock SchemaGate = new();
private static int _schemaReady;
/// <summary>
@ -36,10 +38,9 @@ public static class PostgresPositionBootstrap
var ddl = File.ReadAllText(ddlPath);
using var conn = dataSource.OpenConnection();
using (var cmd = new Npgsql.NpgsqlCommand(ddl, conn))
{
cmd.ExecuteNonQuery();
}
using var cmd = new Npgsql.NpgsqlCommand(ddl, conn);
cmd.ExecuteNonQuery();
System.Threading.Volatile.Write(ref _schemaReady, 1);
}

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Quests;
/// <summary>Applies NEO-116 quest progress table DDL once per process.</summary>
@ -5,7 +7,7 @@ public static class PostgresPlayerQuestProgressBootstrap
{
private static readonly string DdlRelativePath = Path.Combine("db", "migrations", "V008__player_quest_progress.sql");
private static readonly object SchemaGate = new();
private static readonly Lock SchemaGate = new();
private static int _schemaReady;

View File

@ -126,14 +126,12 @@ public static class PrototypeE7M2QuestCatalogRules
private static NormalizedBundle NormalizeBundle(QuestRewardBundleRow bundle) =>
new(
bundle.ItemGrants
[.. bundle.ItemGrants
.OrderBy(g => g.ItemId, StringComparer.Ordinal)
.ThenBy(g => g.Quantity)
.ToArray(),
bundle.SkillXpGrants
.ThenBy(g => g.Quantity)],
[.. bundle.SkillXpGrants
.OrderBy(g => g.SkillId, StringComparer.Ordinal)
.ThenBy(g => g.Amount)
.ToArray());
.ThenBy(g => g.Amount)]);
private static bool BundlesEqual(NormalizedBundle left, NormalizedBundle right)
{

View File

@ -161,18 +161,15 @@ public static class PrototypeE7M3QuestFactionRules
private static NormalizedBundle NormalizeBundle(QuestRewardBundleRow bundle) =>
new(
bundle.ItemGrants
[.. bundle.ItemGrants
.OrderBy(g => g.ItemId, StringComparer.Ordinal)
.ThenBy(g => g.Quantity)
.ToArray(),
bundle.SkillXpGrants
.ThenBy(g => g.Quantity)],
[.. bundle.SkillXpGrants
.OrderBy(g => g.SkillId, StringComparer.Ordinal)
.ThenBy(g => g.Amount)
.ToArray(),
bundle.ReputationGrants
.ThenBy(g => g.Amount)],
[.. bundle.ReputationGrants
.OrderBy(g => g.FactionId, StringComparer.Ordinal)
.ThenBy(g => g.Amount)
.ToArray());
.ThenBy(g => g.Amount)]);
private static bool BundlesEqual(NormalizedBundle left, NormalizedBundle right)
{

View File

@ -64,9 +64,8 @@ public static class QuestDefinitionCatalogLoader
if (errors.Count > 0)
ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(questsDirectory, "*_quests.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)
.ToArray();
string[] jsonFiles = [.. Directory.GetFiles(questsDirectory, "*_quests.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)];
if (jsonFiles.Length == 0)
errors.Add($"error: no *_quests.json files under {questsDirectory}");

View File

@ -43,7 +43,7 @@ public sealed class QuestDefinitionRegistry(QuestDefinitionCatalog catalog) : IQ
private static IReadOnlyList<QuestDefRow> BuildDefinitionsInIdOrder(QuestDefinitionCatalog catalog)
{
var ids = catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal).ToArray();
string[] ids = [.. catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal)];
var list = new List<QuestDefRow>(ids.Length);
foreach (var id in ids)
{

View File

@ -27,9 +27,9 @@ public sealed class InMemoryRewardDeliveryStore : IRewardDeliveryStore
eventsByKey[key] = deliveryEvent with
{
GrantedItems = deliveryEvent.GrantedItems.ToArray(),
GrantedSkillXp = deliveryEvent.GrantedSkillXp.ToArray(),
GrantedReputation = deliveryEvent.GrantedReputation.ToArray(),
GrantedItems = [..deliveryEvent.GrantedItems],
GrantedSkillXp = [..deliveryEvent.GrantedSkillXp],
GrantedReputation = [..deliveryEvent.GrantedReputation],
};
return true;
}

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Skills;
/// <summary>Applies NEO-38 skill progression table DDL once per process.</summary>
@ -5,7 +7,7 @@ public static class PostgresSkillProgressionBootstrap
{
private static readonly string DdlRelativePath = Path.Combine("db", "migrations", "V003__player_skill_progression.sql");
private static readonly object SchemaGate = new();
private static readonly Lock SchemaGate = new();
private static int _schemaReady;

View File

@ -26,9 +26,8 @@ public static class SkillDefinitionCatalogLoader
if (errors.Count > 0)
ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(skillsDirectory, "*_skills.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)
.ToArray();
string[] jsonFiles = [.. Directory.GetFiles(skillsDirectory, "*_skills.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal)];
if (jsonFiles.Length == 0)
errors.Add($"error: no *_skills.json files under {skillsDirectory}");

View File

@ -27,7 +27,7 @@ public sealed class SkillDefinitionRegistry(SkillDefinitionCatalog catalog) : IS
/// <inheritdoc />
public IReadOnlyList<SkillDefRow> GetDefinitionsInIdOrder()
{
var ids = catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal).ToArray();
string[] ids = [.. catalog.ById.Keys.OrderBy(k => k, StringComparer.Ordinal)];
var list = new List<SkillDefRow>(ids.Length);
foreach (var id in ids)
{