Add payload serializer guardrails
This commit is contained in:
@@ -101,8 +101,9 @@ dotnet dotnet-ef migrations add <MigrationName> --project RpgRoller/RpgRoller.cs
|
|||||||
- Browser interop is in `RpgRoller/wwwroot/js/rpgroller-api.js`.
|
- Browser interop is in `RpgRoller/wwwroot/js/rpgroller-api.js`.
|
||||||
- Workspace reads are resolved server-side through scoped query services; browser interop remains for browser-only concerns such as session storage, SSE wiring, and DOM helpers.
|
- Workspace reads are resolved server-side through scoped query services; browser interop remains for browser-only concerns such as session storage, SSE wiring, and DOM helpers.
|
||||||
- Live workspace refreshes now compare separate roster, per-character sheet, and log versions so unrelated state changes do not force a full roster + sheet + log reload.
|
- Live workspace refreshes now compare separate roster, per-character sheet, and log versions so unrelated state changes do not force a full roster + sheet + log reload.
|
||||||
- Workspace campaign data is loaded in bounded slices: visible campaign summaries, a selected campaign roster, a selected character sheet, and a 50-row incremental log window backed by `/api/campaigns/{campaignId}/log/page`.
|
- Workspace campaign data is loaded in bounded slices: visible campaign summaries, a selected campaign roster, a selected character sheet, and a 25-row incremental log window backed by `/api/campaigns/{campaignId}/log/page`.
|
||||||
- Campaign log rows now ship compact summary data first and lazy-load dice + breakdown detail through `/api/rolls/{rollId}` only when a row is expanded.
|
- Campaign log rows now ship compact summary data first and lazy-load dice + breakdown detail through `/api/rolls/{rollId}` only when a row is expanded.
|
||||||
|
- Hot API contracts share a source-generated `System.Text.Json` context, and HTTP JSON responses are gzip-compressed when the client advertises support.
|
||||||
- OpenAPI contract source remains at `openapi/RpgRoller.json`.
|
- OpenAPI contract source remains at `openapi/RpgRoller.json`.
|
||||||
|
|
||||||
## Test and Coverage
|
## Test and Coverage
|
||||||
@@ -111,6 +112,7 @@ dotnet dotnet-ef migrations add <MigrationName> --project RpgRoller/RpgRoller.cs
|
|||||||
```powershell
|
```powershell
|
||||||
dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --collect:"XPlat Code Coverage" --settings RpgRoller.Tests/coverlet.runsettings
|
dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --collect:"XPlat Code Coverage" --settings RpgRoller.Tests/coverlet.runsettings
|
||||||
```
|
```
|
||||||
|
- Regression tests enforce payload budgets for the hottest contracts: character sheet reads, initial log page loads, incremental log updates, and roll mutation responses.
|
||||||
- Coverage gate:
|
- Coverage gate:
|
||||||
```powershell
|
```powershell
|
||||||
pwsh ./scripts/check-coverage.ps1 -MinLineRate 0.90 -MinBranchRate 0.70
|
pwsh ./scripts/check-coverage.ps1 -MinLineRate 0.90 -MinBranchRate 0.70
|
||||||
|
|||||||
27
RpgRoller.Tests/Api/ResponseCompressionApiTests.cs
Normal file
27
RpgRoller.Tests/Api/ResponseCompressionApiTests.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace RpgRoller.Tests;
|
||||||
|
|
||||||
|
public sealed class ResponseCompressionApiTests : ApiTestBase
|
||||||
|
{
|
||||||
|
public ResponseCompressionApiTests(WebApplicationFactory<Program> factory) : base(factory)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AuthenticatedJsonResponses_EnableGzipCompression()
|
||||||
|
{
|
||||||
|
using var factory = CreateFactory();
|
||||||
|
using var client = factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||||
|
|
||||||
|
await RegisterAsync(client, "gm-compress", "Password123", "GM");
|
||||||
|
await LoginAsync(client, "gm-compress", "Password123");
|
||||||
|
_ = await PostAsync<CreateCampaignRequest, CampaignSummary>(client, "/api/campaigns", new("Compressed", "d6"));
|
||||||
|
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Get, "/api/campaigns");
|
||||||
|
request.Headers.Add("Accept-Encoding", "gzip");
|
||||||
|
|
||||||
|
var response = await client.SendAsync(request);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
Assert.Contains("gzip", response.Content.Headers.ContentEncoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
127
RpgRoller.Tests/PayloadBudgetTests.cs
Normal file
127
RpgRoller.Tests/PayloadBudgetTests.cs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
|
namespace RpgRoller.Tests;
|
||||||
|
|
||||||
|
public sealed class PayloadBudgetTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CharacterSheetPayload_StaysWithinBudget()
|
||||||
|
{
|
||||||
|
using var harness = ServiceTestSupport.CreateHarness();
|
||||||
|
var service = harness.Service;
|
||||||
|
|
||||||
|
service.Register("gm-sheet", "Password123", "GM");
|
||||||
|
service.Register("owner-sheet", "Password123", "Owner");
|
||||||
|
|
||||||
|
var gmSession = ServiceTestSupport.GetValue(service.Login("gm-sheet", "Password123")).SessionToken;
|
||||||
|
var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-sheet", "Password123")).SessionToken;
|
||||||
|
|
||||||
|
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Payload Sheet", "d6"));
|
||||||
|
var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Payload Hero", campaign.Id));
|
||||||
|
|
||||||
|
var groupIds = new List<Guid>
|
||||||
|
{
|
||||||
|
ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, character.Id, "Combat", "2D+1", 1, true)).Id,
|
||||||
|
ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, character.Id, "Social", "2D+1", 1, true)).Id,
|
||||||
|
ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, character.Id, "Knowledge", "2D+1", 1, true)).Id,
|
||||||
|
ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, character.Id, "Survival", "2D+1", 1, true)).Id
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var i = 0; i < 18; i++)
|
||||||
|
{
|
||||||
|
Guid? skillGroupId = i < 16 ? groupIds[i % groupIds.Count] : null;
|
||||||
|
_ = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, $"Skill {i:D2}", "2D+1", 1, true, skillGroupId));
|
||||||
|
}
|
||||||
|
|
||||||
|
var sheet = ServiceTestSupport.GetValue(service.GetCharacterSheet(ownerSession, character.Id));
|
||||||
|
AssertPayloadWithinBudget(sheet, 12 * 1024, "initial character sheet");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CampaignLogInitialPagePayload_StaysWithinBudget()
|
||||||
|
{
|
||||||
|
using var harness = ServiceTestSupport.CreateHarness(CreateScriptedRolls(220));
|
||||||
|
var service = harness.Service;
|
||||||
|
|
||||||
|
service.Register("gm-log-budget", "Password123", "GM");
|
||||||
|
service.Register("owner-log-budget", "Password123", "Owner");
|
||||||
|
|
||||||
|
var gmSession = ServiceTestSupport.GetValue(service.Login("gm-log-budget", "Password123")).SessionToken;
|
||||||
|
var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-log-budget", "Password123")).SessionToken;
|
||||||
|
|
||||||
|
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Payload Log", "d6"));
|
||||||
|
var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Log Hero", campaign.Id));
|
||||||
|
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Stealth", "2D+1", 1, true));
|
||||||
|
|
||||||
|
for (var i = 0; i < 25; i++)
|
||||||
|
_ = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public"));
|
||||||
|
|
||||||
|
var page = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, limit: 25));
|
||||||
|
AssertPayloadWithinBudget(page, 8 * 1024, "initial log page");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CampaignLogIncrementalPayload_StaysWithinBudget()
|
||||||
|
{
|
||||||
|
using var harness = ServiceTestSupport.CreateHarness(CreateScriptedRolls(240));
|
||||||
|
var service = harness.Service;
|
||||||
|
|
||||||
|
service.Register("gm-log-delta", "Password123", "GM");
|
||||||
|
service.Register("owner-log-delta", "Password123", "Owner");
|
||||||
|
|
||||||
|
var gmSession = ServiceTestSupport.GetValue(service.Login("gm-log-delta", "Password123")).SessionToken;
|
||||||
|
var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-log-delta", "Password123")).SessionToken;
|
||||||
|
|
||||||
|
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Payload Delta", "d6"));
|
||||||
|
var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Delta Hero", campaign.Id));
|
||||||
|
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Stealth", "2D+1", 1, true));
|
||||||
|
|
||||||
|
for (var i = 0; i < 25; i++)
|
||||||
|
_ = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public"));
|
||||||
|
|
||||||
|
var initialPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, limit: 25));
|
||||||
|
_ = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public"));
|
||||||
|
|
||||||
|
var incrementalPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, initialPage.Cursor, 25));
|
||||||
|
AssertPayloadWithinBudget(incrementalPage, 2 * 1024, "incremental log update");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RollResultPayload_StaysWithinJsInteropBudget()
|
||||||
|
{
|
||||||
|
using var harness = ServiceTestSupport.CreateHarness(CreateScriptedRolls(40));
|
||||||
|
var service = harness.Service;
|
||||||
|
|
||||||
|
service.Register("gm-roll-budget", "Password123", "GM");
|
||||||
|
service.Register("owner-roll-budget", "Password123", "Owner");
|
||||||
|
|
||||||
|
var gmSession = ServiceTestSupport.GetValue(service.Login("gm-roll-budget", "Password123")).SessionToken;
|
||||||
|
var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-roll-budget", "Password123")).SessionToken;
|
||||||
|
|
||||||
|
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Payload Roll", "d6"));
|
||||||
|
var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Roll Hero", campaign.Id));
|
||||||
|
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Wild Roll", "6D+3", 3, true));
|
||||||
|
|
||||||
|
var roll = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public"));
|
||||||
|
AssertPayloadWithinBudget(roll, 16 * 1024, "roll mutation response");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertPayloadWithinBudget<T>(T payload, int maxBytes, string label)
|
||||||
|
{
|
||||||
|
var byteCount = JsonSerializer.SerializeToUtf8Bytes(payload, SerializerOptions).Length;
|
||||||
|
Assert.True(byteCount <= maxBytes, $"{label} payload was {byteCount} bytes, above the {maxBytes}-byte budget.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] CreateScriptedRolls(int count)
|
||||||
|
{
|
||||||
|
var values = new[] { 6, 5, 4, 3, 2, 1 };
|
||||||
|
var scriptedRolls = new int[count];
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
scriptedRolls[i] = values[i % values.Length];
|
||||||
|
|
||||||
|
return scriptedRolls;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions SerializerOptions = RpgRollerJson.CreateSerializerOptions();
|
||||||
|
}
|
||||||
@@ -1236,7 +1236,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private const string CampaignSessionKey = "campaign";
|
private const string CampaignSessionKey = "campaign";
|
||||||
private const string MobilePanelSessionKey = "play-panel";
|
private const string MobilePanelSessionKey = "play-panel";
|
||||||
private const string RollVisibilitySessionKey = "roll-visibility";
|
private const string RollVisibilitySessionKey = "roll-visibility";
|
||||||
private const int CampaignLogWindowSize = 50;
|
private const int CampaignLogWindowSize = 25;
|
||||||
private const int ToastDurationMs = 3200;
|
private const int ToastDurationMs = 3200;
|
||||||
|
|
||||||
private sealed record WorkspaceToast(Guid Id, string Message, bool IsError);
|
private sealed record WorkspaceToast(Guid Id, string Message, bool IsError);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
namespace RpgRoller.Components;
|
namespace RpgRoller.Components;
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ public sealed class RpgRollerApiClient
|
|||||||
throw new ApiRequestException(response.Status, response.Error ?? "Request failed.");
|
throw new ApiRequestException(response.Status, response.Error ?? "Request failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions JsonOptions = RpgRollerJson.CreateSerializerOptions();
|
||||||
private readonly IJSRuntime m_Js;
|
private readonly IJSRuntime m_Js;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,4 +50,4 @@ public sealed class ApiRequestException : Exception
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int StatusCode { get; }
|
public int StatusCode { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
19
RpgRoller/Contracts/RpgRollerJson.cs
Normal file
19
RpgRoller/Contracts/RpgRollerJson.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace RpgRoller.Contracts;
|
||||||
|
|
||||||
|
public static class RpgRollerJson
|
||||||
|
{
|
||||||
|
public static JsonSerializerOptions CreateSerializerOptions()
|
||||||
|
{
|
||||||
|
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||||
|
Configure(options);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Configure(JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (!options.TypeInfoResolverChain.Contains(RpgRollerJsonSerializerContext.Default))
|
||||||
|
options.TypeInfoResolverChain.Insert(0, RpgRollerJsonSerializerContext.Default);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
RpgRoller/Contracts/RpgRollerJsonSerializerContext.cs
Normal file
58
RpgRoller/Contracts/RpgRollerJsonSerializerContext.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace RpgRoller.Contracts;
|
||||||
|
|
||||||
|
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
|
||||||
|
[JsonSerializable(typeof(ApiError))]
|
||||||
|
[JsonSerializable(typeof(AdminUserSummary))]
|
||||||
|
[JsonSerializable(typeof(AdminUserSummary[]))]
|
||||||
|
[JsonSerializable(typeof(CampaignGmSummary))]
|
||||||
|
[JsonSerializable(typeof(CampaignLogEntry))]
|
||||||
|
[JsonSerializable(typeof(CampaignLogEntry[]))]
|
||||||
|
[JsonSerializable(typeof(CampaignLogListEntry))]
|
||||||
|
[JsonSerializable(typeof(CampaignLogListEntry[]))]
|
||||||
|
[JsonSerializable(typeof(CampaignLogPage))]
|
||||||
|
[JsonSerializable(typeof(CampaignOption))]
|
||||||
|
[JsonSerializable(typeof(CampaignOption[]))]
|
||||||
|
[JsonSerializable(typeof(CampaignRollDetail))]
|
||||||
|
[JsonSerializable(typeof(CampaignRoster))]
|
||||||
|
[JsonSerializable(typeof(CampaignStateSnapshot))]
|
||||||
|
[JsonSerializable(typeof(CampaignSummary))]
|
||||||
|
[JsonSerializable(typeof(CampaignSummary[]))]
|
||||||
|
[JsonSerializable(typeof(CharacterSheet))]
|
||||||
|
[JsonSerializable(typeof(CharacterSheetSkill))]
|
||||||
|
[JsonSerializable(typeof(CharacterSheetSkillGroup))]
|
||||||
|
[JsonSerializable(typeof(CharacterStateVersion))]
|
||||||
|
[JsonSerializable(typeof(CharacterStateVersion[]))]
|
||||||
|
[JsonSerializable(typeof(CharacterSummary))]
|
||||||
|
[JsonSerializable(typeof(CharacterSummary[]))]
|
||||||
|
[JsonSerializable(typeof(CreateCampaignRequest))]
|
||||||
|
[JsonSerializable(typeof(CreateCharacterRequest))]
|
||||||
|
[JsonSerializable(typeof(CreateSkillGroupRequest))]
|
||||||
|
[JsonSerializable(typeof(CreateSkillRequest))]
|
||||||
|
[JsonSerializable(typeof(HealthResponse))]
|
||||||
|
[JsonSerializable(typeof(IReadOnlyList<AdminUserSummary>))]
|
||||||
|
[JsonSerializable(typeof(IReadOnlyList<CharacterStateVersion>))]
|
||||||
|
[JsonSerializable(typeof(IReadOnlyList<RollDieResult>))]
|
||||||
|
[JsonSerializable(typeof(IReadOnlyList<string>))]
|
||||||
|
[JsonSerializable(typeof(LoginRequest))]
|
||||||
|
[JsonSerializable(typeof(MeResponse))]
|
||||||
|
[JsonSerializable(typeof(RegisterRequest))]
|
||||||
|
[JsonSerializable(typeof(RollDieResult))]
|
||||||
|
[JsonSerializable(typeof(RollDieResult[]))]
|
||||||
|
[JsonSerializable(typeof(RollResult))]
|
||||||
|
[JsonSerializable(typeof(RollSkillRequest))]
|
||||||
|
[JsonSerializable(typeof(RulesetDefinition))]
|
||||||
|
[JsonSerializable(typeof(RulesetDefinition[]))]
|
||||||
|
[JsonSerializable(typeof(SkillGroupSummary))]
|
||||||
|
[JsonSerializable(typeof(SkillSummary))]
|
||||||
|
[JsonSerializable(typeof(string[]))]
|
||||||
|
[JsonSerializable(typeof(UpdateCharacterRequest))]
|
||||||
|
[JsonSerializable(typeof(UpdateSkillGroupRequest))]
|
||||||
|
[JsonSerializable(typeof(UpdateSkillRequest))]
|
||||||
|
[JsonSerializable(typeof(UpdateUserRolesRequest))]
|
||||||
|
[JsonSerializable(typeof(UserSummary))]
|
||||||
|
public partial class RpgRollerJsonSerializerContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
|
using Microsoft.AspNetCore.ResponseCompression;
|
||||||
using RpgRoller.Api;
|
using RpgRoller.Api;
|
||||||
using RpgRoller.Components;
|
using RpgRoller.Components;
|
||||||
|
using RpgRoller.Contracts;
|
||||||
using RpgRoller.Hosting;
|
using RpgRoller.Hosting;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
builder.Services.AddRpgRollerCore(builder.Configuration, builder.Environment);
|
builder.Services.AddRpgRollerCore(builder.Configuration, builder.Environment);
|
||||||
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
|
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
|
||||||
|
builder.Services.AddResponseCompression(options =>
|
||||||
|
{
|
||||||
|
options.EnableForHttps = true;
|
||||||
|
options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["application/json"]);
|
||||||
|
});
|
||||||
|
builder.Services.ConfigureHttpJsonOptions(options => RpgRollerJson.Configure(options.SerializerOptions));
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddScoped<RpgRollerApiClient>();
|
builder.Services.AddScoped<RpgRollerApiClient>();
|
||||||
builder.Services.AddScoped<WorkspaceSessionTokenAccessor>();
|
builder.Services.AddScoped<WorkspaceSessionTokenAccessor>();
|
||||||
@@ -25,6 +33,7 @@ if (!string.IsNullOrWhiteSpace(configuredPathBase))
|
|||||||
app.UsePathBase(normalizedPathBase);
|
app.UsePathBase(normalizedPathBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.UseResponseCompression();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.UseAntiforgery();
|
app.UseAntiforgery();
|
||||||
|
|
||||||
|
|||||||
@@ -797,7 +797,7 @@ public sealed class GameService : IGameService
|
|||||||
|
|
||||||
var (user, campaign) = context.Value!;
|
var (user, campaign) = context.Value!;
|
||||||
var entries = GetVisibleCampaignLogEntriesLocked(user, campaign)
|
var entries = GetVisibleCampaignLogEntriesLocked(user, campaign)
|
||||||
.TakeLast(CampaignLogPageSize)
|
.TakeLast(CampaignLogHistoryWindowSize)
|
||||||
.Select(ToLogEntry)
|
.Select(ToLogEntry)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
@@ -1596,9 +1596,9 @@ public sealed class GameService : IGameService
|
|||||||
private static int NormalizeCampaignLogPageSize(int? limit)
|
private static int NormalizeCampaignLogPageSize(int? limit)
|
||||||
{
|
{
|
||||||
if (!limit.HasValue)
|
if (!limit.HasValue)
|
||||||
return CampaignLogPageSize;
|
return CampaignLogLivePageSize;
|
||||||
|
|
||||||
return Math.Clamp(limit.Value, 1, CampaignLogPageSize);
|
return Math.Clamp(limit.Value, 1, CampaignLogLivePageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static UserAccount CloneUser(UserAccount user)
|
private static UserAccount CloneUser(UserAccount user)
|
||||||
@@ -1692,8 +1692,9 @@ public sealed class GameService : IGameService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private const int CampaignLogPageSize = 100;
|
private const int CampaignLogHistoryWindowSize = 100;
|
||||||
private static readonly JsonSerializerOptions DiceJsonOptions = new(JsonSerializerDefaults.Web);
|
private const int CampaignLogLivePageSize = 25;
|
||||||
|
private static readonly JsonSerializerOptions DiceJsonOptions = RpgRollerJson.CreateSerializerOptions();
|
||||||
private readonly Dictionary<Guid, Campaign> m_CampaignsById = [];
|
private readonly Dictionary<Guid, Campaign> m_CampaignsById = [];
|
||||||
private readonly Dictionary<Guid, CampaignStateTracker> m_CampaignStateById = [];
|
private readonly Dictionary<Guid, CampaignStateTracker> m_CampaignStateById = [];
|
||||||
private readonly Dictionary<Guid, Character> m_CharactersById = [];
|
private readonly Dictionary<Guid, Character> m_CharactersById = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user