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. - **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. - **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. - Prefer **expression-bodied** members only when they stay one clear idea.
- **LINQ:** favor readability over micro-chains; break complex queries across lines. - **LINQ:** favor readability over micro-chains; break complex queries across lines.

View File

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

View File

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

View File

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

View File

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

View File

@ -85,7 +85,7 @@ public sealed class InMemoryReputationDeltaStoreTests
// Act // Act
var rows = store.GetDeltasForPlayer(PlayerId); var rows = store.GetDeltasForPlayer(PlayerId);
// Assert // 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] [Fact]

View File

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

View File

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

View File

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

View File

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

View File

@ -89,7 +89,7 @@ public class MasteryCatalogRegistryTests
// Act // Act
var tracks = registry.GetTracksInSkillIdOrder(); var tracks = registry.GetTracksInSkillIdOrder();
// Assert // Assert
Assert.Equal(["intrusion", "refine", "salvage"], tracks.Select(t => t.SkillId).ToArray()); Assert.Equal(["intrusion", "refine", "salvage"], [.. tracks.Select(t => t.SkillId)]);
} }
[Fact] [Fact]
@ -111,7 +111,7 @@ public class MasteryCatalogRegistryTests
// Act // Act
var ordered = registry.GetPerksInIdOrder(); var ordered = registry.GetPerksInIdOrder();
// Assert // 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] [Fact]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -63,7 +63,7 @@ public sealed class QuestProgressApiTests
await using var factory = new InMemoryWebApplicationFactory(); await using var factory = new InMemoryWebApplicationFactory();
var client = factory.CreateClient(); var client = factory.CreateClient();
var registry = factory.Services.GetRequiredService<IQuestDefinitionRegistry>(); 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 // Act
var response = await client.GetAsync($"/game/players/{PlayerId}/quest-progress"); 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(QuestProgressListResponse.CurrentSchemaVersion, body!.SchemaVersion);
Assert.Equal(PlayerId, body.PlayerId); Assert.Equal(PlayerId, body.PlayerId);
Assert.Equal(expectedQuestOrder.Length, body.Quests.Count); 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) foreach (var row in body.Quests)
{ {
Assert.Equal(QuestProgressApi.StatusNotStarted, row.Status); Assert.Equal(QuestProgressApi.StatusNotStarted, row.Status);

View File

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

View File

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

View File

@ -26,9 +26,8 @@ public static class AbilityDefinitionCatalogLoader
if (errors.Count > 0) if (errors.Count > 0)
ThrowIfAny(errors); ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(abilitiesDirectory, "*_abilities.json", SearchOption.TopDirectoryOnly) string[] jsonFiles = [.. Directory.GetFiles(abilitiesDirectory, "*_abilities.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal) .OrderBy(p => p, StringComparer.Ordinal)];
.ToArray();
if (jsonFiles.Length == 0) if (jsonFiles.Length == 0)
errors.Add($"error: no *_abilities.json files under {abilitiesDirectory}"); 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) 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); var list = new List<AbilityDefRow>(ids.Length);
foreach (var id in ids) foreach (var id in ids)
{ {

View File

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

View File

@ -27,7 +27,7 @@ public sealed class ContractTemplateRegistry(ContractTemplateCatalog catalog) :
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<ContractTemplateRow> GetDefinitionsInIdOrder() 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); var list = new List<ContractTemplateRow>(ids.Length);
foreach (var id in ids) foreach (var id in ids)
list.Add(catalog.ById[id]); list.Add(catalog.ById[id]);

View File

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

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Contracts; namespace NeonSprawl.Server.Game.Contracts;
/// <summary>Applies NEO-146 contract instance table DDL once per process.</summary> /// <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 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; private static int _schemaReady;

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Contracts; namespace NeonSprawl.Server.Game.Contracts;
/// <summary>Applies NEO-146 contract outcome table DDL once per process.</summary> /// <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 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; 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) => private static string SerializeGrants<T>(IReadOnlyList<T> grants) =>
JsonSerializer.Serialize(grants, JsonOptions); JsonSerializer.Serialize(grants, JsonOptions);
private static IReadOnlyList<RewardItemGrantApplied> DeserializeItems(string json) private static RewardItemGrantApplied[] DeserializeItems(string json)
{ {
var parsed = JsonSerializer.Deserialize<RewardItemGrantApplied[]>(json, JsonOptions); var parsed = JsonSerializer.Deserialize<RewardItemGrantApplied[]>(json, JsonOptions);
return parsed ?? []; return parsed ?? [];
} }
private static IReadOnlyList<RewardSkillXpGrantApplied> DeserializeSkillXp(string json) private static RewardSkillXpGrantApplied[] DeserializeSkillXp(string json)
{ {
var parsed = JsonSerializer.Deserialize<RewardSkillXpGrantApplied[]>(json, JsonOptions); var parsed = JsonSerializer.Deserialize<RewardSkillXpGrantApplied[]>(json, JsonOptions);
return parsed ?? []; return parsed ?? [];
} }
private static IReadOnlyList<RewardReputationGrantApplied> DeserializeReputation(string json) private static RewardReputationGrantApplied[] DeserializeReputation(string json)
{ {
var parsed = JsonSerializer.Deserialize<RewardReputationGrantApplied[]>(json, JsonOptions); var parsed = JsonSerializer.Deserialize<RewardReputationGrantApplied[]>(json, JsonOptions);
return parsed ?? []; return parsed ?? [];
@ -201,9 +201,9 @@ public sealed class PostgresContractOutcomeStore(Npgsql.NpgsqlDataSource dataSou
PlayerId = playerId, PlayerId = playerId,
ContractInstanceId = instanceId, ContractInstanceId = instanceId,
IdempotencyKey = idempotencyKey, IdempotencyKey = idempotencyKey,
GrantedItems = row.GrantedItems.ToArray(), GrantedItems = [..row.GrantedItems],
GrantedSkillXp = row.GrantedSkillXp.ToArray(), GrantedSkillXp = [..row.GrantedSkillXp],
GrantedReputation = row.GrantedReputation.ToArray(), GrantedReputation = [..row.GrantedReputation],
}; };
return true; return true;
} }

View File

@ -36,9 +36,8 @@ public static class RecipeDefinitionCatalogLoader
if (errors.Count > 0) if (errors.Count > 0)
ThrowIfAny(errors); ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(recipesDirectory, "*_recipes.json", SearchOption.TopDirectoryOnly) string[] jsonFiles = [.. Directory.GetFiles(recipesDirectory, "*_recipes.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal) .OrderBy(p => p, StringComparer.Ordinal)];
.ToArray();
if (jsonFiles.Length == 0) if (jsonFiles.Length == 0)
errors.Add($"error: no *_recipes.json files under {recipesDirectory}"); 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) 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); var list = new List<RecipeDefRow>(ids.Length);
foreach (var id in ids) foreach (var id in ids)
{ {

View File

@ -30,9 +30,8 @@ public static class EncounterDefinitionCatalogLoader
if (errors.Count > 0) if (errors.Count > 0)
ThrowIfAny(errors); ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(encountersDirectory, "*_encounters.json", SearchOption.TopDirectoryOnly) string[] jsonFiles = [.. Directory.GetFiles(encountersDirectory, "*_encounters.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal) .OrderBy(p => p, StringComparer.Ordinal)];
.ToArray();
if (jsonFiles.Length == 0) if (jsonFiles.Length == 0)
errors.Add($"error: no *_encounters.json files under {encountersDirectory}"); 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) 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); var list = new List<EncounterDefRow>(ids.Length);
foreach (var id in ids) foreach (var id in ids)
{ {

View File

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

View File

@ -35,9 +35,8 @@ public static class RewardTableDefinitionCatalogLoader
if (errors.Count > 0) if (errors.Count > 0)
ThrowIfAny(errors); ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(rewardTablesDirectory, "*_reward_tables.json", SearchOption.TopDirectoryOnly) string[] jsonFiles = [.. Directory.GetFiles(rewardTablesDirectory, "*_reward_tables.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal) .OrderBy(p => p, StringComparer.Ordinal)];
.ToArray();
if (jsonFiles.Length == 0) if (jsonFiles.Length == 0)
errors.Add($"error: no *_reward_tables.json files under {rewardTablesDirectory}"); 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) 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); var list = new List<RewardTableDefRow>(ids.Length);
foreach (var id in ids) foreach (var id in ids)
{ {

View File

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

View File

@ -27,7 +27,7 @@ public sealed class FactionDefinitionRegistry(FactionDefinitionCatalog catalog)
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<FactionDefRow> GetDefinitionsInIdOrder() 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); var list = new List<FactionDefRow>(ids.Length);
foreach (var id in ids) foreach (var id in ids)
{ {

View File

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

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Factions; namespace NeonSprawl.Server.Game.Factions;
/// <summary>Applies NEO-135 faction standing table DDL once per process.</summary> /// <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 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; private static int _schemaReady;

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Factions; namespace NeonSprawl.Server.Game.Factions;
/// <summary>Applies NEO-135 reputation delta audit table DDL once per process.</summary> /// <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 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; private static int _schemaReady;

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Gathering; namespace NeonSprawl.Server.Game.Gathering;
/// <summary>Applies NEO-61 resource-node instance table DDL once per process.</summary> /// <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 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; private static int _schemaReady;

View File

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

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Gigs; namespace NeonSprawl.Server.Game.Gigs;
/// <summary>Applies NEO-44 gig progression table DDL once per process.</summary> /// <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 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; private static int _schemaReady;

View File

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

View File

@ -27,7 +27,7 @@ public sealed class ItemDefinitionRegistry(ItemDefinitionCatalog catalog) : IIte
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<ItemDefRow> GetDefinitionsInIdOrder() 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); var list = new List<ItemDefRow>(ids.Length);
foreach (var id in ids) foreach (var id in ids)
{ {

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Items; namespace NeonSprawl.Server.Game.Items;
/// <summary>Applies NEO-54 inventory table DDL once per process.</summary> /// <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 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; private static int _schemaReady;

View File

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

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Mastery; namespace NeonSprawl.Server.Game.Mastery;
/// <summary>Applies NEO-47 perk state table DDL once per process.</summary> /// <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 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; private static int _schemaReady;

View File

@ -26,9 +26,8 @@ public static class NpcBehaviorDefinitionCatalogLoader
if (errors.Count > 0) if (errors.Count > 0)
ThrowIfAny(errors); ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(npcBehaviorsDirectory, "*_npc_behaviors.json", SearchOption.TopDirectoryOnly) string[] jsonFiles = [.. Directory.GetFiles(npcBehaviorsDirectory, "*_npc_behaviors.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal) .OrderBy(p => p, StringComparer.Ordinal)];
.ToArray();
if (jsonFiles.Length == 0) if (jsonFiles.Length == 0)
errors.Add($"error: no *_npc_behaviors.json files under {npcBehaviorsDirectory}"); 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) 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); var list = new List<NpcBehaviorDefRow>(ids.Length);
foreach (var id in ids) foreach (var id in ids)
{ {

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.PositionState; namespace NeonSprawl.Server.Game.PositionState;
/// <summary> /// <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 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; private static int _schemaReady;
/// <summary> /// <summary>
@ -36,10 +38,9 @@ public static class PostgresPositionBootstrap
var ddl = File.ReadAllText(ddlPath); var ddl = File.ReadAllText(ddlPath);
using var conn = dataSource.OpenConnection(); using var conn = dataSource.OpenConnection();
using (var cmd = new Npgsql.NpgsqlCommand(ddl, conn)) using var cmd = new Npgsql.NpgsqlCommand(ddl, conn);
{
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
}
System.Threading.Volatile.Write(ref _schemaReady, 1); System.Threading.Volatile.Write(ref _schemaReady, 1);
} }

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Quests; namespace NeonSprawl.Server.Game.Quests;
/// <summary>Applies NEO-116 quest progress table DDL once per process.</summary> /// <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 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; private static int _schemaReady;

View File

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

View File

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

View File

@ -64,9 +64,8 @@ public static class QuestDefinitionCatalogLoader
if (errors.Count > 0) if (errors.Count > 0)
ThrowIfAny(errors); ThrowIfAny(errors);
var jsonFiles = Directory.GetFiles(questsDirectory, "*_quests.json", SearchOption.TopDirectoryOnly) string[] jsonFiles = [.. Directory.GetFiles(questsDirectory, "*_quests.json", SearchOption.TopDirectoryOnly)
.OrderBy(p => p, StringComparer.Ordinal) .OrderBy(p => p, StringComparer.Ordinal)];
.ToArray();
if (jsonFiles.Length == 0) if (jsonFiles.Length == 0)
errors.Add($"error: no *_quests.json files under {questsDirectory}"); 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) 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); var list = new List<QuestDefRow>(ids.Length);
foreach (var id in ids) foreach (var id in ids)
{ {

View File

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

View File

@ -1,3 +1,5 @@
using System.Threading;
namespace NeonSprawl.Server.Game.Skills; namespace NeonSprawl.Server.Game.Skills;
/// <summary>Applies NEO-38 skill progression table DDL once per process.</summary> /// <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 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; private static int _schemaReady;

View File

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

View File

@ -27,7 +27,7 @@ public sealed class SkillDefinitionRegistry(SkillDefinitionCatalog catalog) : IS
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyList<SkillDefRow> GetDefinitionsInIdOrder() 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); var list = new List<SkillDefRow>(ids.Length);
foreach (var id in ids) foreach (var id in ids)
{ {