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
parent
1922900477
commit
b455ff5ad5
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue