14 KiB
RpgRoller
RpgRoller is an ASP.NET Core and Blazor Server app for lightweight tabletop campaign play, character sheets, and dice workflows.
RpgRoller/: web app, API endpoints, domain model, EF Core persistence, Blazor components, and static assetsRpgRoller.Tests/: xUnit coverage for API behavior, services, hosting, payload budgets, and persistence and migration pathsRpgRoller.sln: solution used by local development and repo scriptsPOSTMORTEM.md: architecture analysis of the May 2026 Firefox and RoboForm failure in the authenticated workspaceTASKS.md: the current execution plan for the approved frontend routing rewrite
Test layout:
RpgRoller.Tests/Api/: endpoint and host-facing integration testsRpgRoller.Tests/Services/: service and rules-engine testsRpgRoller.Tests/Support/: shared harnesses, builders, and test host helpers
Code Organization
Backend:
RpgRoller/Program.cs: app bootstrap, JSON options, compression, API and component mapping, and optionalPathBaseRpgRoller/Hosting/: service registration, startup initialization, SQLite path resolution, and schema upgradesRpgRoller/Api/: minimal API endpoint groups, request mappings, cookie and session helpers, and result mappingRpgRoller/Services/: gameplay and account workflows behindIGameServiceRpgRoller/Services/GameService.cs: facade over composed domain servicesRpgRoller/Services/GameAuthService.cs: registration, login, logout, session lookup, andGetMeRpgRoller/Services/GameCampaignService.cs: campaign creation, listing, roster reads, campaign options, and deletionRpgRoller/Services/GameCharacterService.cs: character creation, updates, activation, deletion, transfer, and owner-scoped readsRpgRoller/Services/GameSkillService.cs: skill-group CRUD, skill CRUD, sheet shaping, and ruleset validationRpgRoller/Services/GameRollService.cs: skill and custom rolls, compact log pages, roll detail, and campaign state snapshotsRpgRoller/Services/GameUserAdministrationService.cs: username reads, admin user listing, role updates, and account deletionRpgRoller/Services/GameStateStore.cs,GameStateCloneFactory.cs, andGamePersistenceService.cs: in-memory runtime state, campaign-state version tracking, and SQLite load and save boundariesRpgRoller/Services/GameAuthorization.cs,GameContextResolver.cs, andGameDtoMapper.cs: shared authorization, session and campaign resolution, and backend read-model mappingRpgRoller/Services/RollEngine.cs,StandardRollEngine.cs,D6RollEngine.cs,RolemasterRollEngine.cs,RollBreakdownFormatter.cs, andCampaignLogSummaryBuilder.cs: ruleset-specific dice execution, breakdown formatting, and compact campaign-log summariesRpgRoller/Services/SkillDefinitionValidator.cs,RoleSerializer.cs,RollVisibilityParser.cs, andCustomRollOptionsResolver.cs: shared rules and parsing helpers
Frontend:
RpgRoller/Components/App.razor: current HTML shell and the request-time branch that decides whether/serves the static auth page or the interactive appRpgRoller/Components/Routes.razor: Blazor router and layout hookupRpgRoller/Components/Layout/MainLayout.razor: default layoutRpgRoller/Components/Pages/LoginPage.razor: route marker for the static/loginauth documentRpgRoller/Components/Pages/PlayPage.razor,CampaignsPage.razor, andAdminPage.razor: authenticated route entry points for the interactive workspaceRpgRoller/Components/Pages/AuthenticatedPageBase.cs: shared logout-to-/loginredirect helper for authenticated route pagesRpgRoller/Components/Pages/Workspace.razor: authenticated workspace UI with play, campaign management, admin, toasts, and modalsRpgRoller/Components/Pages/Workspace.razor.cs: workspace composition root, lifecycle, coordinator wiring, JS-invokable entry points, and menu item constructionRpgRoller/Components/Pages/WorkspaceState.cs: workspace UI state plus pure computed and formatting projections used directly by the Razor viewRpgRoller/Components/Pages/WorkspaceSessionCoordinator.cs,WorkspaceCampaignCoordinator.cs,WorkspaceCampaignScopeCoordinator.cs,WorkspacePlayCoordinator.cs,WorkspaceAdminCoordinator.cs,WorkspaceLiveStateController.cs,WorkspaceFeedbackService.cs, andWorkspaceToast.cs: session bootstrap, campaign scope, play and log, admin, live update, and toast concerns used byWorkspaceRpgRoller/Components/Pages/HomeControls/StaticAuthPage.razor: plain HTML login and registration page used at/loginRpgRoller/Components/Pages/HomeControls/: workspace child components, forms, header, panels, and modal controlsRpgRoller/Components/RpgRollerApiClient.cs: browser API client for write actionsRpgRoller/Components/WorkspaceQueryService.cs: browser-facing read client for workspace dataRpgRoller/wwwroot/js/rpgroller-api.js: browser interop for auth forms, session storage, SSE wiring, and DOM helpersRpgRoller/wwwroot/styles.css: app styling and responsive layout
Current repo note:
POSTMORTEM.mddocuments why the current authenticated workspace architecture is fragile and why the next major frontend change is a route-first rewrite of the authenticated shell.TASKS.mdis the authoritative execution plan for that rewrite and must be kept current while the work proceeds.
Runtime and Persistence
- Persistence uses EF Core with SQLite (
Microsoft.EntityFrameworkCore.Sqlite). - The default database file is
RpgRoller/App_Data/rpgroller.db. ConnectionStrings__RpgRolleroverrides the SQLite path for local runs, tests, or temporary environments.- Startup applies pending EF Core migrations through
Database.Migrate(). - The app loads runtime state into memory during startup and persists successful state changes back to SQLite.
RpgRoller/App_Data/rpgroller.development.dbis a checked-in migration coverage fixture used by hosting tests that copy it to a temporary file before validation.
Product Capabilities
- Supported campaign rulesets: D6 System, D&D 5e, and Rolemaster
- Account registration, login, session-based auth, and role-aware authorization
- Admin tools for user listing, role updates, account deletion, and direct SQLite database download
- Campaign creation, roster reads, participant-scoped visibility, and owner and admin deletion
- Character creation, activation, owner transfer, campaign reassignment or unlinking, and owner and admin deletion
- Skill groups with reusable defaults plus skill and skill-group create, edit, reassign, and delete flows
- Owner-scoped play workspace that lists only the current user's characters while preserving GM and admin management capabilities
- Campaign log paging, lazy-loaded roll detail, compact summaries, and live state refresh through SSE
- Custom roll submission from the play screen without creating a persisted skill
- Instant skill filtering in the character panel
- Campaign management owner labels based on display names
Rolemaster support:
- Standard expressions such as
d10,15d10,2d10+48, andd100-15 - Open-ended percentile expressions such as
d100!+85 - Conditional
FumbleRangehandling for open-ended percentile skills and skill-group defaults - Persisted and validated automatic retry toggle for open-ended percentile skills; only eligible Rolemaster skills can enable it
- Rolemaster skill rolls open a modal prompt before rolling so the player can apply a one-shot situational modifier; the prompt autofocuses, supports Enter and Escape, and closes when clicking outside it
- One-shot situational modifiers are transient Rolemaster-only roll inputs; the temporary modifier is applied to both the first attempt and any automatic retry attempt
- Automatic retry windows for eligible open-ended skills: results
76-90retry once with+5, and results91-110retry once with+10 - Open-ended high chaining and low-end subtraction with ordered die metadata in roll detail
- Compact log badges and summaries for open-ended, retry, and fumble-related events, including
Retry +5andRetry +10
Current Frontend Architecture
The current frontend is in an intermediate state that was created while mitigating the Firefox and RoboForm failure documented in POSTMORTEM.md.
Today, / is an auth-aware entry redirect:
- anonymous
GET /redirects to/login - authenticated
GET /redirects to/play RpgRoller/Components/App.razorstill decides between the static/logindocument and the interactive route set based on the request path, not auth state
Inside the authenticated app, /play, /campaigns, and /admin are now real Blazor routes, and the hamburger menu navigates between those URLs. The interactive shell is still structurally transitional, because Workspace.razor continues to own all three major authenticated subtrees behind one component.
This architecture works functionally but remains structurally fragile because:
- the HTML shell still branches on request path to keep
/loginstatic - the authenticated workspace still performs staged startup in
OnAfterRenderAsync - the app coordinates state across Blazor component state, browser
sessionStorage,fetch, and SSE during early startup - the shared
Workspacecomponent still conditionally renders play, campaign management, and admin DOM instead of letting each route own its own subtree
Approved Rewrite Direction
The approved remediation direction is a route-first authenticated shell:
/becomes an auth-aware entry point that redirects to/loginor/play/loginhosts the anonymous auth experience/play,/campaigns, and/adminbecome real authenticated routes- the hamburger menu becomes route navigation instead of in-memory screen switching
- SSE and heavy play bootstrap stay scoped to
/play - the large
Workspacecomponent is split so each route owns a smaller, more stable subtree
This rewrite is not complete yet. Follow TASKS.md for the execution plan.
Local Development
Prerequisites:
- .NET SDK 10.0+
- Node.js 22+
- Firefox
- geckodriver
Initial setup:
dotnet tool restore
npm ci
Run locally:
- Start the app:
dotnet run --project RpgRoller/RpgRoller.csproj - Open
http://localhost:5000or the URL printed in the console. - Expect
/to redirect to/loginwhen anonymous and to/playwhen a valid session cookie already exists.
Browser smoke helpers:
- Run the checked-in smoke suite against an isolated temporary SQLite database:
node ./scripts/run-selenium.js - Run the Selenium smoke suite directly when the app is already running:
npm run e2e:smoke
VS Code launch profiles in .vscode/launch.json:
RpgRoller: ServerRpgRoller: Server + Edge (F5)RpgRoller: Server + Firefox (F5)
Environment overrides:
- Set
ConnectionStrings__RpgRollerto point at a custom SQLite database. - Set
PathBaseto host the app under a sub-path such as/rpgroller.
Migration authoring:
dotnet dotnet-ef migrations add <MigrationName> --project RpgRoller/RpgRoller.csproj --startup-project RpgRoller/RpgRoller.csproj
SQLite migration rule:
- Keep table-rebuild operations separate from unrelated schema or data changes so EF Core does not emit non-transactional migration warnings.
Frontend Runtime
- The UI currently runs as Blazor Server with interactive components for the authenticated workspace and a plain HTML plus JavaScript auth page for anonymous users at
/. - Static assets are linked through Blazor's
@Assets[...]pipeline for fingerprinted cache-busting URLs. - Workspace reads are resolved through API requests in
WorkspaceQueryService; browser interop stays focused on auth forms, session storage, SSE wiring, and small DOM helpers. - Live workspace refresh compares separate roster, per-character sheet, and log versions so unrelated changes do not trigger full reloads.
- Campaign log data is loaded in bounded slices: campaign summaries, one selected roster, one selected character sheet, and a 25-row incremental log window from
/api/campaigns/{campaignId}/log/page. - Log rows return compact summary data first and lazy-load full detail from
/api/rolls/{rollId}when expanded. - Newly appended local rolls auto-expand in the play workspace and reuse the roll response as the initial detail payload.
- Custom roll submission uses the selected character context; D6 uses baseline wild-die and fumble behavior, while D&D 5e and Rolemaster use the submitted expression directly.
- API JSON contracts use the source-generated
RpgRollerJsonSerializerContext. - HTTP JSON responses are gzip-compressed when the client advertises support.
- The OpenAPI contract source lives at
openapi/RpgRoller.json.
Test and Coverage
- Test command:
dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --collect:"XPlat Code Coverage" --settings RpgRoller.Tests/coverlet.runsettings - Coverage gate:
pwsh ./scripts/check-coverage.ps1 -MinLineRate 0.90 -MinBranchRate 0.70 - Local parity script:
pwsh ./scripts/ci-local.ps1 scripts/ci-local.ps1writes coverage collector output to a unique temporary results directory outside the repo, reads coverage from there, removes that directory at the end of the run, and sweeps straycoverage.cobertura.xmlfiles fromRpgRoller.Tests/TestResults.- Regression tests enforce payload budgets for character sheet reads, initial and incremental campaign log loads, roll mutation responses, and lazy-loaded Rolemaster roll detail payloads.
RpgRoller.Tests/coverlet.runsettingsmeasures the fullRpgRollerbackend assembly.