diff --git a/RpgRoller.Tests/Api/AuthApiTests.cs b/RpgRoller.Tests/Api/AuthApiTests.cs index 151f7e0..d4afb91 100644 --- a/RpgRoller.Tests/Api/AuthApiTests.cs +++ b/RpgRoller.Tests/Api/AuthApiTests.cs @@ -1,11 +1,7 @@ namespace RpgRoller.Tests; -public sealed class AuthApiTests : ApiTestBase +public sealed class AuthApiTests(WebApplicationFactory factory) : ApiTestBase(factory) { - public AuthApiTests(WebApplicationFactory factory) : base(factory) - { - } - [Fact] public async Task RegisterLoginAndMeFlow_WorksWithDuplicateUsernameGuard() { diff --git a/RpgRoller.Tests/Api/CampaignApiTests.cs b/RpgRoller.Tests/Api/CampaignApiTests.cs index e316ca6..107dae3 100644 --- a/RpgRoller.Tests/Api/CampaignApiTests.cs +++ b/RpgRoller.Tests/Api/CampaignApiTests.cs @@ -2,12 +2,8 @@ using System.Text; namespace RpgRoller.Tests; -public sealed class CampaignApiTests : ApiTestBase +public sealed class CampaignApiTests(WebApplicationFactory factory) : ApiTestBase(factory) { - public CampaignApiTests(WebApplicationFactory factory) : base(factory) - { - } - [Fact] public async Task CampaignCharacterAndSkillFlow_EnforcesRulesetValidation() { diff --git a/RpgRoller.Tests/Api/FrontendHostTests.cs b/RpgRoller.Tests/Api/FrontendHostTests.cs index 774a713..0050adb 100644 --- a/RpgRoller.Tests/Api/FrontendHostTests.cs +++ b/RpgRoller.Tests/Api/FrontendHostTests.cs @@ -1,11 +1,7 @@ namespace RpgRoller.Tests; -public sealed class FrontendHostTests : ApiTestBase +public sealed class FrontendHostTests(WebApplicationFactory factory) : ApiTestBase(factory) { - public FrontendHostTests(WebApplicationFactory factory) : base(factory) - { - } - [Fact] public async Task RootPath_ServesBlazorFrontendShell() { diff --git a/RpgRoller.Tests/Api/ResponseCompressionApiTests.cs b/RpgRoller.Tests/Api/ResponseCompressionApiTests.cs index c989a30..d16447b 100644 --- a/RpgRoller.Tests/Api/ResponseCompressionApiTests.cs +++ b/RpgRoller.Tests/Api/ResponseCompressionApiTests.cs @@ -1,11 +1,7 @@ namespace RpgRoller.Tests; -public sealed class ResponseCompressionApiTests : ApiTestBase +public sealed class ResponseCompressionApiTests(WebApplicationFactory factory) : ApiTestBase(factory) { - public ResponseCompressionApiTests(WebApplicationFactory factory) : base(factory) - { - } - [Fact] public async Task AuthenticatedJsonResponses_EnableGzipCompression() { diff --git a/RpgRoller.Tests/Api/RolemasterApiTests.cs b/RpgRoller.Tests/Api/RolemasterApiTests.cs index 392bd6f..af7021a 100644 --- a/RpgRoller.Tests/Api/RolemasterApiTests.cs +++ b/RpgRoller.Tests/Api/RolemasterApiTests.cs @@ -1,11 +1,7 @@ namespace RpgRoller.Tests; -public sealed class RolemasterApiTests : ApiTestBase +public sealed class RolemasterApiTests(WebApplicationFactory factory) : ApiTestBase(factory) { - public RolemasterApiTests(WebApplicationFactory factory) : base(factory) - { - } - [Fact] public async Task RolemasterRollEndpoints_ExecuteGenericRolemasterExpressions() { diff --git a/RpgRoller.Tests/Api/RollVisibilityApiTests.cs b/RpgRoller.Tests/Api/RollVisibilityApiTests.cs index 4392f9a..a046164 100644 --- a/RpgRoller.Tests/Api/RollVisibilityApiTests.cs +++ b/RpgRoller.Tests/Api/RollVisibilityApiTests.cs @@ -1,11 +1,7 @@ namespace RpgRoller.Tests; -public sealed class RollVisibilityApiTests : ApiTestBase +public sealed class RollVisibilityApiTests(WebApplicationFactory factory) : ApiTestBase(factory) { - public RollVisibilityApiTests(WebApplicationFactory factory) : base(factory) - { - } - [Fact] public async Task RollVisibilityAndAuthorization_AreEnforced() { diff --git a/RpgRoller.Tests/Api/SystemApiTests.cs b/RpgRoller.Tests/Api/SystemApiTests.cs index 6ed8171..223705f 100644 --- a/RpgRoller.Tests/Api/SystemApiTests.cs +++ b/RpgRoller.Tests/Api/SystemApiTests.cs @@ -1,11 +1,7 @@ namespace RpgRoller.Tests; -public sealed class SystemApiTests : ApiTestBase +public sealed class SystemApiTests(WebApplicationFactory factory) : ApiTestBase(factory) { - public SystemApiTests(WebApplicationFactory factory) : base(factory) - { - } - [Fact] public async Task RulesetAndSseEndpoints_ReturnExpectedResponses() { diff --git a/RpgRoller.Tests/BackendCoverageTests.cs b/RpgRoller.Tests/BackendCoverageTests.cs index 97e45c4..b260afa 100644 --- a/RpgRoller.Tests/BackendCoverageTests.cs +++ b/RpgRoller.Tests/BackendCoverageTests.cs @@ -11,11 +11,11 @@ public sealed class BackendCoverageTests var extensionsType = typeof(Program).Assembly.GetType("RpgRoller.Api.SessionTokenHttpContextExtensions"); Assert.NotNull(extensionsType); - var method = extensionsType!.GetMethod("GetRequiredSessionToken", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + var method = extensionsType.GetMethod("GetRequiredSessionToken", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); Assert.NotNull(method); var context = new DefaultHttpContext(); - var exception = Assert.Throws(() => method!.Invoke(null, [context])); + var exception = Assert.Throws(() => method.Invoke(null, [context])); Assert.IsType(exception.InnerException); } } \ No newline at end of file diff --git a/RpgRoller.Tests/Services/ServiceRollHelperTests.cs b/RpgRoller.Tests/Services/ServiceRollHelperTests.cs index 297981a..77cb926 100644 --- a/RpgRoller.Tests/Services/ServiceRollHelperTests.cs +++ b/RpgRoller.Tests/Services/ServiceRollHelperTests.cs @@ -2,20 +2,15 @@ namespace RpgRoller.Tests; public sealed class ServiceRollHelperTests { - private sealed class FixedDiceRoller : IDiceRoller + private sealed class FixedDiceRoller(IEnumerable values) : IDiceRoller { - public FixedDiceRoller(IEnumerable values) - { - m_Values = new(values); - } - public int Roll(int sides) { var next = m_Values.Count > 0 ? m_Values.Dequeue() : 1; return Math.Clamp(next, 1, sides); } - private readonly Queue m_Values; + private readonly Queue m_Values = new(values); } [Fact] diff --git a/RpgRoller.Tests/Services/ServiceSkillGroupAndOwnershipTests.cs b/RpgRoller.Tests/Services/ServiceSkillGroupAndOwnershipTests.cs index d0a7cc0..c371ac0 100644 --- a/RpgRoller.Tests/Services/ServiceSkillGroupAndOwnershipTests.cs +++ b/RpgRoller.Tests/Services/ServiceSkillGroupAndOwnershipTests.cs @@ -88,7 +88,7 @@ public sealed class ServiceSkillGroupAndOwnershipTests var gmTransfer = ServiceTestSupport.GetValue(service.UpdateCharacter(gmSession, character.Id, "Transferred", campaign.Id, "receiver")); var receiver = service.GetUserBySession(receiverSession); Assert.NotNull(receiver); - Assert.Equal(receiver!.Id, gmTransfer.OwnerUserId); + Assert.Equal(receiver.Id, gmTransfer.OwnerUserId); var previousOwnerMe = ServiceTestSupport.GetValue(service.GetMe(ownerSession)); Assert.Null(previousOwnerMe.ActiveCharacterId); @@ -133,7 +133,7 @@ public sealed class ServiceSkillGroupAndOwnershipTests var adminTwo = service.GetUserBySession(adminTwoSession); Assert.NotNull(adminTwo); - _ = ServiceTestSupport.GetValue(service.UpdateUserRoles(gmSession, adminTwo!.Id, ["admin"])); + _ = ServiceTestSupport.GetValue(service.UpdateUserRoles(gmSession, adminTwo.Id, ["admin"])); var adminUnlink = ServiceTestSupport.GetValue(service.UpdateCharacter(adminTwoSession, character.Id, "Admin Unlink", null)); Assert.Null(adminUnlink.CampaignId); diff --git a/RpgRoller.Tests/Support/ApiTestBase.cs b/RpgRoller.Tests/Support/ApiTestBase.cs index 02c00d9..7f11812 100644 --- a/RpgRoller.Tests/Support/ApiTestBase.cs +++ b/RpgRoller.Tests/Support/ApiTestBase.cs @@ -8,32 +8,22 @@ using RpgRoller.Hosting; namespace RpgRoller.Tests; -public abstract class ApiTestBase : IClassFixture> +public abstract class ApiTestBase(WebApplicationFactory factory) : IClassFixture> { - private sealed class FixedDiceRoller : IDiceRoller + private sealed class FixedDiceRoller(IEnumerable values) : IDiceRoller { - public FixedDiceRoller(IEnumerable values) - { - m_Values = new(values); - } - public int Roll(int sides) { var next = m_Values.Count > 0 ? m_Values.Dequeue() : 1; return Math.Clamp(next, 1, sides); } - private readonly Queue m_Values; - } - - protected ApiTestBase(WebApplicationFactory factory) - { - m_BaseFactory = factory; + private readonly Queue m_Values = new(values); } protected WebApplicationFactory CreateFactory(params int[] rollValues) { - return m_BaseFactory.WithWebHostBuilder(builder => + return factory.WithWebHostBuilder(builder => { builder.ConfigureLogging(logging => { @@ -95,6 +85,4 @@ public abstract class ApiTestBase : IClassFixture Assert.NotNull(result); return result; } - - private readonly WebApplicationFactory m_BaseFactory; } \ No newline at end of file diff --git a/RpgRoller.Tests/Support/ServiceTestSupport.cs b/RpgRoller.Tests/Support/ServiceTestSupport.cs index 89bfd37..9fca2a0 100644 --- a/RpgRoller.Tests/Support/ServiceTestSupport.cs +++ b/RpgRoller.Tests/Support/ServiceTestSupport.cs @@ -46,29 +46,19 @@ internal static class ServiceTestSupport public int HashCalls { get; private set; } } - private sealed class FixedDiceRoller : IDiceRoller + private sealed class FixedDiceRoller(IEnumerable values) : IDiceRoller { - public FixedDiceRoller(IEnumerable values) - { - m_Values = new(values); - } - public int Roll(int sides) { var next = m_Values.Count > 0 ? m_Values.Dequeue() : 1; return Math.Clamp(next, 1, sides); } - private readonly Queue m_Values; + private readonly Queue m_Values = new(values); } - internal sealed class SqliteDbContextFactory : IDbContextFactory, IDisposable + internal sealed class SqliteDbContextFactory(string dbPath) : IDbContextFactory, IDisposable { - public SqliteDbContextFactory(string dbPath) - { - m_Options = new DbContextOptionsBuilder().UseSqlite($"Data Source={dbPath}").Options; - } - public RpgRollerDbContext CreateDbContext() { return new(m_Options); @@ -78,7 +68,7 @@ internal static class ServiceTestSupport { } - private readonly DbContextOptions m_Options; + private readonly DbContextOptions m_Options = new DbContextOptionsBuilder().UseSqlite($"Data Source={dbPath}").Options; } internal static ServiceHarness CreateHarness(params int[] rollValues) diff --git a/RpgRoller/Components/Pages/WorkspaceAdminCoordinator.cs b/RpgRoller/Components/Pages/WorkspaceAdminCoordinator.cs index 1478168..3d7ddc4 100644 --- a/RpgRoller/Components/Pages/WorkspaceAdminCoordinator.cs +++ b/RpgRoller/Components/Pages/WorkspaceAdminCoordinator.cs @@ -6,114 +6,93 @@ using RpgRoller.Domain; namespace RpgRoller.Components.Pages; [ExcludeFromCodeCoverage] -public sealed class WorkspaceAdminCoordinator +public sealed class WorkspaceAdminCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, RpgRollerApiClient apiClient, WorkspaceQueryService workspaceQuery, Action clearAuthenticatedState, Func stopStateEventsAsync, Func onLoggedOutAsync) { - public WorkspaceAdminCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, RpgRollerApiClient apiClient, WorkspaceQueryService workspaceQuery, Action clearAuthenticatedState, Func stopStateEventsAsync, Func onLoggedOutAsync) - { - m_State = state; - m_Feedback = feedback; - m_JS = js; - m_ApiClient = apiClient; - m_WorkspaceQuery = workspaceQuery; - m_ClearAuthenticatedState = clearAuthenticatedState; - m_StopStateEventsAsync = stopStateEventsAsync; - m_OnLoggedOutAsync = onLoggedOutAsync; - } - public async Task EnsureAdminUsersLoadedAsync() { - if (!m_State.IsCurrentUserAdmin || m_State.HasLoadedAdminUsers || m_State.IsAdminDataLoading) + if (!state.IsCurrentUserAdmin || state.HasLoadedAdminUsers || state.IsAdminDataLoading) return; - m_State.IsAdminDataLoading = true; + state.IsAdminDataLoading = true; try { await ReloadAdminUsersAsync(); } catch (ApiRequestException ex) when (ex.StatusCode == 401) { - m_ClearAuthenticatedState(); - await m_StopStateEventsAsync(); - await m_OnLoggedOutAsync("Session expired. Please log in again."); + clearAuthenticatedState(); + await stopStateEventsAsync(); + await onLoggedOutAsync("Session expired. Please log in again."); } catch (ApiRequestException ex) { - m_Feedback.SetStatus(ex.Message, true); + feedback.SetStatus(ex.Message, true); } finally { - m_State.IsAdminDataLoading = false; + state.IsAdminDataLoading = false; } } public async Task ToggleAdminRoleAsync(AdminUserSummary user) { - if (m_State.IsMutating || m_State.User is null || !m_State.IsCurrentUserAdmin || user.Id == m_State.User.Id) + if (state.IsMutating || state.User is null || !state.IsCurrentUserAdmin || user.Id == state.User.Id) return; - m_State.IsMutating = true; + state.IsMutating = true; try { IReadOnlyList roles = HasAdminRole(user) ? Array.Empty() : [UserRoles.Admin]; - _ = await m_ApiClient.RequestAsync("PUT", $"/api/admin/users/{user.Id}/roles", new UpdateUserRolesRequest(roles)); + _ = await apiClient.RequestAsync("PUT", $"/api/admin/users/{user.Id}/roles", new UpdateUserRolesRequest(roles)); await ReloadAdminUsersAsync(); - m_Feedback.SetStatus("User roles updated.", false); + feedback.SetStatus("User roles updated.", false); } catch (ApiRequestException ex) { - m_Feedback.SetStatus(ex.Message, true); + feedback.SetStatus(ex.Message, true); } finally { - m_State.IsMutating = false; + state.IsMutating = false; } } public async Task DeleteUserAsync(AdminUserSummary user) { - if (m_State.IsMutating || m_State.User is null || !m_State.IsCurrentUserAdmin || user.Id == m_State.User.Id) + if (state.IsMutating || state.User is null || !state.IsCurrentUserAdmin || user.Id == state.User.Id) return; - var confirmed = await m_JS.InvokeAsync("confirm", $"Delete user '{user.Username}'?"); + var confirmed = await js.InvokeAsync("confirm", $"Delete user '{user.Username}'?"); if (!confirmed) return; - m_State.IsMutating = true; + state.IsMutating = true; try { - _ = await m_ApiClient.RequestAsync("DELETE", $"/api/admin/users/{user.Id}"); + _ = await apiClient.RequestAsync("DELETE", $"/api/admin/users/{user.Id}"); await ReloadAdminUsersAsync(); - m_Feedback.SetStatus("User deleted.", false); + feedback.SetStatus("User deleted.", false); } catch (ApiRequestException ex) { - m_Feedback.SetStatus(ex.Message, true); + feedback.SetStatus(ex.Message, true); } finally { - m_State.IsMutating = false; + state.IsMutating = false; } } private async Task ReloadAdminUsersAsync() { - m_State.AdminUsers = (await m_WorkspaceQuery.GetAdminUsersAsync()).OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase).ToList(); + state.AdminUsers = (await workspaceQuery.GetAdminUsersAsync()).OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase).ToList(); - m_State.HasLoadedAdminUsers = true; + state.HasLoadedAdminUsers = true; } private static bool HasAdminRole(AdminUserSummary user) { return user.Roles.Contains(UserRoles.Admin, StringComparer.OrdinalIgnoreCase); } - - private readonly RpgRollerApiClient m_ApiClient; - private readonly Action m_ClearAuthenticatedState; - private readonly WorkspaceFeedbackService m_Feedback; - private readonly IJSRuntime m_JS; - private readonly Func m_OnLoggedOutAsync; - private readonly WorkspaceState m_State; - private readonly Func m_StopStateEventsAsync; - private readonly WorkspaceQueryService m_WorkspaceQuery; } \ No newline at end of file diff --git a/RpgRoller/Components/Pages/WorkspaceCampaignCoordinator.cs b/RpgRoller/Components/Pages/WorkspaceCampaignCoordinator.cs index fef01f0..2d0b20f 100644 --- a/RpgRoller/Components/Pages/WorkspaceCampaignCoordinator.cs +++ b/RpgRoller/Components/Pages/WorkspaceCampaignCoordinator.cs @@ -6,179 +6,156 @@ using RpgRoller.Contracts; namespace RpgRoller.Components.Pages; [ExcludeFromCodeCoverage] -public sealed class WorkspaceCampaignCoordinator +public sealed class WorkspaceCampaignCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, RpgRollerApiClient apiClient, Func loadKnownUsernamesAsync, Func reloadCampaignsAsync, Func reloadCharacterCampaignOptionsAsync, Func refreshCampaignScopeAsync, Func syncStateEventsAsync) { - public WorkspaceCampaignCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, RpgRollerApiClient apiClient, Func loadKnownUsernamesAsync, Func reloadCampaignsAsync, Func reloadCharacterCampaignOptionsAsync, Func refreshCampaignScopeAsync, Func syncStateEventsAsync) - { - m_State = state; - m_Feedback = feedback; - m_JS = js; - m_ApiClient = apiClient; - m_LoadKnownUsernamesAsync = loadKnownUsernamesAsync; - m_ReloadCampaignsAsync = reloadCampaignsAsync; - m_ReloadCharacterCampaignOptionsAsync = reloadCharacterCampaignOptionsAsync; - m_RefreshCampaignScopeAsync = refreshCampaignScopeAsync; - m_SyncStateEventsAsync = syncStateEventsAsync; - } - public async Task OnCampaignSelectionChangedAsync(ChangeEventArgs args) { if (!Guid.TryParse(args.Value?.ToString(), out var campaignId)) return; - m_State.SelectedCampaignId = campaignId; - await m_JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, campaignId.ToString()); - await m_RefreshCampaignScopeAsync(); - await m_SyncStateEventsAsync(); - m_State.IsScreenMenuOpen = false; + state.SelectedCampaignId = campaignId; + await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, campaignId.ToString()); + await refreshCampaignScopeAsync(); + await syncStateEventsAsync(); + state.IsScreenMenuOpen = false; } public async Task OnCampaignCreatedAsync(Guid campaignId) { - await m_ReloadCampaignsAsync(campaignId); - await m_ReloadCharacterCampaignOptionsAsync(); - await m_RefreshCampaignScopeAsync(); - await m_SyncStateEventsAsync(); - m_Feedback.SetStatus("Campaign created.", false); + await reloadCampaignsAsync(campaignId); + await reloadCharacterCampaignOptionsAsync(); + await refreshCampaignScopeAsync(); + await syncStateEventsAsync(); + feedback.SetStatus("Campaign created.", false); } public void OpenCreateCharacterModal() { - m_State.CreateCharacterInitialModel = new() + state.CreateCharacterInitialModel = new() { Name = string.Empty, - CampaignId = m_State.SelectedCampaignId?.ToString() ?? m_State.CharacterCampaignOptions.FirstOrDefault()?.Id.ToString() ?? string.Empty, + CampaignId = state.SelectedCampaignId?.ToString() ?? state.CharacterCampaignOptions.FirstOrDefault()?.Id.ToString() ?? string.Empty, OwnerUsername = string.Empty }; - m_State.CreateCharacterFormVersion += 1; - m_State.CanEditCharacterOwner = false; - m_State.ShowCreateCharacterModal = true; + state.CreateCharacterFormVersion += 1; + state.CanEditCharacterOwner = false; + state.ShowCreateCharacterModal = true; } public async Task OpenEditCharacterModal(CharacterSummary character) { - if (m_State.IsCurrentUserGm || m_State.IsCurrentUserAdmin) - await m_LoadKnownUsernamesAsync(); + if (state.IsCurrentUserGm || state.IsCurrentUserAdmin) + await loadKnownUsernamesAsync(); - m_State.EditingCharacterId = character.Id; - m_State.EditCharacterInitialModel = new() + state.EditingCharacterId = character.Id; + state.EditCharacterInitialModel = new() { Name = character.Name, CampaignId = character.CampaignId?.ToString() ?? string.Empty, OwnerUsername = string.Empty }; - m_State.EditCharacterFormVersion += 1; - m_State.CanEditCharacterOwner = m_State.IsCurrentUserGm || m_State.IsCurrentUserAdmin; - m_State.ShowEditCharacterModal = true; + state.EditCharacterFormVersion += 1; + state.CanEditCharacterOwner = state.IsCurrentUserGm || state.IsCurrentUserAdmin; + state.ShowEditCharacterModal = true; } public void CloseCharacterModals() { - m_State.ShowCreateCharacterModal = false; - m_State.ShowEditCharacterModal = false; - m_State.CanEditCharacterOwner = false; - m_State.EditingCharacterId = null; + state.ShowCreateCharacterModal = false; + state.ShowEditCharacterModal = false; + state.CanEditCharacterOwner = false; + state.EditingCharacterId = null; } public async Task OnCharacterCreatedAsync(Guid? campaignId) { CloseCharacterModals(); - await m_ReloadCampaignsAsync(campaignId); - await m_ReloadCharacterCampaignOptionsAsync(); - await m_RefreshCampaignScopeAsync(); - await m_SyncStateEventsAsync(); - m_Feedback.SetStatus("Character created.", false); + await reloadCampaignsAsync(campaignId); + await reloadCharacterCampaignOptionsAsync(); + await refreshCampaignScopeAsync(); + await syncStateEventsAsync(); + feedback.SetStatus("Character created.", false); } public async Task OnCharacterUpdatedAsync(Guid? campaignId) { CloseCharacterModals(); - await m_ReloadCampaignsAsync(campaignId); - await m_ReloadCharacterCampaignOptionsAsync(); - await m_RefreshCampaignScopeAsync(); - await m_SyncStateEventsAsync(); - m_Feedback.SetStatus(campaignId.HasValue ? "Character updated." : "Character unlinked from campaign.", false); + await reloadCampaignsAsync(campaignId); + await reloadCharacterCampaignOptionsAsync(); + await refreshCampaignScopeAsync(); + await syncStateEventsAsync(); + feedback.SetStatus(campaignId.HasValue ? "Character updated." : "Character unlinked from campaign.", false); } public async Task DeleteSelectedCampaignAsync() { - if (m_State.SelectedCampaign is null || m_State.IsMutating || !m_State.CanDeleteSelectedCampaign) + if (state.SelectedCampaign is null || state.IsMutating || !state.CanDeleteSelectedCampaign) return; - var confirmed = await m_JS.InvokeAsync("confirm", $"Delete campaign '{m_State.SelectedCampaign.Name}'?"); + var confirmed = await js.InvokeAsync("confirm", $"Delete campaign '{state.SelectedCampaign.Name}'?"); if (!confirmed) return; - m_State.IsMutating = true; + state.IsMutating = true; try { - _ = await m_ApiClient.RequestAsync("DELETE", $"/api/campaigns/{m_State.SelectedCampaign.Id}"); - await m_ReloadCampaignsAsync(null); - await m_ReloadCharacterCampaignOptionsAsync(); - await m_RefreshCampaignScopeAsync(); - await m_SyncStateEventsAsync(); - m_Feedback.SetStatus("Campaign deleted.", false); + _ = await apiClient.RequestAsync("DELETE", $"/api/campaigns/{state.SelectedCampaign.Id}"); + await reloadCampaignsAsync(null); + await reloadCharacterCampaignOptionsAsync(); + await refreshCampaignScopeAsync(); + await syncStateEventsAsync(); + feedback.SetStatus("Campaign deleted.", false); } catch (ApiRequestException ex) { - m_Feedback.SetStatus(ex.Message, true); + feedback.SetStatus(ex.Message, true); } finally { - m_State.IsMutating = false; + state.IsMutating = false; } } public async Task DeleteCharacterAsync(CharacterSummary character) { - if (m_State.IsMutating || !CanDeleteCharacter(character)) + if (state.IsMutating || !CanDeleteCharacter(character)) return; - var confirmed = await m_JS.InvokeAsync("confirm", $"Delete character '{character.Name}'?"); + var confirmed = await js.InvokeAsync("confirm", $"Delete character '{character.Name}'?"); if (!confirmed) return; - m_State.IsMutating = true; + state.IsMutating = true; try { - _ = await m_ApiClient.RequestAsync("DELETE", $"/api/characters/{character.Id}"); - await m_ReloadCampaignsAsync(m_State.SelectedCampaignId); - await m_ReloadCharacterCampaignOptionsAsync(); - await m_RefreshCampaignScopeAsync(); - await m_SyncStateEventsAsync(); - m_Feedback.SetStatus("Character deleted.", false); + _ = await apiClient.RequestAsync("DELETE", $"/api/characters/{character.Id}"); + await reloadCampaignsAsync(state.SelectedCampaignId); + await reloadCharacterCampaignOptionsAsync(); + await refreshCampaignScopeAsync(); + await syncStateEventsAsync(); + feedback.SetStatus("Character deleted.", false); } catch (ApiRequestException ex) { - m_Feedback.SetStatus(ex.Message, true); + feedback.SetStatus(ex.Message, true); } finally { - m_State.IsMutating = false; + state.IsMutating = false; } } public bool CanEditCharacter(CharacterSummary character) { - return m_State.User is not null && (character.OwnerUserId == m_State.User.Id || m_State.IsCurrentUserGm || m_State.IsCurrentUserAdmin); + return state.User is not null && (character.OwnerUserId == state.User.Id || state.IsCurrentUserGm || state.IsCurrentUserAdmin); } public bool CanDeleteCharacter(CharacterSummary character) { - return m_State.User is not null && (character.OwnerUserId == m_State.User.Id || m_State.IsCurrentUserAdmin); + return state.User is not null && (character.OwnerUserId == state.User.Id || state.IsCurrentUserAdmin); } private const string CampaignSessionKey = "campaign"; - - private readonly RpgRollerApiClient m_ApiClient; - private readonly WorkspaceFeedbackService m_Feedback; - private readonly IJSRuntime m_JS; - private readonly Func m_LoadKnownUsernamesAsync; - private readonly Func m_RefreshCampaignScopeAsync; - private readonly Func m_ReloadCampaignsAsync; - private readonly Func m_ReloadCharacterCampaignOptionsAsync; - private readonly WorkspaceState m_State; - private readonly Func m_SyncStateEventsAsync; } \ No newline at end of file diff --git a/RpgRoller/Components/Pages/WorkspaceCampaignScopeCoordinator.cs b/RpgRoller/Components/Pages/WorkspaceCampaignScopeCoordinator.cs index 52e2056..e09e2f1 100644 --- a/RpgRoller/Components/Pages/WorkspaceCampaignScopeCoordinator.cs +++ b/RpgRoller/Components/Pages/WorkspaceCampaignScopeCoordinator.cs @@ -4,149 +4,120 @@ using Microsoft.JSInterop; namespace RpgRoller.Components.Pages; [ExcludeFromCodeCoverage] -public sealed class WorkspaceCampaignScopeCoordinator +public sealed class WorkspaceCampaignScopeCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, WorkspaceQueryService workspaceQuery, Func ensureSelectedCharacterActiveAsync, Func refreshSelectedCharacterSheetAsync, Func refreshCampaignLogAsync, Action resetCampaignLogDetailState, Action resetCampaignStateTracking, Action clearAuthenticatedState, Func stopStateEventsAsync, Func onLoggedOutAsync) { - public WorkspaceCampaignScopeCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, WorkspaceQueryService workspaceQuery, Func ensureSelectedCharacterActiveAsync, Func refreshSelectedCharacterSheetAsync, Func refreshCampaignLogAsync, Action resetCampaignLogDetailState, Action resetCampaignStateTracking, Action clearAuthenticatedState, Func stopStateEventsAsync, Func onLoggedOutAsync) - { - m_State = state; - m_Feedback = feedback; - m_JS = js; - m_WorkspaceQuery = workspaceQuery; - m_EnsureSelectedCharacterActiveAsync = ensureSelectedCharacterActiveAsync; - m_RefreshSelectedCharacterSheetAsync = refreshSelectedCharacterSheetAsync; - m_RefreshCampaignLogAsync = refreshCampaignLogAsync; - m_ResetCampaignLogDetailState = resetCampaignLogDetailState; - m_ResetCampaignStateTracking = resetCampaignStateTracking; - m_ClearAuthenticatedState = clearAuthenticatedState; - m_StopStateEventsAsync = stopStateEventsAsync; - m_OnLoggedOutAsync = onLoggedOutAsync; - } - public async Task ReloadCampaignsAsync(Guid? preferredCampaignId) { - var campaigns = await m_WorkspaceQuery.GetCampaignsAsync(); - m_State.Campaigns = campaigns.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList(); + var campaigns = await workspaceQuery.GetCampaignsAsync(); + state.Campaigns = campaigns.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList(); - if (m_State.Campaigns.Count == 0) + if (state.Campaigns.Count == 0) { - m_State.SelectedCampaignId = null; - await m_JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, null); + state.SelectedCampaignId = null; + await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, null); return; } - var campaignIds = m_State.Campaigns.Select(campaign => campaign.Id).ToHashSet(); + var campaignIds = state.Campaigns.Select(campaign => campaign.Id).ToHashSet(); if (preferredCampaignId.HasValue && campaignIds.Contains(preferredCampaignId.Value)) - m_State.SelectedCampaignId = preferredCampaignId.Value; - else if (!m_State.SelectedCampaignId.HasValue || !campaignIds.Contains(m_State.SelectedCampaignId.Value)) - m_State.SelectedCampaignId = m_State.Campaigns[0].Id; + state.SelectedCampaignId = preferredCampaignId.Value; + else if (!state.SelectedCampaignId.HasValue || !campaignIds.Contains(state.SelectedCampaignId.Value)) + state.SelectedCampaignId = state.Campaigns[0].Id; - await m_JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, m_State.SelectedCampaignId?.ToString()); + await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, state.SelectedCampaignId?.ToString()); } public async Task ReloadCharacterCampaignOptionsAsync() { - var campaignOptions = await m_WorkspaceQuery.GetCharacterCampaignOptionsAsync(); - m_State.CharacterCampaignOptions = campaignOptions.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList(); + var campaignOptions = await workspaceQuery.GetCharacterCampaignOptionsAsync(); + state.CharacterCampaignOptions = campaignOptions.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList(); } public async Task RefreshCampaignRosterAsync() { - if (!m_State.SelectedCampaignId.HasValue) + if (!state.SelectedCampaignId.HasValue) { - m_State.SelectedCampaign = null; - m_State.SelectedCharacterId = null; + state.SelectedCampaign = null; + state.SelectedCharacterId = null; return; } - m_State.SelectedCampaign = await m_WorkspaceQuery.GetCampaignAsync(m_State.SelectedCampaignId.Value); + state.SelectedCampaign = await workspaceQuery.GetCampaignAsync(state.SelectedCampaignId.Value); SyncSelectedCharacter(); - if (m_State.IsPlayScreen && m_State.PlaySelectedCharacterId.HasValue && m_State.SelectedCharacterId != m_State.PlaySelectedCharacterId) - m_State.SelectedCharacterId = m_State.PlaySelectedCharacterId; + if (state.IsPlayScreen && state.PlaySelectedCharacterId.HasValue && state.SelectedCharacterId != state.PlaySelectedCharacterId) + state.SelectedCharacterId = state.PlaySelectedCharacterId; - await m_EnsureSelectedCharacterActiveAsync(); + await ensureSelectedCharacterActiveAsync(); } public async Task RefreshCampaignScopeAsync() { - if (!m_State.SelectedCampaignId.HasValue) + if (!state.SelectedCampaignId.HasValue) { - m_State.SelectedCampaign = null; - m_State.SelectedCharacterSkills = []; - m_State.SelectedCharacterSkillGroups = []; - m_State.CampaignLog = []; - m_State.SelectedCharacterId = null; - m_State.ConnectionState = "offline"; - m_State.CurrentCampaignState = null; - m_State.CampaignLogCursor = null; - m_ResetCampaignLogDetailState(); + state.SelectedCampaign = null; + state.SelectedCharacterSkills = []; + state.SelectedCharacterSkillGroups = []; + state.CampaignLog = []; + state.SelectedCharacterId = null; + state.ConnectionState = "offline"; + state.CurrentCampaignState = null; + state.CampaignLogCursor = null; + resetCampaignLogDetailState(); return; } - m_State.IsCampaignDataLoading = true; + state.IsCampaignDataLoading = true; try { await RefreshCampaignRosterAsync(); - await m_RefreshSelectedCharacterSheetAsync(); - await m_RefreshCampaignLogAsync(null); - m_ResetCampaignStateTracking(); + await refreshSelectedCharacterSheetAsync(); + await refreshCampaignLogAsync(null); + resetCampaignStateTracking(); } catch (ApiRequestException ex) when (ex.StatusCode == 401) { - m_ClearAuthenticatedState(); - await m_StopStateEventsAsync(); - await m_OnLoggedOutAsync("Session expired. Please log in again."); + clearAuthenticatedState(); + await stopStateEventsAsync(); + await onLoggedOutAsync("Session expired. Please log in again."); } catch (ApiRequestException ex) { - m_Feedback.SetStatus(ex.Message, true); + feedback.SetStatus(ex.Message, true); } finally { - m_State.IsCampaignDataLoading = false; + state.IsCampaignDataLoading = false; } } public async Task SetMobilePanelAsync(string panel) { - m_State.MobilePanel = string.Equals(panel, "log", StringComparison.OrdinalIgnoreCase) ? "log" : "character"; - await m_JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", MobilePanelSessionKey, m_State.MobilePanel); + state.MobilePanel = string.Equals(panel, "log", StringComparison.OrdinalIgnoreCase) ? "log" : "character"; + await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", MobilePanelSessionKey, state.MobilePanel); } private void SyncSelectedCharacter() { - if (m_State.SelectedCampaign is null || m_State.SelectedCampaign.Characters.Length == 0) + if (state.SelectedCampaign is null || state.SelectedCampaign.Characters.Length == 0) { - m_State.SelectedCharacterId = null; + state.SelectedCharacterId = null; return; } - var candidateIds = m_State.SelectedCampaign.Characters.Select(character => character.Id).ToHashSet(); - if (m_State.SelectedCharacterId.HasValue && candidateIds.Contains(m_State.SelectedCharacterId.Value)) + var candidateIds = state.SelectedCampaign.Characters.Select(character => character.Id).ToHashSet(); + if (state.SelectedCharacterId.HasValue && candidateIds.Contains(state.SelectedCharacterId.Value)) return; - if (m_State.ActiveCharacterId.HasValue && candidateIds.Contains(m_State.ActiveCharacterId.Value)) + if (state.ActiveCharacterId.HasValue && candidateIds.Contains(state.ActiveCharacterId.Value)) { - m_State.SelectedCharacterId = m_State.ActiveCharacterId; + state.SelectedCharacterId = state.ActiveCharacterId; return; } - m_State.SelectedCharacterId = m_State.SelectedCampaign.Characters[0].Id; + state.SelectedCharacterId = state.SelectedCampaign.Characters[0].Id; } private const string CampaignSessionKey = "campaign"; private const string MobilePanelSessionKey = "play-panel"; - - private readonly Action m_ClearAuthenticatedState; - private readonly Func m_EnsureSelectedCharacterActiveAsync; - private readonly WorkspaceFeedbackService m_Feedback; - private readonly IJSRuntime m_JS; - private readonly Func m_OnLoggedOutAsync; - private readonly Func m_RefreshCampaignLogAsync; - private readonly Func m_RefreshSelectedCharacterSheetAsync; - private readonly Action m_ResetCampaignLogDetailState; - private readonly Action m_ResetCampaignStateTracking; - private readonly WorkspaceState m_State; - private readonly Func m_StopStateEventsAsync; - private readonly WorkspaceQueryService m_WorkspaceQuery; } \ No newline at end of file diff --git a/RpgRoller/Components/Pages/WorkspaceFeedbackService.cs b/RpgRoller/Components/Pages/WorkspaceFeedbackService.cs index 82f58c5..3f54214 100644 --- a/RpgRoller/Components/Pages/WorkspaceFeedbackService.cs +++ b/RpgRoller/Components/Pages/WorkspaceFeedbackService.cs @@ -1,13 +1,7 @@ namespace RpgRoller.Components.Pages; -public sealed class WorkspaceFeedbackService +public sealed class WorkspaceFeedbackService(WorkspaceState state, Func requestRefreshAsync) { - public WorkspaceFeedbackService(WorkspaceState state, Func requestRefreshAsync) - { - m_State = state; - m_RequestRefreshAsync = requestRefreshAsync; - } - public void SetStatus(string message, bool isError) { Announce(message); @@ -16,18 +10,18 @@ public sealed class WorkspaceFeedbackService public void Announce(string message) { - m_State.LiveAnnouncement = message; + state.LiveAnnouncement = message; } public void ClearToasts() { - m_State.Toasts.Clear(); + state.Toasts.Clear(); } private void AddToast(string message, bool isError) { var toastId = Guid.NewGuid(); - m_State.Toasts.Add(new(toastId, message, isError)); + state.Toasts.Add(new(toastId, message, isError)); _ = DismissToastLaterAsync(toastId); } @@ -35,12 +29,12 @@ public sealed class WorkspaceFeedbackService { await Task.Delay(ToastDurationMs); - if (m_State.Toasts.RemoveAll(toast => toast.Id == toastId) == 0) + if (state.Toasts.RemoveAll(toast => toast.Id == toastId) == 0) return; try { - await m_RequestRefreshAsync(); + await requestRefreshAsync(); } catch (ObjectDisposedException) { @@ -48,7 +42,4 @@ public sealed class WorkspaceFeedbackService } private const int ToastDurationMs = 3200; - - private readonly Func m_RequestRefreshAsync; - private readonly WorkspaceState m_State; } \ No newline at end of file diff --git a/RpgRoller/Components/Pages/WorkspaceLiveStateController.cs b/RpgRoller/Components/Pages/WorkspaceLiveStateController.cs index fbf80f4..844009b 100644 --- a/RpgRoller/Components/Pages/WorkspaceLiveStateController.cs +++ b/RpgRoller/Components/Pages/WorkspaceLiveStateController.cs @@ -4,101 +4,89 @@ using RpgRoller.Contracts; namespace RpgRoller.Components.Pages; [ExcludeFromCodeCoverage] -public sealed class WorkspaceLiveStateController +public sealed class WorkspaceLiveStateController(WorkspaceState state, WorkspaceFeedbackService feedback, Func startStateEventsAsync, Func stopStateEventsCoreAsync, Func refreshCampaignRosterAsync, Func refreshSelectedCharacterSheetAsync, Func refreshCampaignLogAsync, Func requestRefreshAsync) { - public WorkspaceLiveStateController(WorkspaceState state, WorkspaceFeedbackService feedback, Func startStateEventsAsync, Func stopStateEventsCoreAsync, Func refreshCampaignRosterAsync, Func refreshSelectedCharacterSheetAsync, Func refreshCampaignLogAsync, Func requestRefreshAsync) + public async Task OnStateEventReceivedAsync(CampaignStateSnapshot state1) { - m_State = state; - m_Feedback = feedback; - m_StartStateEventsAsync = startStateEventsAsync; - m_StopStateEventsCoreAsync = stopStateEventsCoreAsync; - m_RefreshCampaignRosterAsync = refreshCampaignRosterAsync; - m_RefreshSelectedCharacterSheetAsync = refreshSelectedCharacterSheetAsync; - m_RefreshCampaignLogAsync = refreshCampaignLogAsync; - m_RequestRefreshAsync = requestRefreshAsync; - } - - public async Task OnStateEventReceivedAsync(CampaignStateSnapshot state) - { - if (m_State.StateRefreshInProgress) + if (state.StateRefreshInProgress) return; - if (!m_State.SelectedCampaignId.HasValue || state.CampaignId != m_State.SelectedCampaignId.Value) + if (!state.SelectedCampaignId.HasValue || state1.CampaignId != state.SelectedCampaignId.Value) return; - m_State.StateRefreshInProgress = true; + state.StateRefreshInProgress = true; try { - if (m_State.CurrentCampaignState is null) + if (state.CurrentCampaignState is null) { - m_State.CurrentCampaignState = state; + state.CurrentCampaignState = state1; return; } - var previousState = m_State.CurrentCampaignState; - var previousSelectedCharacterId = m_State.SelectedCharacterId; + var previousState = state.CurrentCampaignState; + var previousSelectedCharacterId = state.SelectedCharacterId; var previousSelectedCharacterVersion = GetCharacterVersion(previousState, previousSelectedCharacterId); - var rosterChanged = state.RosterVersion != previousState.RosterVersion; - var logChanged = m_State.IsPlayScreen && state.LogVersion != previousState.LogVersion; + var rosterChanged = state1.RosterVersion != previousState.RosterVersion; + var logChanged = state.IsPlayScreen && state1.LogVersion != previousState.LogVersion; if (rosterChanged) - await m_RefreshCampaignRosterAsync(); + await refreshCampaignRosterAsync(); - var selectedCharacterChanged = previousSelectedCharacterId != m_State.SelectedCharacterId; - var selectedCharacterVersionChanged = m_State.IsPlayScreen && !selectedCharacterChanged && GetCharacterVersion(state, m_State.SelectedCharacterId) != previousSelectedCharacterVersion; + var selectedCharacterChanged = previousSelectedCharacterId != state.SelectedCharacterId; + var selectedCharacterVersionChanged = state.IsPlayScreen && !selectedCharacterChanged && GetCharacterVersion(state1, state.SelectedCharacterId) != previousSelectedCharacterVersion; - if (m_State.IsPlayScreen && (selectedCharacterChanged || selectedCharacterVersionChanged)) - await m_RefreshSelectedCharacterSheetAsync(); + if (state.IsPlayScreen && (selectedCharacterChanged || selectedCharacterVersionChanged)) + await refreshSelectedCharacterSheetAsync(); if (logChanged) - await m_RefreshCampaignLogAsync(m_State.CampaignLogCursor); + await refreshCampaignLogAsync(state.CampaignLogCursor); - m_State.CurrentCampaignState = state; + state.CurrentCampaignState = state1; } finally { - m_State.StateRefreshInProgress = false; - await m_RequestRefreshAsync(); + state.StateRefreshInProgress = false; + await requestRefreshAsync(); } } - public async Task OnConnectionStateChangedAsync(string state) + public async Task OnConnectionStateChangedAsync(string state1) { - m_State.ConnectionState = state switch + state.ConnectionState = state1 switch { "connected" => "connected", "reconnecting" => "reconnecting", _ => "offline" }; - if (m_State.ConnectionState == "reconnecting") - m_Feedback.Announce("Reconnecting to live updates."); + if (state.ConnectionState == "reconnecting") + feedback.Announce("Reconnecting to live updates."); - if (m_State.ConnectionState == "offline") - m_Feedback.Announce("Live updates offline. Use manual refresh."); + if (state.ConnectionState == "offline") + feedback.Announce("Live updates offline. Use manual refresh."); - await m_RequestRefreshAsync(); + await requestRefreshAsync(); } public async Task SyncStateEventsAsync() { - if (m_State.User is null || !m_State.SelectedCampaignId.HasValue || m_State.IsAdminScreen) + if (state.User is null || !state.SelectedCampaignId.HasValue || state.IsAdminScreen) { await StopStateEventsAsync(); - m_State.ConnectionState = "offline"; + state.ConnectionState = "offline"; return; } - await m_StartStateEventsAsync(m_State.SelectedCampaignId.Value); - m_State.ConnectionState = "reconnecting"; + await startStateEventsAsync(state.SelectedCampaignId.Value); + state.ConnectionState = "reconnecting"; } public async Task StopStateEventsAsync() { - if (!m_State.HasInteractiveRenderStarted) + if (!state.HasInteractiveRenderStarted) return; - await m_StopStateEventsCoreAsync(); + await stopStateEventsCoreAsync(); } private static long GetCharacterVersion(CampaignStateSnapshot snapshot, Guid? characterId) @@ -108,13 +96,4 @@ public sealed class WorkspaceLiveStateController return snapshot.CharacterVersions.FirstOrDefault(version => version.CharacterId == characterId.Value)?.Version ?? 0; } - - private readonly WorkspaceFeedbackService m_Feedback; - private readonly Func m_RefreshCampaignLogAsync; - private readonly Func m_RefreshCampaignRosterAsync; - private readonly Func m_RefreshSelectedCharacterSheetAsync; - private readonly Func m_RequestRefreshAsync; - private readonly Func m_StartStateEventsAsync; - private readonly WorkspaceState m_State; - private readonly Func m_StopStateEventsCoreAsync; } \ No newline at end of file diff --git a/RpgRoller/Components/Pages/WorkspacePlayCoordinator.cs b/RpgRoller/Components/Pages/WorkspacePlayCoordinator.cs index 81c3769..66741b7 100644 --- a/RpgRoller/Components/Pages/WorkspacePlayCoordinator.cs +++ b/RpgRoller/Components/Pages/WorkspacePlayCoordinator.cs @@ -4,54 +4,44 @@ using RpgRoller.Contracts; namespace RpgRoller.Components.Pages; [ExcludeFromCodeCoverage] -public sealed class WorkspacePlayCoordinator +public sealed class WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, RpgRollerApiClient apiClient, WorkspaceQueryService workspaceQuery, Func canEditCharacter, Func requestRefreshAsync) { - public WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, RpgRollerApiClient apiClient, WorkspaceQueryService workspaceQuery, Func canEditCharacter, Func requestRefreshAsync) - { - m_State = state; - m_Feedback = feedback; - m_ApiClient = apiClient; - m_WorkspaceQuery = workspaceQuery; - m_CanEditCharacter = canEditCharacter; - m_RequestRefreshAsync = requestRefreshAsync; - } - public async Task RefreshCampaignLogAsync(Guid? afterRollId = null) { - if (!m_State.SelectedCampaignId.HasValue || !m_State.IsPlayScreen) + if (!state.SelectedCampaignId.HasValue || !state.IsPlayScreen) { - m_State.CampaignLog = []; - m_State.CampaignLogCursor = null; + state.CampaignLog = []; + state.CampaignLogCursor = null; ResetCampaignLogDetailState(); return; } - var previousLogCount = m_State.CampaignLog.Count; - var page = await m_WorkspaceQuery.GetCampaignLogPageAsync(m_State.SelectedCampaignId.Value, afterRollId, CampaignLogWindowSize); + var previousLogCount = state.CampaignLog.Count; + var page = await workspaceQuery.GetCampaignLogPageAsync(state.SelectedCampaignId.Value, afterRollId, CampaignLogWindowSize); Guid? newestRollId = null; if (!afterRollId.HasValue || page.ResetRequired) - m_State.CampaignLog = page.Entries.ToList(); + state.CampaignLog = page.Entries.ToList(); else if (page.Entries.Length > 0) { - m_State.CampaignLog.AddRange(page.Entries); - if (m_State.CampaignLog.Count > CampaignLogWindowSize) - m_State.CampaignLog = m_State.CampaignLog.TakeLast(CampaignLogWindowSize).ToList(); + state.CampaignLog.AddRange(page.Entries); + if (state.CampaignLog.Count > CampaignLogWindowSize) + state.CampaignLog = state.CampaignLog.TakeLast(CampaignLogWindowSize).ToList(); } var shouldAutoExpandNewest = afterRollId.HasValue && page.Entries.Length > 0; - if (!shouldAutoExpandNewest && !afterRollId.HasValue && m_State.CurrentCampaignState is not null && previousLogCount == 0 && page.Entries.Length > 0) + if (!shouldAutoExpandNewest && !afterRollId.HasValue && state.CurrentCampaignState is not null && previousLogCount == 0 && page.Entries.Length > 0) shouldAutoExpandNewest = true; if (shouldAutoExpandNewest) { newestRollId = page.Entries[^1].RollId; - m_State.ExpandedCampaignLogRollId = newestRollId; - m_State.FreshCampaignLogRollId = newestRollId; + state.ExpandedCampaignLogRollId = newestRollId; + state.FreshCampaignLogRollId = newestRollId; } else if (!afterRollId.HasValue) - m_State.FreshCampaignLogRollId = null; + state.FreshCampaignLogRollId = null; - m_State.CampaignLogCursor = page.Cursor ?? afterRollId; + state.CampaignLogCursor = page.Cursor ?? afterRollId; TrimCampaignLogDetails(); if (newestRollId.HasValue) @@ -60,23 +50,23 @@ public sealed class WorkspacePlayCoordinator public async Task SelectCharacterAsync(Guid characterId) { - m_State.SelectedCharacterId = characterId; + state.SelectedCharacterId = characterId; await RefreshSelectedCharacterSheetAsync(); await EnsureSelectedCharacterActiveAsync(); } public async Task RefreshSelectedCharacterSheetAsync() { - if (!m_State.SelectedCharacterId.HasValue || m_State.SelectedCampaign is null || !m_State.IsPlayScreen) + if (!state.SelectedCharacterId.HasValue || state.SelectedCampaign is null || !state.IsPlayScreen) { - m_State.SelectedCharacterSkills = []; - m_State.SelectedCharacterSkillGroups = []; + state.SelectedCharacterSkills = []; + state.SelectedCharacterSkillGroups = []; return; } - var sheet = await m_WorkspaceQuery.GetCharacterSheetAsync(m_State.SelectedCharacterId.Value); - m_State.SelectedCharacterSkillGroups = sheet.SkillGroups.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase).ToList(); - m_State.SelectedCharacterSkills = sheet.Skills.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase).ToList(); + var sheet = await workspaceQuery.GetCharacterSheetAsync(state.SelectedCharacterId.Value); + state.SelectedCharacterSkillGroups = sheet.SkillGroups.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase).ToList(); + state.SelectedCharacterSkills = sheet.Skills.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase).ToList(); } public Task EnsureSelectedCharacterActiveAsync() @@ -86,16 +76,16 @@ public sealed class WorkspacePlayCoordinator public async Task ToggleRollDetailAsync(Guid rollId) { - if (m_State.ExpandedCampaignLogRollId == rollId) + if (state.ExpandedCampaignLogRollId == rollId) { - m_State.ExpandedCampaignLogRollId = null; - if (m_State.FreshCampaignLogRollId == rollId) - m_State.FreshCampaignLogRollId = null; + state.ExpandedCampaignLogRollId = null; + if (state.FreshCampaignLogRollId == rollId) + state.FreshCampaignLogRollId = null; return; } - m_State.ExpandedCampaignLogRollId = rollId; - m_State.FreshCampaignLogRollId = null; + state.ExpandedCampaignLogRollId = rollId; + state.FreshCampaignLogRollId = null; await EnsureRollDetailLoadedAsync(rollId); } @@ -103,212 +93,212 @@ public sealed class WorkspacePlayCoordinator { await RefreshSelectedCharacterSheetAsync(); ResetCampaignStateTracking(); - m_Feedback.SetStatus("Skill created.", false); + feedback.SetStatus("Skill created.", false); } public async Task OnSkillUpdatedAsync(Guid _) { await RefreshSelectedCharacterSheetAsync(); ResetCampaignStateTracking(); - m_Feedback.SetStatus("Skill updated.", false); + feedback.SetStatus("Skill updated.", false); } public async Task OnSkillGroupCreatedAsync(Guid _) { await RefreshSelectedCharacterSheetAsync(); ResetCampaignStateTracking(); - m_Feedback.SetStatus("Skill group created.", false); + feedback.SetStatus("Skill group created.", false); } public async Task OnSkillGroupUpdatedAsync(Guid _) { await RefreshSelectedCharacterSheetAsync(); ResetCampaignStateTracking(); - m_Feedback.SetStatus("Skill group updated.", false); + feedback.SetStatus("Skill group updated.", false); } public async Task OnSkillDeletedAsync(Guid _) { await RefreshSelectedCharacterSheetAsync(); ResetCampaignStateTracking(); - m_Feedback.SetStatus("Skill deleted.", false); + feedback.SetStatus("Skill deleted.", false); } public async Task OnSkillGroupDeletedAsync(Guid _) { await RefreshSelectedCharacterSheetAsync(); ResetCampaignStateTracking(); - m_Feedback.SetStatus("Skill group deleted.", false); + feedback.SetStatus("Skill group deleted.", false); } public Task OnCharacterPanelErrorAsync(string message) { - m_Feedback.SetStatus(message, true); + feedback.SetStatus(message, true); return Task.CompletedTask; } public Task OnCampaignLogPanelErrorAsync(string message) { - m_Feedback.SetStatus(message, true); + feedback.SetStatus(message, true); return Task.CompletedTask; } public async Task RollSkillAsync(Guid skillId) { - if (m_State.SelectedCampaign is null) + if (state.SelectedCampaign is null) { - m_Feedback.SetStatus("No campaign selected.", true); + feedback.SetStatus("No campaign selected.", true); return; } - m_State.IsMutating = true; + state.IsMutating = true; try { - var roll = await m_ApiClient.RequestAsync("POST", $"/api/skills/{skillId}/roll", new RollSkillRequest(m_State.RollVisibility)); + var roll = await apiClient.RequestAsync("POST", $"/api/skills/{skillId}/roll", new RollSkillRequest(state.RollVisibility)); await HandleRecordedRollAsync(roll); } catch (ApiRequestException ex) { - m_Feedback.SetStatus(ex.Message, true); + feedback.SetStatus(ex.Message, true); } finally { - m_State.IsMutating = false; + state.IsMutating = false; } } public async Task OnCustomRollCreatedAsync(RollResult roll) { - m_State.IsMutating = true; + state.IsMutating = true; try { await HandleRecordedRollAsync(roll); } finally { - m_State.IsMutating = false; + state.IsMutating = false; } } public bool CanEditSkill(CharacterSheetSkill skill) { - if (m_State.SelectedCharacter is null) + if (state.SelectedCharacter is null) return false; - return m_CanEditCharacter(m_State.SelectedCharacter); + return canEditCharacter(state.SelectedCharacter); } public CampaignRollDetail? ResolveRollDetail(Guid rollId) { - return m_State.CampaignLogDetails.GetValueOrDefault(rollId); + return state.CampaignLogDetails.GetValueOrDefault(rollId); } public bool IsRollDetailLoading(Guid rollId) { - return m_State.CampaignLogDetailsLoading.Contains(rollId); + return state.CampaignLogDetailsLoading.Contains(rollId); } public string? GetRollDetailError(Guid rollId) { - return m_State.CampaignLogDetailErrors.GetValueOrDefault(rollId); + return state.CampaignLogDetailErrors.GetValueOrDefault(rollId); } public void ResetCampaignLogDetailState() { - m_State.ExpandedCampaignLogRollId = null; - m_State.FreshCampaignLogRollId = null; - m_State.CampaignLogDetails.Clear(); - m_State.CampaignLogDetailsLoading.Clear(); - m_State.CampaignLogDetailErrors.Clear(); + state.ExpandedCampaignLogRollId = null; + state.FreshCampaignLogRollId = null; + state.CampaignLogDetails.Clear(); + state.CampaignLogDetailsLoading.Clear(); + state.CampaignLogDetailErrors.Clear(); } public void ResetCampaignStateTracking() { - m_State.CurrentCampaignState = null; + state.CurrentCampaignState = null; } private async Task EnsureSelectedCharacterActiveCoreAsync() { - if (!m_State.SelectedCharacterId.HasValue || m_State.SelectedCampaign is null) + if (!state.SelectedCharacterId.HasValue || state.SelectedCampaign is null) return; - var character = m_State.SelectedCampaign.Characters.FirstOrDefault(c => c.Id == m_State.SelectedCharacterId.Value); - if (character is null || !CanActivateCharacter(character, m_State.User) || m_State.ActiveCharacterId == character.Id) + var character = state.SelectedCampaign.Characters.FirstOrDefault(c => c.Id == state.SelectedCharacterId.Value); + if (character is null || !CanActivateCharacter(character, state.User) || state.ActiveCharacterId == character.Id) return; try { - await m_ApiClient.RequestWithoutPayloadAsync("POST", $"/api/characters/{character.Id}/activate"); - m_State.ActiveCharacterId = character.Id; + await apiClient.RequestWithoutPayloadAsync("POST", $"/api/characters/{character.Id}/activate"); + state.ActiveCharacterId = character.Id; } catch (ApiRequestException ex) { - m_Feedback.SetStatus(ex.Message, true); + feedback.SetStatus(ex.Message, true); } } private async Task HandleRecordedRollAsync(RollResult roll) { - m_State.LastRoll = roll; - m_State.CampaignLogDetails[roll.RollId] = ToCampaignRollDetail(roll); - m_State.CampaignLogDetailErrors.Remove(roll.RollId); + state.LastRoll = roll; + state.CampaignLogDetails[roll.RollId] = ToCampaignRollDetail(roll); + state.CampaignLogDetailErrors.Remove(roll.RollId); - await RefreshCampaignLogAsync(m_State.CampaignLogCursor); + await RefreshCampaignLogAsync(state.CampaignLogCursor); PromoteFreshRoll(roll.RollId); ResetCampaignStateTracking(); - m_Feedback.SetStatus("Roll recorded.", false); - m_Feedback.Announce("Roll result updated."); + feedback.SetStatus("Roll recorded.", false); + feedback.Announce("Roll result updated."); } private void TrimCampaignLogDetails() { - var visibleRollIds = m_State.CampaignLog.Select(entry => entry.RollId).ToHashSet(); + var visibleRollIds = state.CampaignLog.Select(entry => entry.RollId).ToHashSet(); - foreach (var rollId in m_State.CampaignLogDetails.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray()) - m_State.CampaignLogDetails.Remove(rollId); + foreach (var rollId in state.CampaignLogDetails.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray()) + state.CampaignLogDetails.Remove(rollId); - foreach (var rollId in m_State.CampaignLogDetailsLoading.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray()) - m_State.CampaignLogDetailsLoading.Remove(rollId); + foreach (var rollId in state.CampaignLogDetailsLoading.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray()) + state.CampaignLogDetailsLoading.Remove(rollId); - foreach (var rollId in m_State.CampaignLogDetailErrors.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray()) - m_State.CampaignLogDetailErrors.Remove(rollId); + foreach (var rollId in state.CampaignLogDetailErrors.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray()) + state.CampaignLogDetailErrors.Remove(rollId); - if (m_State.ExpandedCampaignLogRollId.HasValue && !visibleRollIds.Contains(m_State.ExpandedCampaignLogRollId.Value)) - m_State.ExpandedCampaignLogRollId = null; + if (state.ExpandedCampaignLogRollId.HasValue && !visibleRollIds.Contains(state.ExpandedCampaignLogRollId.Value)) + state.ExpandedCampaignLogRollId = null; - if (m_State.FreshCampaignLogRollId.HasValue && !visibleRollIds.Contains(m_State.FreshCampaignLogRollId.Value)) - m_State.FreshCampaignLogRollId = null; + if (state.FreshCampaignLogRollId.HasValue && !visibleRollIds.Contains(state.FreshCampaignLogRollId.Value)) + state.FreshCampaignLogRollId = null; } private async Task EnsureRollDetailLoadedAsync(Guid rollId) { - m_State.CampaignLogDetailErrors.Remove(rollId); - if (m_State.CampaignLogDetails.ContainsKey(rollId) || m_State.CampaignLogDetailsLoading.Contains(rollId)) + state.CampaignLogDetailErrors.Remove(rollId); + if (state.CampaignLogDetails.ContainsKey(rollId) || state.CampaignLogDetailsLoading.Contains(rollId)) return; - m_State.CampaignLogDetailsLoading.Add(rollId); + state.CampaignLogDetailsLoading.Add(rollId); try { - m_State.CampaignLogDetails[rollId] = await m_WorkspaceQuery.GetRollDetailAsync(rollId); + state.CampaignLogDetails[rollId] = await workspaceQuery.GetRollDetailAsync(rollId); } catch (ApiRequestException ex) { - m_State.CampaignLogDetailErrors[rollId] = ex.Message; + state.CampaignLogDetailErrors[rollId] = ex.Message; } finally { - m_State.CampaignLogDetailsLoading.Remove(rollId); - await m_RequestRefreshAsync(); + state.CampaignLogDetailsLoading.Remove(rollId); + await requestRefreshAsync(); } } private void PromoteFreshRoll(Guid rollId) { - if (!m_State.CampaignLog.Any(entry => entry.RollId == rollId)) + if (!state.CampaignLog.Any(entry => entry.RollId == rollId)) return; - m_State.ExpandedCampaignLogRollId = rollId; - m_State.FreshCampaignLogRollId = rollId; + state.ExpandedCampaignLogRollId = rollId; + state.FreshCampaignLogRollId = rollId; } private static bool CanActivateCharacter(CharacterSummary character, UserSummary? user) @@ -322,11 +312,4 @@ public sealed class WorkspacePlayCoordinator } private const int CampaignLogWindowSize = 25; - - private readonly RpgRollerApiClient m_ApiClient; - private readonly Func m_CanEditCharacter; - private readonly WorkspaceFeedbackService m_Feedback; - private readonly Func m_RequestRefreshAsync; - private readonly WorkspaceState m_State; - private readonly WorkspaceQueryService m_WorkspaceQuery; } \ No newline at end of file diff --git a/RpgRoller/Components/Pages/WorkspaceSessionCoordinator.cs b/RpgRoller/Components/Pages/WorkspaceSessionCoordinator.cs index 22b93a7..9a1a71a 100644 --- a/RpgRoller/Components/Pages/WorkspaceSessionCoordinator.cs +++ b/RpgRoller/Components/Pages/WorkspaceSessionCoordinator.cs @@ -3,40 +3,22 @@ using RpgRoller.Contracts; namespace RpgRoller.Components.Pages; -public sealed class WorkspaceSessionCoordinator +public sealed class WorkspaceSessionCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, RpgRollerApiClient apiClient, WorkspaceQueryService workspaceQuery, Func reloadCampaignsAsync, Func reloadCharacterCampaignOptionsAsync, Func refreshCampaignScopeAsync, Func syncStateEventsAsync, Func stopStateEventsAsync, Func ensureAdminUsersLoadedAsync, Action resetCampaignLogDetailState, Func requestRefreshAsync, Func onLoggedOutAsync) { - public WorkspaceSessionCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, RpgRollerApiClient apiClient, WorkspaceQueryService workspaceQuery, Func reloadCampaignsAsync, Func reloadCharacterCampaignOptionsAsync, Func refreshCampaignScopeAsync, Func syncStateEventsAsync, Func stopStateEventsAsync, Func ensureAdminUsersLoadedAsync, Action resetCampaignLogDetailState, Func requestRefreshAsync, Func onLoggedOutAsync) - { - m_State = state; - m_Feedback = feedback; - m_JS = js; - m_ApiClient = apiClient; - m_WorkspaceQuery = workspaceQuery; - m_ReloadCampaignsAsync = reloadCampaignsAsync; - m_ReloadCharacterCampaignOptionsAsync = reloadCharacterCampaignOptionsAsync; - m_RefreshCampaignScopeAsync = refreshCampaignScopeAsync; - m_SyncStateEventsAsync = syncStateEventsAsync; - m_StopStateEventsAsync = stopStateEventsAsync; - m_EnsureAdminUsersLoadedAsync = ensureAdminUsersLoadedAsync; - m_ResetCampaignLogDetailState = resetCampaignLogDetailState; - m_RequestRefreshAsync = requestRefreshAsync; - m_OnLoggedOutAsync = onLoggedOutAsync; - } - public async Task InitializeAsync() { - var storedScreen = await m_JS.InvokeAsync("rpgRollerApi.getSessionValue", ScreenSessionKey); - m_State.CurrentScreen = NormalizeRequestedScreen(storedScreen) ?? ScreenPlay; + var storedScreen = await js.InvokeAsync("rpgRollerApi.getSessionValue", ScreenSessionKey); + state.CurrentScreen = NormalizeRequestedScreen(storedScreen) ?? ScreenPlay; - var storedPanel = await m_JS.InvokeAsync("rpgRollerApi.getSessionValue", MobilePanelSessionKey); + var storedPanel = await js.InvokeAsync("rpgRollerApi.getSessionValue", MobilePanelSessionKey); if (string.Equals(storedPanel, "log", StringComparison.OrdinalIgnoreCase)) - m_State.MobilePanel = "log"; + state.MobilePanel = "log"; - var storedRollVisibility = await m_JS.InvokeAsync("rpgRollerApi.getSessionValue", RollVisibilitySessionKey); - m_State.RollVisibility = NormalizeRollVisibility(storedRollVisibility); + var storedRollVisibility = await js.InvokeAsync("rpgRollerApi.getSessionValue", RollVisibilitySessionKey); + state.RollVisibility = NormalizeRollVisibility(storedRollVisibility); Guid? preferredCampaignId = null; - var storedCampaignId = await m_JS.InvokeAsync("rpgRollerApi.getSessionValue", CampaignSessionKey); + var storedCampaignId = await js.InvokeAsync("rpgRollerApi.getSessionValue", CampaignSessionKey); if (Guid.TryParse(storedCampaignId, out var parsedCampaignId)) preferredCampaignId = parsedCampaignId; @@ -45,17 +27,17 @@ public sealed class WorkspaceSessionCoordinator var reloaded = await ReloadAuthenticatedSessionAsync(preferredCampaignId); if (!reloaded) - await m_OnLoggedOutAsync("Session expired. Please log in again."); + await onLoggedOutAsync("Session expired. Please log in again."); } public async Task RetryAfterHealthIssueAsync() { await CheckHealthAsync(); - if (!m_State.HasHealthIssue && m_State.User is not null) + if (!state.HasHealthIssue && state.User is not null) { - var reloaded = await ReloadAuthenticatedSessionAsync(m_State.SelectedCampaignId); + var reloaded = await ReloadAuthenticatedSessionAsync(state.SelectedCampaignId); if (!reloaded) - await m_OnLoggedOutAsync("Session expired. Please log in again."); + await onLoggedOutAsync("Session expired. Please log in again."); } } @@ -63,102 +45,102 @@ public sealed class WorkspaceSessionCoordinator { try { - var usernames = await m_WorkspaceQuery.GetUsernamesAsync(); - m_State.KnownUsernames = usernames.OrderBy(username => username, StringComparer.OrdinalIgnoreCase).ToList(); + var usernames = await workspaceQuery.GetUsernamesAsync(); + state.KnownUsernames = usernames.OrderBy(username => username, StringComparer.OrdinalIgnoreCase).ToList(); } catch (ApiRequestException ex) { - m_State.KnownUsernames = []; - m_Feedback.SetStatus(ex.Message, true); + state.KnownUsernames = []; + feedback.SetStatus(ex.Message, true); } } public async Task LogoutAsync() { - if (m_State.IsMutating) + if (state.IsMutating) return; - m_State.IsMutating = true; + state.IsMutating = true; try { - await m_ApiClient.RequestWithoutPayloadAsync("POST", "/api/auth/logout"); + await apiClient.RequestWithoutPayloadAsync("POST", "/api/auth/logout"); } catch (ApiRequestException) { } finally { - m_State.IsMutating = false; + state.IsMutating = false; } ClearAuthenticatedState(); - await m_StopStateEventsAsync(); - await m_OnLoggedOutAsync("Logged out."); + await stopStateEventsAsync(); + await onLoggedOutAsync("Logged out."); } public async Task SwitchScreenAsync(string screen) { var targetScreen = NormalizeRequestedScreen(screen) ?? ScreenPlay; - if (string.Equals(targetScreen, ScreenAdmin, StringComparison.OrdinalIgnoreCase) && !m_State.IsCurrentUserAdmin) + if (string.Equals(targetScreen, ScreenAdmin, StringComparison.OrdinalIgnoreCase) && !state.IsCurrentUserAdmin) targetScreen = ScreenPlay; - m_State.CurrentScreen = targetScreen; - m_State.IsScreenMenuOpen = false; - await PersistScreenPreferenceAsync(m_State.CurrentScreen); - await m_RequestRefreshAsync(); + state.CurrentScreen = targetScreen; + state.IsScreenMenuOpen = false; + await PersistScreenPreferenceAsync(state.CurrentScreen); + await requestRefreshAsync(); - if (m_State.User is not null) + if (state.User is not null) { - await m_RefreshCampaignScopeAsync(); - await m_SyncStateEventsAsync(); + await refreshCampaignScopeAsync(); + await syncStateEventsAsync(); } - if (m_State.IsAdminScreen) + if (state.IsAdminScreen) { - await m_EnsureAdminUsersLoadedAsync(); - await m_RequestRefreshAsync(); + await ensureAdminUsersLoadedAsync(); + await requestRefreshAsync(); } } public async Task OnRollVisibilityChangedAsync(string visibility) { - m_State.RollVisibility = NormalizeRollVisibility(visibility); - await m_JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", RollVisibilitySessionKey, m_State.RollVisibility); + state.RollVisibility = NormalizeRollVisibility(visibility); + await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", RollVisibilitySessionKey, state.RollVisibility); } public void ClearAuthenticatedState() { - m_State.User = null; - m_State.ActiveCharacterId = null; - m_State.SelectedCampaignId = null; - m_State.SelectedCampaign = null; - m_State.Campaigns = []; - m_State.CharacterCampaignOptions = []; - m_State.SelectedCharacterSkills = []; - m_State.SelectedCharacterSkillGroups = []; - m_State.CampaignLog = []; - m_State.CampaignLogCursor = null; - m_ResetCampaignLogDetailState(); - m_State.SelectedCharacterId = null; - m_State.LastRoll = null; - m_State.KnownUsernames = []; - m_State.ShowCreateCharacterModal = false; - m_State.ShowEditCharacterModal = false; - m_State.CanEditCharacterOwner = false; - m_State.CreateCharacterInitialModel = new(); - m_State.EditCharacterInitialModel = new(); - m_State.CreateCharacterFormVersion = 0; - m_State.EditCharacterFormVersion = 0; - m_State.AdminUsers = []; - m_State.HasLoadedAdminUsers = false; - m_State.IsAdminDataLoading = false; - m_Feedback.ClearToasts(); + state.User = null; + state.ActiveCharacterId = null; + state.SelectedCampaignId = null; + state.SelectedCampaign = null; + state.Campaigns = []; + state.CharacterCampaignOptions = []; + state.SelectedCharacterSkills = []; + state.SelectedCharacterSkillGroups = []; + state.CampaignLog = []; + state.CampaignLogCursor = null; + resetCampaignLogDetailState(); + state.SelectedCharacterId = null; + state.LastRoll = null; + state.KnownUsernames = []; + state.ShowCreateCharacterModal = false; + state.ShowEditCharacterModal = false; + state.CanEditCharacterOwner = false; + state.CreateCharacterInitialModel = new(); + state.EditCharacterInitialModel = new(); + state.CreateCharacterFormVersion = 0; + state.EditCharacterFormVersion = 0; + state.AdminUsers = []; + state.HasLoadedAdminUsers = false; + state.IsAdminDataLoading = false; + feedback.ClearToasts(); } private async Task CheckHealthAsync() { - m_State.HasHealthIssue = false; - m_State.HealthIssueMessage = string.Empty; + state.HasHealthIssue = false; + state.HealthIssueMessage = string.Empty; await Task.CompletedTask; } @@ -166,11 +148,11 @@ public sealed class WorkspaceSessionCoordinator { try { - m_State.Rulesets = (await m_WorkspaceQuery.GetRulesetsAsync()).ToList(); + state.Rulesets = (await workspaceQuery.GetRulesetsAsync()).ToList(); } catch (ApiRequestException ex) { - m_Feedback.SetStatus(ex.Message, true); + feedback.SetStatus(ex.Message, true); } } @@ -180,21 +162,21 @@ public sealed class WorkspaceSessionCoordinator if (me is null) { ClearAuthenticatedState(); - await m_StopStateEventsAsync(); + await stopStateEventsAsync(); return false; } - m_State.User = me.User; - m_State.ActiveCharacterId = me.ActiveCharacterId; + state.User = me.User; + state.ActiveCharacterId = me.ActiveCharacterId; await EnsureScreenAccessAsync(); - await m_ReloadCampaignsAsync(preferredCampaignId ?? me.CurrentCampaignId); - await m_ReloadCharacterCampaignOptionsAsync(); - await m_RefreshCampaignScopeAsync(); - await m_SyncStateEventsAsync(); + await reloadCampaignsAsync(preferredCampaignId ?? me.CurrentCampaignId); + await reloadCharacterCampaignOptionsAsync(); + await refreshCampaignScopeAsync(); + await syncStateEventsAsync(); - if (m_State.IsAdminScreen) - await m_EnsureAdminUsersLoadedAsync(); + if (state.IsAdminScreen) + await ensureAdminUsersLoadedAsync(); return true; } @@ -203,7 +185,7 @@ public sealed class WorkspaceSessionCoordinator { try { - return await m_WorkspaceQuery.GetMeAsync(); + return await workspaceQuery.GetMeAsync(); } catch (ApiRequestException ex) when (ex.StatusCode == 401) { @@ -213,24 +195,24 @@ public sealed class WorkspaceSessionCoordinator private async Task EnsureScreenAccessAsync() { - if (m_State.IsCurrentUserAdmin) + if (state.IsCurrentUserAdmin) return; - m_State.AdminUsers = []; - m_State.HasLoadedAdminUsers = false; + state.AdminUsers = []; + state.HasLoadedAdminUsers = false; - if (!m_State.IsAdminScreen) + if (!state.IsAdminScreen) return; - m_State.CurrentScreen = ScreenPlay; - await PersistScreenPreferenceAsync(m_State.CurrentScreen); + state.CurrentScreen = ScreenPlay; + await PersistScreenPreferenceAsync(state.CurrentScreen); } private async Task PersistScreenPreferenceAsync(string screen) { try { - await m_JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", ScreenSessionKey, screen); + await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", ScreenSessionKey, screen); } catch (JSDisconnectedException) { @@ -271,19 +253,4 @@ public sealed class WorkspaceSessionCoordinator private const string CampaignSessionKey = "campaign"; private const string MobilePanelSessionKey = "play-panel"; private const string RollVisibilitySessionKey = "roll-visibility"; - - private readonly RpgRollerApiClient m_ApiClient; - private readonly Func m_EnsureAdminUsersLoadedAsync; - private readonly WorkspaceFeedbackService m_Feedback; - private readonly IJSRuntime m_JS; - private readonly Func m_OnLoggedOutAsync; - private readonly Func m_RefreshCampaignScopeAsync; - private readonly Func m_ReloadCampaignsAsync; - private readonly Func m_ReloadCharacterCampaignOptionsAsync; - private readonly Func m_RequestRefreshAsync; - private readonly Action m_ResetCampaignLogDetailState; - private readonly WorkspaceState m_State; - private readonly Func m_StopStateEventsAsync; - private readonly Func m_SyncStateEventsAsync; - private readonly WorkspaceQueryService m_WorkspaceQuery; } \ No newline at end of file diff --git a/RpgRoller/Components/RpgRollerApiClient.cs b/RpgRoller/Components/RpgRollerApiClient.cs index 5b75e24..1f600dd 100644 --- a/RpgRoller/Components/RpgRollerApiClient.cs +++ b/RpgRoller/Components/RpgRollerApiClient.cs @@ -4,7 +4,7 @@ using RpgRoller.Contracts; namespace RpgRoller.Components; -public sealed class RpgRollerApiClient +public sealed class RpgRollerApiClient(IJSRuntime js) { private sealed class JsApiResponse { @@ -15,14 +15,9 @@ public sealed class RpgRollerApiClient public JsonElement Data { get; set; } } - public RpgRollerApiClient(IJSRuntime js) - { - m_Js = js; - } - public async Task RequestAsync(string method, string path, object? payload = null) { - var response = await m_Js.InvokeAsync("rpgRollerApi.request", method, path, payload); + var response = await js.InvokeAsync("rpgRollerApi.request", method, path, payload); if (!response.Ok) throw new ApiRequestException(response.Status, response.Error ?? "Request failed.", response.Code); @@ -34,23 +29,16 @@ public sealed class RpgRollerApiClient public async Task RequestWithoutPayloadAsync(string method, string path) { - var response = await m_Js.InvokeAsync("rpgRollerApi.request", method, path, null); + var response = await js.InvokeAsync("rpgRollerApi.request", method, path, null); if (!response.Ok) throw new ApiRequestException(response.Status, response.Error ?? "Request failed.", response.Code); } private static readonly JsonSerializerOptions JsonOptions = RpgRollerJson.CreateSerializerOptions(); - private readonly IJSRuntime m_Js; } -public sealed class ApiRequestException : Exception +public sealed class ApiRequestException(int statusCode, string message, string? errorCode = null) : Exception(message) { - public ApiRequestException(int statusCode, string message, string? errorCode = null) : base(message) - { - StatusCode = statusCode; - ErrorCode = errorCode; - } - - public int StatusCode { get; } - public string? ErrorCode { get; } + public int StatusCode { get; } = statusCode; + public string? ErrorCode { get; } = errorCode; } \ No newline at end of file diff --git a/RpgRoller/Components/WorkspaceQueryService.cs b/RpgRoller/Components/WorkspaceQueryService.cs index 917b1e7..f588293 100644 --- a/RpgRoller/Components/WorkspaceQueryService.cs +++ b/RpgRoller/Components/WorkspaceQueryService.cs @@ -3,72 +3,66 @@ using RpgRoller.Services; namespace RpgRoller.Components; -public sealed class WorkspaceQueryService +public sealed class WorkspaceQueryService(IGameService gameService, WorkspaceSessionTokenAccessor sessionTokenAccessor) { - public WorkspaceQueryService(IGameService gameService, WorkspaceSessionTokenAccessor sessionTokenAccessor) - { - m_GameService = gameService; - m_SessionTokenAccessor = sessionTokenAccessor; - } - public Task GetMeAsync() { - return Task.FromResult(GetValue(m_GameService.GetMe(GetRequiredSessionToken()))); + return Task.FromResult(GetValue(gameService.GetMe(GetRequiredSessionToken()))); } public Task> GetRulesetsAsync() { - return Task.FromResult(m_GameService.GetRulesets()); + return Task.FromResult(gameService.GetRulesets()); } public Task> GetCampaignsAsync() { - return Task.FromResult(GetValue(m_GameService.GetCampaigns(GetRequiredSessionToken()))); + return Task.FromResult(GetValue(gameService.GetCampaigns(GetRequiredSessionToken()))); } public Task> GetCharacterCampaignOptionsAsync() { - return Task.FromResult(GetValue(m_GameService.GetCharacterCampaignOptions(GetRequiredSessionToken()))); + return Task.FromResult(GetValue(gameService.GetCharacterCampaignOptions(GetRequiredSessionToken()))); } public Task GetCampaignAsync(Guid campaignId) { - return Task.FromResult(GetValue(m_GameService.GetCampaign(GetRequiredSessionToken(), campaignId))); + return Task.FromResult(GetValue(gameService.GetCampaign(GetRequiredSessionToken(), campaignId))); } public Task> GetUsernamesAsync() { - return Task.FromResult(GetValue(m_GameService.GetUsernames(GetRequiredSessionToken()))); + return Task.FromResult(GetValue(gameService.GetUsernames(GetRequiredSessionToken()))); } public Task GetCharacterSheetAsync(Guid characterId) { - return Task.FromResult(GetValue(m_GameService.GetCharacterSheet(GetRequiredSessionToken(), characterId))); + return Task.FromResult(GetValue(gameService.GetCharacterSheet(GetRequiredSessionToken(), characterId))); } public Task> GetCampaignLogAsync(Guid campaignId) { - return Task.FromResult(GetValue(m_GameService.GetCampaignLog(GetRequiredSessionToken(), campaignId))); + return Task.FromResult(GetValue(gameService.GetCampaignLog(GetRequiredSessionToken(), campaignId))); } public Task GetCampaignLogPageAsync(Guid campaignId, Guid? afterRollId = null, int? limit = null) { - return Task.FromResult(GetValue(m_GameService.GetCampaignLogPage(GetRequiredSessionToken(), campaignId, afterRollId, limit))); + return Task.FromResult(GetValue(gameService.GetCampaignLogPage(GetRequiredSessionToken(), campaignId, afterRollId, limit))); } public Task GetRollDetailAsync(Guid rollId) { - return Task.FromResult(GetValue(m_GameService.GetRollDetail(GetRequiredSessionToken(), rollId))); + return Task.FromResult(GetValue(gameService.GetRollDetail(GetRequiredSessionToken(), rollId))); } public Task> GetAdminUsersAsync() { - return Task.FromResult(GetValue(m_GameService.GetUsers(GetRequiredSessionToken()))); + return Task.FromResult(GetValue(gameService.GetUsers(GetRequiredSessionToken()))); } private string GetRequiredSessionToken() { - return m_SessionTokenAccessor.GetRequiredSessionToken(); + return sessionTokenAccessor.GetRequiredSessionToken(); } private static T GetValue(ServiceResult result) @@ -84,7 +78,4 @@ public sealed class WorkspaceQueryService var statusCode = error.Code == "unauthorized" ? 401 : 400; return new(statusCode, error.Message, error.Code); } - - private readonly IGameService m_GameService; - private readonly WorkspaceSessionTokenAccessor m_SessionTokenAccessor; } \ No newline at end of file diff --git a/RpgRoller/Data/RpgRollerDbContext.cs b/RpgRoller/Data/RpgRollerDbContext.cs index 853a943..23f7761 100644 --- a/RpgRoller/Data/RpgRollerDbContext.cs +++ b/RpgRoller/Data/RpgRollerDbContext.cs @@ -3,12 +3,8 @@ using RpgRoller.Domain; namespace RpgRoller.Data; -public sealed class RpgRollerDbContext : DbContext +public sealed class RpgRollerDbContext(DbContextOptions options) : DbContext(options) { - public RpgRollerDbContext(DbContextOptions options) : base(options) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => diff --git a/RpgRoller/Services/CampaignLogSummaryBuilder.cs b/RpgRoller/Services/CampaignLogSummaryBuilder.cs index 0e33a53..84aeab4 100644 --- a/RpgRoller/Services/CampaignLogSummaryBuilder.cs +++ b/RpgRoller/Services/CampaignLogSummaryBuilder.cs @@ -23,8 +23,8 @@ public static class CampaignLogSummaryBuilder switch (ruleset) { case RulesetKind.D6: - AddBadgeIfMissing(badges, dice.Any(die => die.Wild && die.Roll == 6), "w6"); - AddBadgeIfMissing(badges, dice.Any(die => die.Wild && die.Roll == 1), "w1"); + AddBadgeIfMissing(badges, dice.Any(die => die is { Wild: true, Roll: 6 }), "w6"); + AddBadgeIfMissing(badges, dice.Any(die => die is { Wild: true, Roll: 1 }), "w1"); break; case RulesetKind.Dnd5e: if (!string.IsNullOrWhiteSpace(expression) && IsSingleD20Expression(expression)) diff --git a/RpgRoller/Services/D6RollEngine.cs b/RpgRoller/Services/D6RollEngine.cs index 36f21db..1dd3f6e 100644 --- a/RpgRoller/Services/D6RollEngine.cs +++ b/RpgRoller/Services/D6RollEngine.cs @@ -3,13 +3,8 @@ using RpgRoller.Domain; namespace RpgRoller.Services; -public sealed class D6RollEngine +public sealed class D6RollEngine(IDiceRoller diceRoller) { - public D6RollEngine(IDiceRoller diceRoller) - { - m_DiceRoller = diceRoller; - } - public (int Total, string Breakdown, IReadOnlyList Dice) Roll(DiceExpression expression, int wildDice, bool allowFumble) { var initialDice = expression.DiceCount; @@ -20,7 +15,7 @@ public sealed class D6RollEngine for (var i = 0; i < currentDice; i += 1) { - var roll = m_DiceRoller.Roll(expression.Sides); + var roll = diceRoller.Roll(expression.Sides); var isWild = i < wildDice; var isCrit = false; var isFumble = false; @@ -89,6 +84,4 @@ public sealed class D6RollEngine return (total, RollBreakdownFormatter.BuildBreakdown(includedDice, expression.Modifier, total), dieResults); } - - private readonly IDiceRoller m_DiceRoller; } \ No newline at end of file diff --git a/RpgRoller/Services/GameAuthService.cs b/RpgRoller/Services/GameAuthService.cs index a9b3cc5..0474bec 100644 --- a/RpgRoller/Services/GameAuthService.cs +++ b/RpgRoller/Services/GameAuthService.cs @@ -4,15 +4,8 @@ using RpgRoller.Domain; namespace RpgRoller.Services; -public sealed class GameAuthService +public sealed class GameAuthService(GameStateStore stateStore, IPasswordHasher passwordHasher, GamePersistenceService persistenceService) { - public GameAuthService(GameStateStore stateStore, IPasswordHasher passwordHasher, GamePersistenceService persistenceService) - { - m_StateStore = stateStore; - m_PasswordHasher = passwordHasher; - m_PersistenceService = persistenceService; - } - public ServiceResult Register(string username, string password, string displayName) { if (string.IsNullOrWhiteSpace(username)) @@ -24,11 +17,11 @@ public sealed class GameAuthService if (string.IsNullOrWhiteSpace(password) || password.Length < 8) return ServiceResult.Failure("invalid_password", "Password must be at least 8 characters."); - lock (m_StateStore.Gate) + lock (stateStore.Gate) { var trimmedUsername = username.Trim(); var normalizedUsername = NormalizeUsername(trimmedUsername); - if (m_StateStore.UserIdsByUsername.ContainsKey(normalizedUsername)) + if (stateStore.UserIdsByUsername.ContainsKey(normalizedUsername)) return ServiceResult.Failure("duplicate_username", "Username is already taken."); var user = new UserAccount @@ -38,16 +31,16 @@ public sealed class GameAuthService UsernameNormalized = normalizedUsername, DisplayName = displayName.Trim(), PasswordHash = string.Empty, - Roles = m_StateStore.UsersById.Count == 0 ? UserRoles.Admin : string.Empty, + Roles = stateStore.UsersById.Count == 0 ? UserRoles.Admin : string.Empty, ActiveCharacterId = null }; - user.PasswordHash = m_PasswordHasher.HashPassword(user, password); + user.PasswordHash = passwordHasher.HashPassword(user, password); - m_StateStore.UsersById[user.Id] = user; - m_StateStore.UserIdsByUsername[user.UsernameNormalized] = user.Id; + stateStore.UsersById[user.Id] = user; + stateStore.UserIdsByUsername[user.UsernameNormalized] = user.Id; - m_PersistenceService.PersistStateLocked(); + persistenceService.PersistStateLocked(); return ServiceResult.Success(GameDtoMapper.ToUserSummary(user)); } } @@ -57,59 +50,59 @@ public sealed class GameAuthService if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password."); - lock (m_StateStore.Gate) + lock (stateStore.Gate) { var normalizedUsername = NormalizeUsername(username.Trim()); - if (!m_StateStore.UserIdsByUsername.TryGetValue(normalizedUsername, out var userId)) + if (!stateStore.UserIdsByUsername.TryGetValue(normalizedUsername, out var userId)) return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password."); - var user = m_StateStore.UsersById[userId]; - var verification = m_PasswordHasher.VerifyHashedPassword(user, user.PasswordHash, password); + var user = stateStore.UsersById[userId]; + var verification = passwordHasher.VerifyHashedPassword(user, user.PasswordHash, password); if (verification == PasswordVerificationResult.Failed) return ServiceResult<(UserSummary User, string SessionToken)>.Failure("invalid_credentials", "Invalid username or password."); if (verification == PasswordVerificationResult.SuccessRehashNeeded) - user.PasswordHash = m_PasswordHasher.HashPassword(user, password); + user.PasswordHash = passwordHasher.HashPassword(user, password); var session = CreateSession(userId); - m_PersistenceService.PersistStateLocked(); + persistenceService.PersistStateLocked(); return ServiceResult<(UserSummary User, string SessionToken)>.Success((GameDtoMapper.ToUserSummary(user), session.Token)); } } public void Logout(string sessionToken) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - if (m_StateStore.SessionsByToken.Remove(sessionToken)) - m_PersistenceService.PersistStateLocked(); + if (stateStore.SessionsByToken.Remove(sessionToken)) + persistenceService.PersistStateLocked(); } } public UserSummary? GetUserBySession(string sessionToken) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); return user is null ? null : GameDtoMapper.ToUserSummary(user); } } public ServiceResult GetMe(string sessionToken) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); Guid? campaignId = null; if (user.ActiveCharacterId is Guid activeCharacterId) { - if (!m_StateStore.CharactersById.TryGetValue(activeCharacterId, out var activeCharacter)) + if (!stateStore.CharactersById.TryGetValue(activeCharacterId, out var activeCharacter)) { user.ActiveCharacterId = null; - m_PersistenceService.PersistStateLocked(); + persistenceService.PersistStateLocked(); } else campaignId = activeCharacter.CampaignId; @@ -129,7 +122,7 @@ public sealed class GameAuthService CreatedAtUtc = DateTimeOffset.UtcNow }; - m_StateStore.SessionsByToken[token] = session; + stateStore.SessionsByToken[token] = session; return session; } @@ -137,8 +130,4 @@ public sealed class GameAuthService { return username.ToUpperInvariant(); } - - private readonly IPasswordHasher m_PasswordHasher; - private readonly GamePersistenceService m_PersistenceService; - private readonly GameStateStore m_StateStore; } \ No newline at end of file diff --git a/RpgRoller/Services/GameCampaignService.cs b/RpgRoller/Services/GameCampaignService.cs index 0895b48..9a9ae59 100644 --- a/RpgRoller/Services/GameCampaignService.cs +++ b/RpgRoller/Services/GameCampaignService.cs @@ -3,14 +3,8 @@ using RpgRoller.Domain; namespace RpgRoller.Services; -public sealed class GameCampaignService +public sealed class GameCampaignService(GameStateStore stateStore, GamePersistenceService persistenceService) { - public GameCampaignService(GameStateStore stateStore, GamePersistenceService persistenceService) - { - m_StateStore = stateStore; - m_PersistenceService = persistenceService; - } - public ServiceResult CreateCampaign(string sessionToken, string name, string rulesetId) { if (string.IsNullOrWhiteSpace(name)) @@ -20,9 +14,9 @@ public sealed class GameCampaignService if (ruleset is null) return ServiceResult.Failure("invalid_ruleset", "Unknown ruleset."); - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); @@ -35,21 +29,21 @@ public sealed class GameCampaignService Version = 1 }; - m_StateStore.CampaignsById[campaign.Id] = campaign; - m_PersistenceService.PersistStateLocked(); - return ServiceResult.Success(GameDtoMapper.ToCampaignSummary(m_StateStore, campaign)); + stateStore.CampaignsById[campaign.Id] = campaign; + persistenceService.PersistStateLocked(); + return ServiceResult.Success(GameDtoMapper.ToCampaignSummary(stateStore, campaign)); } } public ServiceResult> GetCampaigns(string sessionToken) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult>.Failure("unauthorized", "You must be logged in."); - var results = m_StateStore.CampaignsById.Values.Where(campaign => GameAuthorization.CanViewCampaign(m_StateStore, user.Id, campaign.Id)).OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).Select(campaign => GameDtoMapper.ToCampaignSummary(m_StateStore, campaign)).ToArray(); + var results = stateStore.CampaignsById.Values.Where(campaign => GameAuthorization.CanViewCampaign(stateStore, user.Id, campaign.Id)).OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).Select(campaign => GameDtoMapper.ToCampaignSummary(stateStore, campaign)).ToArray(); return ServiceResult>.Success(results); } @@ -57,13 +51,13 @@ public sealed class GameCampaignService public ServiceResult> GetCharacterCampaignOptions(string sessionToken) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult>.Failure("unauthorized", "You must be logged in."); - var options = m_StateStore.CampaignsById.Values.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).Select(GameDtoMapper.ToCampaignOption).ToArray(); + var options = stateStore.CampaignsById.Values.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).Select(GameDtoMapper.ToCampaignOption).ToArray(); return ServiceResult>.Success(options); } @@ -71,50 +65,47 @@ public sealed class GameCampaignService public ServiceResult GetCampaign(string sessionToken, Guid campaignId) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var context = GameContextResolver.ResolveCampaignContextLocked(m_StateStore, sessionToken, campaignId); + var context = GameContextResolver.ResolveCampaignContextLocked(stateStore, sessionToken, campaignId); if (!context.Succeeded) return ServiceResult.Failure(context.Error!.Code, context.Error.Message); var (_, campaign) = context.Value; - return ServiceResult.Success(GameDtoMapper.ToCampaignRoster(m_StateStore, campaign)); + return ServiceResult.Success(GameDtoMapper.ToCampaignRoster(stateStore, campaign)); } } public ServiceResult DeleteCampaign(string sessionToken, Guid campaignId) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); - if (!m_StateStore.CampaignsById.TryGetValue(campaignId, out var campaign)) + if (!stateStore.CampaignsById.TryGetValue(campaignId, out var campaign)) return ServiceResult.Failure("campaign_not_found", "Campaign was not found."); if (campaign.GmUserId != user.Id && !GameAuthorization.HasRole(user, UserRoles.Admin)) return ServiceResult.Failure("forbidden", "Only the campaign owner or admin can delete this campaign."); DeleteCampaignLocked(campaignId); - m_PersistenceService.PersistStateLocked(); + persistenceService.PersistStateLocked(); return ServiceResult.Success(true); } } private void DeleteCampaignLocked(Guid campaignId) { - if (!m_StateStore.CampaignsById.Remove(campaignId)) + if (!stateStore.CampaignsById.Remove(campaignId)) return; - var affectedCharacterIds = m_StateStore.CharactersById.Values.Where(character => character.CampaignId == campaignId).Select(character => character.Id).ToArray(); + var affectedCharacterIds = stateStore.CharactersById.Values.Where(character => character.CampaignId == campaignId).Select(character => character.Id).ToArray(); foreach (var characterId in affectedCharacterIds) - m_StateStore.CharactersById[characterId].CampaignId = null; + stateStore.CharactersById[characterId].CampaignId = null; - m_StateStore.RollLog.RemoveAll(entry => entry.CampaignId == campaignId); - m_StateStore.CampaignStateById.Remove(campaignId); + stateStore.RollLog.RemoveAll(entry => entry.CampaignId == campaignId); + stateStore.CampaignStateById.Remove(campaignId); } - - private readonly GamePersistenceService m_PersistenceService; - private readonly GameStateStore m_StateStore; } \ No newline at end of file diff --git a/RpgRoller/Services/GameCharacterService.cs b/RpgRoller/Services/GameCharacterService.cs index 7c15186..6653d4b 100644 --- a/RpgRoller/Services/GameCharacterService.cs +++ b/RpgRoller/Services/GameCharacterService.cs @@ -3,26 +3,20 @@ using RpgRoller.Domain; namespace RpgRoller.Services; -public sealed class GameCharacterService +public sealed class GameCharacterService(GameStateStore stateStore, GamePersistenceService persistenceService) { - public GameCharacterService(GameStateStore stateStore, GamePersistenceService persistenceService) - { - m_StateStore = stateStore; - m_PersistenceService = persistenceService; - } - public ServiceResult CreateCharacter(string sessionToken, string name, Guid campaignId) { if (string.IsNullOrWhiteSpace(name)) return ServiceResult.Failure("invalid_character_name", "Character name is required."); - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); - if (!m_StateStore.CampaignsById.ContainsKey(campaignId)) + if (!stateStore.CampaignsById.ContainsKey(campaignId)) return ServiceResult.Failure("campaign_not_found", "Campaign was not found."); var character = new Character @@ -33,12 +27,12 @@ public sealed class GameCharacterService Name = name.Trim() }; - m_StateStore.CharactersById[character.Id] = character; - m_StateStore.AddCharacterStateLocked(character.CampaignId, character.Id); - m_StateStore.TouchRosterLocked(character.CampaignId); + stateStore.CharactersById[character.Id] = character; + stateStore.AddCharacterStateLocked(character.CampaignId, character.Id); + stateStore.TouchRosterLocked(character.CampaignId); - m_PersistenceService.PersistStateLocked(); - return ServiceResult.Success(GameDtoMapper.ToCharacterSummary(m_StateStore, character)); + persistenceService.PersistStateLocked(); + return ServiceResult.Success(GameDtoMapper.ToCharacterSummary(stateStore, character)); } } @@ -47,22 +41,22 @@ public sealed class GameCharacterService if (string.IsNullOrWhiteSpace(name)) return ServiceResult.Failure("invalid_character_name", "Character name is required."); - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); - if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character)) + if (!stateStore.CharactersById.TryGetValue(characterId, out var character)) return ServiceResult.Failure("character_not_found", "Character was not found."); Campaign? targetCampaign = null; - if (campaignId.HasValue && !m_StateStore.CampaignsById.TryGetValue(campaignId.Value, out targetCampaign)) + if (campaignId.HasValue && !stateStore.CampaignsById.TryGetValue(campaignId.Value, out targetCampaign)) return ServiceResult.Failure("campaign_not_found", "Campaign was not found."); var isOwner = character.OwnerUserId == user.Id; var isAdmin = GameAuthorization.HasRole(user, UserRoles.Admin); - var isSourceGm = character.CampaignId.HasValue && m_StateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var sourceCampaign) && sourceCampaign.GmUserId == user.Id; + var isSourceGm = character.CampaignId.HasValue && stateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var sourceCampaign) && sourceCampaign.GmUserId == user.Id; var isTargetGm = targetCampaign is not null && targetCampaign.GmUserId == user.Id; if (!isOwner && !isAdmin && !isSourceGm && !isTargetGm) return ServiceResult.Failure("forbidden", "Only the owner, GM, or admin can edit this character."); @@ -76,41 +70,41 @@ public sealed class GameCharacterService { var trimmedOwnerUsername = ownerUsername.Trim(); var normalizedOwnerUsername = NormalizeUsername(trimmedOwnerUsername); - if (!m_StateStore.UserIdsByUsername.TryGetValue(normalizedOwnerUsername, out var targetOwnerUserId)) + if (!stateStore.UserIdsByUsername.TryGetValue(normalizedOwnerUsername, out var targetOwnerUserId)) return ServiceResult.Failure("owner_not_found", "Owner username was not found."); if (targetOwnerUserId != character.OwnerUserId && !isAdmin && !isSourceGm && !isTargetGm) return ServiceResult.Failure("forbidden", "Only the GM or admin can change character owner."); character.OwnerUserId = targetOwnerUserId; - if (character.OwnerUserId != previousOwnerUserId && m_StateStore.UsersById.TryGetValue(previousOwnerUserId, out var previousOwner) && previousOwner.ActiveCharacterId == character.Id) + if (character.OwnerUserId != previousOwnerUserId && stateStore.UsersById.TryGetValue(previousOwnerUserId, out var previousOwner) && previousOwner.ActiveCharacterId == character.Id) previousOwner.ActiveCharacterId = null; } if (sourceCampaignId != character.CampaignId) { - m_StateStore.RemoveCharacterStateLocked(sourceCampaignId, character.Id); - m_StateStore.AddCharacterStateLocked(character.CampaignId, character.Id); + stateStore.RemoveCharacterStateLocked(sourceCampaignId, character.Id); + stateStore.AddCharacterStateLocked(character.CampaignId, character.Id); } - m_StateStore.TouchRosterLocked(sourceCampaignId); + stateStore.TouchRosterLocked(sourceCampaignId); if (sourceCampaignId != character.CampaignId) - m_StateStore.TouchRosterLocked(character.CampaignId); + stateStore.TouchRosterLocked(character.CampaignId); - m_PersistenceService.PersistStateLocked(); - return ServiceResult.Success(GameDtoMapper.ToCharacterSummary(m_StateStore, character)); + persistenceService.PersistStateLocked(); + return ServiceResult.Success(GameDtoMapper.ToCharacterSummary(stateStore, character)); } } public ServiceResult DeleteCharacter(string sessionToken, Guid characterId) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); - if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character)) + if (!stateStore.CharactersById.TryGetValue(characterId, out var character)) return ServiceResult.Failure("character_not_found", "Character was not found."); var isOwner = character.OwnerUserId == user.Id; @@ -119,40 +113,40 @@ public sealed class GameCharacterService return ServiceResult.Failure("forbidden", "Only the owner or admin can delete this character."); DeleteCharacterLocked(characterId); - m_PersistenceService.PersistStateLocked(); + persistenceService.PersistStateLocked(); return ServiceResult.Success(true); } } public ServiceResult ActivateCharacter(string sessionToken, Guid characterId) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); - if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character)) + if (!stateStore.CharactersById.TryGetValue(characterId, out var character)) return ServiceResult.Failure("character_not_found", "Character was not found."); if (character.OwnerUserId != user.Id) return ServiceResult.Failure("forbidden", "You can activate only your own character."); user.ActiveCharacterId = character.Id; - m_PersistenceService.PersistStateLocked(); + persistenceService.PersistStateLocked(); return ServiceResult.Success(true); } } public ServiceResult> GetOwnCharacters(string sessionToken) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult>.Failure("unauthorized", "You must be logged in."); - var characters = m_StateStore.CharactersById.Values.Where(character => character.OwnerUserId == user.Id).OrderBy(character => character.Name, StringComparer.OrdinalIgnoreCase).Select(character => GameDtoMapper.ToCharacterSummary(m_StateStore, character)).ToArray(); + var characters = stateStore.CharactersById.Values.Where(character => character.OwnerUserId == user.Id).OrderBy(character => character.Name, StringComparer.OrdinalIgnoreCase).Select(character => GameDtoMapper.ToCharacterSummary(stateStore, character)).ToArray(); return ServiceResult>.Success(characters); } @@ -160,34 +154,31 @@ public sealed class GameCharacterService private void DeleteCharacterLocked(Guid characterId) { - if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character)) + if (!stateStore.CharactersById.TryGetValue(characterId, out var character)) return; var campaignId = character.CampaignId; - m_StateStore.CharactersById.Remove(characterId); + stateStore.CharactersById.Remove(characterId); - var skillGroupIds = m_StateStore.SkillGroupsById.Values.Where(group => group.CharacterId == characterId).Select(group => group.Id).ToHashSet(); + var skillGroupIds = stateStore.SkillGroupsById.Values.Where(group => group.CharacterId == characterId).Select(group => group.Id).ToHashSet(); foreach (var skillGroupId in skillGroupIds) - m_StateStore.SkillGroupsById.Remove(skillGroupId); + stateStore.SkillGroupsById.Remove(skillGroupId); - var skillIds = m_StateStore.SkillsById.Values.Where(skill => skill.CharacterId == characterId).Select(skill => skill.Id).ToHashSet(); + var skillIds = stateStore.SkillsById.Values.Where(skill => skill.CharacterId == characterId).Select(skill => skill.Id).ToHashSet(); foreach (var skillId in skillIds) - m_StateStore.SkillsById.Remove(skillId); + stateStore.SkillsById.Remove(skillId); - m_StateStore.RollLog.RemoveAll(entry => entry.CharacterId == characterId || skillIds.Contains(entry.SkillId)); + stateStore.RollLog.RemoveAll(entry => entry.CharacterId == characterId || skillIds.Contains(entry.SkillId)); - foreach (var user in m_StateStore.UsersById.Values.Where(account => account.ActiveCharacterId == characterId)) + foreach (var user in stateStore.UsersById.Values.Where(account => account.ActiveCharacterId == characterId)) user.ActiveCharacterId = null; - m_StateStore.RemoveCharacterStateLocked(campaignId, characterId); - m_StateStore.TouchRosterLocked(campaignId); + stateStore.RemoveCharacterStateLocked(campaignId, characterId); + stateStore.TouchRosterLocked(campaignId); } private static string NormalizeUsername(string username) { return username.ToUpperInvariant(); } - - private readonly GamePersistenceService m_PersistenceService; - private readonly GameStateStore m_StateStore; } \ No newline at end of file diff --git a/RpgRoller/Services/GameContextResolver.cs b/RpgRoller/Services/GameContextResolver.cs index 6c1eb02..821832c 100644 --- a/RpgRoller/Services/GameContextResolver.cs +++ b/RpgRoller/Services/GameContextResolver.cs @@ -32,8 +32,8 @@ public static class GameContextResolver public static bool TryResolveCharacterCampaignLocked(GameStateStore stateStore, Character character, out Campaign campaign, out ServiceError? error) { - campaign = default!; - if (!character.CampaignId.HasValue || !stateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var resolvedCampaign) || resolvedCampaign is null) + campaign = null!; + if (!character.CampaignId.HasValue || !stateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var resolvedCampaign)) { error = new("character_not_in_campaign", "Character is not linked to a campaign."); return false; diff --git a/RpgRoller/Services/GamePersistenceService.cs b/RpgRoller/Services/GamePersistenceService.cs index 1adc4d0..ae33d8b 100644 --- a/RpgRoller/Services/GamePersistenceService.cs +++ b/RpgRoller/Services/GamePersistenceService.cs @@ -4,17 +4,11 @@ using RpgRoller.Domain; namespace RpgRoller.Services; -public sealed class GamePersistenceService +public sealed class GamePersistenceService(IDbContextFactory dbContextFactory, GameStateStore stateStore) { - public GamePersistenceService(IDbContextFactory dbContextFactory, GameStateStore stateStore) - { - m_DbContextFactory = dbContextFactory; - m_StateStore = stateStore; - } - public void LoadStateFromDatabase() { - using var db = m_DbContextFactory.CreateDbContext(); + using var db = dbContextFactory.CreateDbContext(); var users = db.Users.AsNoTracking().ToList(); var sessions = db.Sessions.AsNoTracking().ToList(); var campaigns = db.Campaigns.AsNoTracking().ToList(); @@ -23,16 +17,16 @@ public sealed class GamePersistenceService var skills = db.Skills.AsNoTracking().ToList(); var logEntries = db.RollLogEntries.AsNoTracking().ToList().OrderBy(x => x.TimestampUtc).ThenBy(x => x.Id).ToList(); - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - m_StateStore.UsersById.Clear(); - m_StateStore.UserIdsByUsername.Clear(); - m_StateStore.SessionsByToken.Clear(); - m_StateStore.CampaignsById.Clear(); - m_StateStore.CharactersById.Clear(); - m_StateStore.SkillGroupsById.Clear(); - m_StateStore.SkillsById.Clear(); - m_StateStore.RollLog.Clear(); + stateStore.UsersById.Clear(); + stateStore.UserIdsByUsername.Clear(); + stateStore.SessionsByToken.Clear(); + stateStore.CampaignsById.Clear(); + stateStore.CharactersById.Clear(); + stateStore.SkillGroupsById.Clear(); + stateStore.SkillsById.Clear(); + stateStore.RollLog.Clear(); foreach (var user in users) { @@ -48,35 +42,35 @@ public sealed class GamePersistenceService Roles = string.IsNullOrWhiteSpace(user.Roles) ? string.Empty : RoleSerializer.Serialize(RoleSerializer.Parse(user.Roles)), ActiveCharacterId = user.ActiveCharacterId }; - m_StateStore.UsersById[storedUser.Id] = storedUser; - m_StateStore.UserIdsByUsername[normalizedUsername] = storedUser.Id; + stateStore.UsersById[storedUser.Id] = storedUser; + stateStore.UserIdsByUsername[normalizedUsername] = storedUser.Id; } foreach (var session in sessions) { - if (m_StateStore.UsersById.ContainsKey(session.UserId)) - m_StateStore.SessionsByToken[session.Token] = GameStateCloneFactory.CloneSession(session); + if (stateStore.UsersById.ContainsKey(session.UserId)) + stateStore.SessionsByToken[session.Token] = GameStateCloneFactory.CloneSession(session); } foreach (var campaign in campaigns) - m_StateStore.CampaignsById[campaign.Id] = GameStateCloneFactory.CloneCampaign(campaign); + stateStore.CampaignsById[campaign.Id] = GameStateCloneFactory.CloneCampaign(campaign); foreach (var character in characters) - m_StateStore.CharactersById[character.Id] = GameStateCloneFactory.CloneCharacter(character); + stateStore.CharactersById[character.Id] = GameStateCloneFactory.CloneCharacter(character); foreach (var skillGroup in skillGroups) - m_StateStore.SkillGroupsById[skillGroup.Id] = GameStateCloneFactory.CloneSkillGroup(skillGroup); + stateStore.SkillGroupsById[skillGroup.Id] = GameStateCloneFactory.CloneSkillGroup(skillGroup); foreach (var skill in skills) - m_StateStore.SkillsById[skill.Id] = GameStateCloneFactory.CloneSkill(skill); + stateStore.SkillsById[skill.Id] = GameStateCloneFactory.CloneSkill(skill); - m_StateStore.RollLog.AddRange(logEntries.Select(GameStateCloneFactory.CloneRollLogEntry)); + stateStore.RollLog.AddRange(logEntries.Select(GameStateCloneFactory.CloneRollLogEntry)); } } public void PersistStateLocked() { - using var db = m_DbContextFactory.CreateDbContext(); + using var db = dbContextFactory.CreateDbContext(); using var transaction = db.Database.BeginTransaction(); db.RollLogEntries.ExecuteDelete(); @@ -87,13 +81,16 @@ public sealed class GamePersistenceService db.Sessions.ExecuteDelete(); db.Users.ExecuteDelete(); - db.Users.AddRange(m_StateStore.UsersById.Values.Select(GameStateCloneFactory.CloneUser)); - db.Sessions.AddRange(m_StateStore.SessionsByToken.Values.Select(GameStateCloneFactory.CloneSession)); - db.Campaigns.AddRange(m_StateStore.CampaignsById.Values.Select(GameStateCloneFactory.CloneCampaign)); - db.Characters.AddRange(m_StateStore.CharactersById.Values.Select(GameStateCloneFactory.CloneCharacter)); - db.SkillGroups.AddRange(m_StateStore.SkillGroupsById.Values.Select(GameStateCloneFactory.CloneSkillGroup)); - db.Skills.AddRange(m_StateStore.SkillsById.Values.Select(GameStateCloneFactory.CloneSkill)); - db.RollLogEntries.AddRange(m_StateStore.RollLog.Select(GameStateCloneFactory.CloneRollLogEntry)); + lock (stateStore.Gate) + { + db.Users.AddRange(stateStore.UsersById.Values.Select(GameStateCloneFactory.CloneUser)); + db.Sessions.AddRange(stateStore.SessionsByToken.Values.Select(GameStateCloneFactory.CloneSession)); + db.Campaigns.AddRange(stateStore.CampaignsById.Values.Select(GameStateCloneFactory.CloneCampaign)); + db.Characters.AddRange(stateStore.CharactersById.Values.Select(GameStateCloneFactory.CloneCharacter)); + db.SkillGroups.AddRange(stateStore.SkillGroupsById.Values.Select(GameStateCloneFactory.CloneSkillGroup)); + db.Skills.AddRange(stateStore.SkillsById.Values.Select(GameStateCloneFactory.CloneSkill)); + db.RollLogEntries.AddRange(stateStore.RollLog.Select(GameStateCloneFactory.CloneRollLogEntry)); + } db.SaveChanges(); transaction.Commit(); @@ -103,7 +100,4 @@ public sealed class GamePersistenceService { return username.ToUpperInvariant(); } - - private readonly IDbContextFactory m_DbContextFactory; - private readonly GameStateStore m_StateStore; } \ No newline at end of file diff --git a/RpgRoller/Services/GameRollService.cs b/RpgRoller/Services/GameRollService.cs index b2e148f..617f5ba 100644 --- a/RpgRoller/Services/GameRollService.cs +++ b/RpgRoller/Services/GameRollService.cs @@ -4,29 +4,21 @@ using RpgRoller.Domain; namespace RpgRoller.Services; -public sealed class GameRollService +public sealed class GameRollService(GameStateStore stateStore, GamePersistenceService persistenceService, IDiceRoller diceRoller) { - public GameRollService(GameStateStore stateStore, GamePersistenceService persistenceService, IDiceRoller diceRoller) - { - m_StateStore = stateStore; - m_PersistenceService = persistenceService; - m_DiceRoller = diceRoller; - m_RollEngine = new(new(diceRoller), new(diceRoller), new(diceRoller)); - } - public ServiceResult RollSkill(string sessionToken, Guid skillId, string visibility) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); - if (!m_StateStore.SkillsById.TryGetValue(skillId, out var skill)) + if (!stateStore.SkillsById.TryGetValue(skillId, out var skill)) return ServiceResult.Failure("skill_not_found", "Skill was not found."); - var character = m_StateStore.CharactersById[skill.CharacterId]; - if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError)) + var character = stateStore.CharactersById[skill.CharacterId]; + if (!GameContextResolver.TryResolveCharacterCampaignLocked(stateStore, character, out var campaign, out var campaignError)) return ServiceResult.Failure(campaignError!.Code, campaignError.Message); if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign)) @@ -47,16 +39,16 @@ public sealed class GameRollService public ServiceResult RollCustom(string sessionToken, Guid characterId, string expression, string visibility) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); - if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character)) + if (!stateStore.CharactersById.TryGetValue(characterId, out var character)) return ServiceResult.Failure("character_not_found", "Character was not found."); - if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError)) + if (!GameContextResolver.TryResolveCharacterCampaignLocked(stateStore, character, out var campaign, out var campaignError)) return ServiceResult.Failure(campaignError!.Code, campaignError.Message); if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign)) @@ -78,13 +70,13 @@ public sealed class GameRollService public ServiceResult> GetCampaignLog(string sessionToken, Guid campaignId) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var context = GameContextResolver.ResolveCampaignContextLocked(m_StateStore, sessionToken, campaignId); + var context = GameContextResolver.ResolveCampaignContextLocked(stateStore, sessionToken, campaignId); if (!context.Succeeded) return ServiceResult>.Failure(context.Error!.Code, context.Error.Message); - var (user, campaign) = context.Value!; + var (user, campaign) = context.Value; var entries = GetVisibleCampaignLogEntriesLocked(user, campaign).TakeLast(CampaignLogHistoryWindowSize).Select(ToLogEntry).ToArray(); return ServiceResult>.Success(entries); @@ -93,13 +85,13 @@ public sealed class GameRollService public ServiceResult GetCampaignLogPage(string sessionToken, Guid campaignId, Guid? afterRollId = null, int? limit = null) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var context = GameContextResolver.ResolveCampaignContextLocked(m_StateStore, sessionToken, campaignId); + var context = GameContextResolver.ResolveCampaignContextLocked(stateStore, sessionToken, campaignId); if (!context.Succeeded) return ServiceResult.Failure(context.Error!.Code, context.Error.Message); - var (user, campaign) = context.Value!; + var (user, campaign) = context.Value; var pageSize = NormalizeCampaignLogPageSize(limit); var visibleEntries = GetVisibleCampaignLogEntriesLocked(user, campaign).ToArray(); @@ -133,17 +125,17 @@ public sealed class GameRollService public ServiceResult GetRollDetail(string sessionToken, Guid rollId) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); - var entry = m_StateStore.RollLog.FirstOrDefault(candidate => candidate.Id == rollId); + var entry = stateStore.RollLog.FirstOrDefault(candidate => candidate.Id == rollId); if (entry is null) return ServiceResult.Failure("roll_not_found", "Roll was not found."); - if (!m_StateStore.CampaignsById.TryGetValue(entry.CampaignId, out var campaign) || !GameAuthorization.CanViewRoll(m_StateStore, user.Id, campaign, entry)) + if (!stateStore.CampaignsById.TryGetValue(entry.CampaignId, out var campaign) || !GameAuthorization.CanViewRoll(stateStore, user.Id, campaign, entry)) return ServiceResult.Failure("roll_not_found", "Roll was not found."); return ServiceResult.Success(GameDtoMapper.ToCampaignRollDetail(entry, DeserializeDice(entry.Dice).ToArray())); @@ -152,13 +144,13 @@ public sealed class GameRollService public ServiceResult GetCampaignStateSnapshot(string sessionToken, Guid campaignId) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var context = GameContextResolver.ResolveCampaignContextLocked(m_StateStore, sessionToken, campaignId); + var context = GameContextResolver.ResolveCampaignContextLocked(stateStore, sessionToken, campaignId); if (!context.Succeeded) return ServiceResult.Failure(context.Error!.Code, context.Error.Message); - return ServiceResult.Success(GameDtoMapper.ToCampaignStateSnapshot(m_StateStore, context.Value!.Campaign.Id)); + return ServiceResult.Success(GameDtoMapper.ToCampaignStateSnapshot(stateStore, context.Value.Campaign.Id)); } } @@ -178,10 +170,10 @@ public sealed class GameRollService TimestampUtc = DateTimeOffset.UtcNow }; - m_StateStore.RollLog.Add(entry); - m_StateStore.TouchLogLocked(campaign.Id); + stateStore.RollLog.Add(entry); + stateStore.TouchLogLocked(campaign.Id); - m_PersistenceService.PersistStateLocked(); + persistenceService.PersistStateLocked(); return ServiceResult.Success(GameDtoMapper.ToRollResult(entry, roll.Dice)); } @@ -192,15 +184,15 @@ public sealed class GameRollService private IEnumerable GetVisibleCampaignLogEntriesLocked(UserAccount user, Campaign campaign) { - return m_StateStore.RollLog.Where(r => r.CampaignId == campaign.Id).Where(r => r.Visibility == RollVisibility.Public || r.RollerUserId == user.Id || campaign.GmUserId == user.Id).OrderBy(r => r.TimestampUtc).ThenBy(r => r.Id); + return stateStore.RollLog.Where(r => r.CampaignId == campaign.Id).Where(r => r.Visibility == RollVisibility.Public || r.RollerUserId == user.Id || campaign.GmUserId == user.Id).OrderBy(r => r.TimestampUtc).ThenBy(r => r.Id); } private CampaignLogEntry ToLogEntry(RollLogEntry entry) { var dice = DeserializeDice(entry.Dice); - var characterName = m_StateStore.CharactersById.TryGetValue(entry.CharacterId, out var character) ? character.Name : "Unknown character"; + var characterName = stateStore.CharactersById.TryGetValue(entry.CharacterId, out var character) ? character.Name : "Unknown character"; var skillName = ResolveLoggedSkillName(entry); - var rollerDisplayName = GameDtoMapper.ResolveOwnerDisplayName(m_StateStore, entry.RollerUserId, "Unknown owner"); + var rollerDisplayName = GameDtoMapper.ResolveOwnerDisplayName(stateStore, entry.RollerUserId, "Unknown owner"); return GameDtoMapper.ToCampaignLogEntry(entry, characterName, skillName, rollerDisplayName, dice); } @@ -208,7 +200,7 @@ public sealed class GameRollService private CampaignLogListEntry ToLogListEntry(UserAccount user, Campaign campaign, RollLogEntry entry) { var dice = DeserializeDice(entry.Dice); - var characterName = m_StateStore.CharactersById.TryGetValue(entry.CharacterId, out var character) ? character.Name : "Unknown character"; + var characterName = stateStore.CharactersById.TryGetValue(entry.CharacterId, out var character) ? character.Name : "Unknown character"; var skillName = ResolveLoggedSkillName(entry); var loggedExpression = ResolveLoggedExpression(entry); var eventBadges = CampaignLogSummaryBuilder.BuildCompactLogEventBadges(campaign.Ruleset, loggedExpression, dice); @@ -226,7 +218,7 @@ public sealed class GameRollService if (entry.SkillId == CustomRollSkillId) return CustomRollLabel; - return m_StateStore.SkillsById.TryGetValue(entry.SkillId, out var skill) ? skill.Name : "Unknown skill"; + return stateStore.SkillsById.TryGetValue(entry.SkillId, out var skill) ? skill.Name : "Unknown skill"; } private string? ResolveLoggedExpression(RollLogEntry entry) @@ -234,7 +226,7 @@ public sealed class GameRollService if (entry.SkillId == CustomRollSkillId) return CampaignLogSummaryBuilder.ExtractCustomRollExpression(entry.Breakdown, CustomRollBreakdownSeparator); - return m_StateStore.SkillsById.TryGetValue(entry.SkillId, out var skill) ? skill.DiceRollDefinition : null; + return stateStore.SkillsById.TryGetValue(entry.SkillId, out var skill) ? skill.DiceRollDefinition : null; } private string ResolveLogRollerLabel(UserAccount user, Campaign campaign, RollLogEntry entry) @@ -245,7 +237,7 @@ public sealed class GameRollService if (entry.RollerUserId == campaign.GmUserId) return "GM"; - return GameDtoMapper.ResolveOwnerDisplayName(m_StateStore, entry.RollerUserId, "Unknown owner"); + return GameDtoMapper.ResolveOwnerDisplayName(stateStore, entry.RollerUserId, "Unknown owner"); } private static string ResolveLogVisibilityLabel(UserAccount user, Campaign campaign, RollLogEntry entry) @@ -299,8 +291,6 @@ public sealed class GameRollService private const string CustomRollLabel = "Custom roll"; private static readonly Guid CustomRollSkillId = Guid.Empty; private static readonly JsonSerializerOptions DiceJsonOptions = RpgRollerJson.CreateSerializerOptions(); - private readonly IDiceRoller m_DiceRoller; - private readonly GamePersistenceService m_PersistenceService; - private readonly RollEngine m_RollEngine; - private readonly GameStateStore m_StateStore; + private readonly IDiceRoller m_DiceRoller = diceRoller; + private readonly RollEngine m_RollEngine = new(new(diceRoller), new(diceRoller), new(diceRoller)); } \ No newline at end of file diff --git a/RpgRoller/Services/GameService.cs b/RpgRoller/Services/GameService.cs index 5a3993a..88956e8 100644 --- a/RpgRoller/Services/GameService.cs +++ b/RpgRoller/Services/GameService.cs @@ -10,18 +10,18 @@ public sealed class GameService : IGameService { public GameService(IDbContextFactory dbContextFactory, IPasswordHasher passwordHasher, IDiceRoller diceRoller) { - m_StateStore = new(); - m_PersistenceService = new(dbContextFactory, m_StateStore); - m_AuthService = new(m_StateStore, passwordHasher, m_PersistenceService); - m_CampaignService = new(m_StateStore, m_PersistenceService); - m_CharacterService = new(m_StateStore, m_PersistenceService); - m_RollService = new(m_StateStore, m_PersistenceService, diceRoller); - m_SkillService = new(m_StateStore, m_PersistenceService); - m_UserAdministrationService = new(m_StateStore, m_PersistenceService); - m_PersistenceService.LoadStateFromDatabase(); - lock (m_StateStore.Gate) + GameStateStore stateStore = new(); + GamePersistenceService persistenceService = new(dbContextFactory, stateStore); + m_AuthService = new(stateStore, passwordHasher, persistenceService); + m_CampaignService = new(stateStore, persistenceService); + m_CharacterService = new(stateStore, persistenceService); + m_RollService = new(stateStore, persistenceService, diceRoller); + m_SkillService = new(stateStore, persistenceService); + m_UserAdministrationService = new(stateStore, persistenceService); + persistenceService.LoadStateFromDatabase(); + lock (stateStore.Gate) { - m_StateStore.RebuildCampaignStateLocked(); + stateStore.RebuildCampaignStateLocked(); } } @@ -191,12 +191,9 @@ public sealed class GameService : IGameService } private readonly GameAuthService m_AuthService; - private readonly GameCampaignService m_CampaignService; private readonly GameCharacterService m_CharacterService; - private readonly GamePersistenceService m_PersistenceService; private readonly GameRollService m_RollService; private readonly GameSkillService m_SkillService; - private readonly GameStateStore m_StateStore; private readonly GameUserAdministrationService m_UserAdministrationService; } \ No newline at end of file diff --git a/RpgRoller/Services/GameSkillService.cs b/RpgRoller/Services/GameSkillService.cs index 61bd9d5..c77a8b4 100644 --- a/RpgRoller/Services/GameSkillService.cs +++ b/RpgRoller/Services/GameSkillService.cs @@ -3,29 +3,23 @@ using RpgRoller.Domain; namespace RpgRoller.Services; -public sealed class GameSkillService +public sealed class GameSkillService(GameStateStore stateStore, GamePersistenceService persistenceService) { - public GameSkillService(GameStateStore stateStore, GamePersistenceService persistenceService) - { - m_StateStore = stateStore; - m_PersistenceService = persistenceService; - } - public ServiceResult CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null) { if (string.IsNullOrWhiteSpace(name)) return ServiceResult.Failure("invalid_skill_group_name", "Skill group name is required."); - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); - if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character)) + if (!stateStore.CharactersById.TryGetValue(characterId, out var character)) return ServiceResult.Failure("character_not_found", "Character was not found."); - if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError)) + if (!GameContextResolver.TryResolveCharacterCampaignLocked(stateStore, character, out var campaign, out var campaignError)) return ServiceResult.Failure(campaignError!.Code, campaignError.Message); if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign)) @@ -40,16 +34,16 @@ public sealed class GameSkillService Id = Guid.NewGuid(), CharacterId = character.Id, Name = name.Trim(), - DiceRollDefinition = prototypeValidation.Value!.CanonicalExpression, + DiceRollDefinition = prototypeValidation.Value.CanonicalExpression, WildDice = prototypeValidation.Value.WildDice, AllowFumble = prototypeValidation.Value.AllowFumble, FumbleRange = prototypeValidation.Value.FumbleRange }; - m_StateStore.SkillGroupsById[group.Id] = group; - m_StateStore.TouchCharacterLocked(campaign.Id, character.Id); + stateStore.SkillGroupsById[group.Id] = group; + stateStore.TouchCharacterLocked(campaign.Id, character.Id); - m_PersistenceService.PersistStateLocked(); + persistenceService.PersistStateLocked(); return ServiceResult.Success(GameDtoMapper.ToSkillGroupSummary(group)); } } @@ -59,17 +53,17 @@ public sealed class GameSkillService if (string.IsNullOrWhiteSpace(name)) return ServiceResult.Failure("invalid_skill_group_name", "Skill group name is required."); - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); - if (!m_StateStore.SkillGroupsById.TryGetValue(skillGroupId, out var group)) + if (!stateStore.SkillGroupsById.TryGetValue(skillGroupId, out var group)) return ServiceResult.Failure("skill_group_not_found", "Skill group was not found."); - var character = m_StateStore.CharactersById[group.CharacterId]; - if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError)) + var character = stateStore.CharactersById[group.CharacterId]; + if (!GameContextResolver.TryResolveCharacterCampaignLocked(stateStore, character, out var campaign, out var campaignError)) return ServiceResult.Failure(campaignError!.Code, campaignError.Message); if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign)) @@ -80,42 +74,42 @@ public sealed class GameSkillService return ServiceResult.Failure(prototypeValidation.Error!.Code, prototypeValidation.Error.Message); group.Name = name.Trim(); - group.DiceRollDefinition = prototypeValidation.Value!.CanonicalExpression; + group.DiceRollDefinition = prototypeValidation.Value.CanonicalExpression; group.WildDice = prototypeValidation.Value.WildDice; group.AllowFumble = prototypeValidation.Value.AllowFumble; group.FumbleRange = prototypeValidation.Value.FumbleRange; - m_StateStore.TouchCharacterLocked(campaign.Id, character.Id); + stateStore.TouchCharacterLocked(campaign.Id, character.Id); - m_PersistenceService.PersistStateLocked(); + persistenceService.PersistStateLocked(); return ServiceResult.Success(GameDtoMapper.ToSkillGroupSummary(group)); } } public ServiceResult DeleteSkillGroup(string sessionToken, Guid skillGroupId) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); - if (!m_StateStore.SkillGroupsById.TryGetValue(skillGroupId, out var group)) + if (!stateStore.SkillGroupsById.TryGetValue(skillGroupId, out var group)) return ServiceResult.Failure("skill_group_not_found", "Skill group was not found."); - var character = m_StateStore.CharactersById[group.CharacterId]; - if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError)) + var character = stateStore.CharactersById[group.CharacterId]; + if (!GameContextResolver.TryResolveCharacterCampaignLocked(stateStore, character, out var campaign, out var campaignError)) return ServiceResult.Failure(campaignError!.Code, campaignError.Message); if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign)) return ServiceResult.Failure("forbidden", "Only the owner or GM can manage skill groups."); - foreach (var skill in m_StateStore.SkillsById.Values.Where(skill => skill.SkillGroupId == group.Id)) + foreach (var skill in stateStore.SkillsById.Values.Where(skill => skill.SkillGroupId == group.Id)) skill.SkillGroupId = null; - m_StateStore.SkillGroupsById.Remove(group.Id); - m_StateStore.TouchCharacterLocked(campaign.Id, character.Id); + stateStore.SkillGroupsById.Remove(group.Id); + stateStore.TouchCharacterLocked(campaign.Id, character.Id); - m_PersistenceService.PersistStateLocked(); + persistenceService.PersistStateLocked(); return ServiceResult.Success(true); } } @@ -125,16 +119,16 @@ public sealed class GameSkillService if (string.IsNullOrWhiteSpace(name)) return ServiceResult.Failure("invalid_skill_name", "Skill name is required."); - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); - if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character)) + if (!stateStore.CharactersById.TryGetValue(characterId, out var character)) return ServiceResult.Failure("character_not_found", "Character was not found."); - if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError)) + if (!GameContextResolver.TryResolveCharacterCampaignLocked(stateStore, character, out var campaign, out var campaignError)) return ServiceResult.Failure(campaignError!.Code, campaignError.Message); if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign)) @@ -154,16 +148,16 @@ public sealed class GameSkillService CharacterId = character.Id, SkillGroupId = resolvedSkillGroupId.Value, Name = name.Trim(), - DiceRollDefinition = skillValidation.Value!.CanonicalExpression, + DiceRollDefinition = skillValidation.Value.CanonicalExpression, WildDice = skillValidation.Value.WildDice, AllowFumble = skillValidation.Value.AllowFumble, FumbleRange = skillValidation.Value.FumbleRange }; - m_StateStore.SkillsById[skill.Id] = skill; - m_StateStore.TouchCharacterLocked(campaign.Id, character.Id); + stateStore.SkillsById[skill.Id] = skill; + stateStore.TouchCharacterLocked(campaign.Id, character.Id); - m_PersistenceService.PersistStateLocked(); + persistenceService.PersistStateLocked(); return ServiceResult.Success(GameDtoMapper.ToSkillSummary(skill)); } } @@ -173,17 +167,17 @@ public sealed class GameSkillService if (string.IsNullOrWhiteSpace(name)) return ServiceResult.Failure("invalid_skill_name", "Skill name is required."); - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); - if (!m_StateStore.SkillsById.TryGetValue(skillId, out var skill)) + if (!stateStore.SkillsById.TryGetValue(skillId, out var skill)) return ServiceResult.Failure("skill_not_found", "Skill was not found."); - var character = m_StateStore.CharactersById[skill.CharacterId]; - if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError)) + var character = stateStore.CharactersById[skill.CharacterId]; + if (!GameContextResolver.TryResolveCharacterCampaignLocked(stateStore, character, out var campaign, out var campaignError)) return ServiceResult.Failure(campaignError!.Code, campaignError.Message); if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign)) @@ -198,62 +192,62 @@ public sealed class GameSkillService return ServiceResult.Failure(resolvedSkillGroupId.Error!.Code, resolvedSkillGroupId.Error.Message); skill.Name = name.Trim(); - skill.DiceRollDefinition = skillValidation.Value!.CanonicalExpression; + skill.DiceRollDefinition = skillValidation.Value.CanonicalExpression; skill.WildDice = skillValidation.Value.WildDice; skill.AllowFumble = skillValidation.Value.AllowFumble; skill.FumbleRange = skillValidation.Value.FumbleRange; skill.SkillGroupId = resolvedSkillGroupId.Value; - m_StateStore.TouchCharacterLocked(campaign.Id, character.Id); + stateStore.TouchCharacterLocked(campaign.Id, character.Id); - m_PersistenceService.PersistStateLocked(); + persistenceService.PersistStateLocked(); return ServiceResult.Success(GameDtoMapper.ToSkillSummary(skill)); } } public ServiceResult DeleteSkill(string sessionToken, Guid skillId) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); - if (!m_StateStore.SkillsById.TryGetValue(skillId, out var skill)) + if (!stateStore.SkillsById.TryGetValue(skillId, out var skill)) return ServiceResult.Failure("skill_not_found", "Skill was not found."); - var character = m_StateStore.CharactersById[skill.CharacterId]; - if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError)) + var character = stateStore.CharactersById[skill.CharacterId]; + if (!GameContextResolver.TryResolveCharacterCampaignLocked(stateStore, character, out var campaign, out var campaignError)) return ServiceResult.Failure(campaignError!.Code, campaignError.Message); if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign)) return ServiceResult.Failure("forbidden", "Only the owner or GM can edit skills."); - m_StateStore.SkillsById.Remove(skill.Id); - m_StateStore.TouchCharacterLocked(campaign.Id, character.Id); + stateStore.SkillsById.Remove(skill.Id); + stateStore.TouchCharacterLocked(campaign.Id, character.Id); - m_PersistenceService.PersistStateLocked(); + persistenceService.PersistStateLocked(); return ServiceResult.Success(true); } } public ServiceResult GetCharacterSheet(string sessionToken, Guid characterId) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); - if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character)) + if (!stateStore.CharactersById.TryGetValue(characterId, out var character)) return ServiceResult.Failure("character_not_found", "Character was not found."); - if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError)) + if (!GameContextResolver.TryResolveCharacterCampaignLocked(stateStore, character, out var campaign, out var campaignError)) return ServiceResult.Failure(campaignError!.Code, campaignError.Message); - if (!GameAuthorization.CanViewCampaign(m_StateStore, user.Id, campaign.Id)) + if (!GameAuthorization.CanViewCampaign(stateStore, user.Id, campaign.Id)) return ServiceResult.Failure("forbidden", "You are not a participant in this campaign."); - return ServiceResult.Success(GameDtoMapper.ToCharacterSheet(m_StateStore, character.Id)); + return ServiceResult.Success(GameDtoMapper.ToCharacterSheet(stateStore, character.Id)); } } @@ -262,7 +256,7 @@ public sealed class GameSkillService if (!requestedSkillGroupId.HasValue) return ServiceResult.Success(null); - if (!m_StateStore.SkillGroupsById.TryGetValue(requestedSkillGroupId.Value, out var skillGroup)) + if (!stateStore.SkillGroupsById.TryGetValue(requestedSkillGroupId.Value, out var skillGroup)) return ServiceResult.Failure("skill_group_not_found", "Skill group was not found."); if (skillGroup.CharacterId != characterId) @@ -270,7 +264,4 @@ public sealed class GameSkillService return ServiceResult.Success(skillGroup.Id); } - - private readonly GamePersistenceService m_PersistenceService; - private readonly GameStateStore m_StateStore; } \ No newline at end of file diff --git a/RpgRoller/Services/GameUserAdministrationService.cs b/RpgRoller/Services/GameUserAdministrationService.cs index df2e489..0ac4b1d 100644 --- a/RpgRoller/Services/GameUserAdministrationService.cs +++ b/RpgRoller/Services/GameUserAdministrationService.cs @@ -3,23 +3,17 @@ using RpgRoller.Domain; namespace RpgRoller.Services; -public sealed class GameUserAdministrationService +public sealed class GameUserAdministrationService(GameStateStore stateStore, GamePersistenceService persistenceService) { - public GameUserAdministrationService(GameStateStore stateStore, GamePersistenceService persistenceService) - { - m_StateStore = stateStore; - m_PersistenceService = persistenceService; - } - public ServiceResult> GetUsernames(string sessionToken) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult>.Failure("unauthorized", "You must be logged in."); - var usernames = m_StateStore.UsersById.Values.Select(account => account.Username).OrderBy(username => username, StringComparer.OrdinalIgnoreCase).ToArray(); + var usernames = stateStore.UsersById.Values.Select(account => account.Username).OrderBy(username => username, StringComparer.OrdinalIgnoreCase).ToArray(); return ServiceResult>.Success(usernames); } @@ -27,16 +21,16 @@ public sealed class GameUserAdministrationService public ServiceResult> GetUsers(string sessionToken) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult>.Failure("unauthorized", "You must be logged in."); if (!GameAuthorization.HasRole(user, UserRoles.Admin)) return ServiceResult>.Failure("forbidden", "Admin role is required."); - var users = m_StateStore.UsersById.Values.OrderBy(account => account.Username, StringComparer.OrdinalIgnoreCase).Select(GameDtoMapper.ToAdminUserSummary).ToArray(); + var users = stateStore.UsersById.Values.OrderBy(account => account.Username, StringComparer.OrdinalIgnoreCase).Select(GameDtoMapper.ToAdminUserSummary).ToArray(); return ServiceResult>.Success(users); } @@ -44,16 +38,16 @@ public sealed class GameUserAdministrationService public ServiceResult UpdateUserRoles(string sessionToken, Guid userId, IReadOnlyList roles) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); if (!GameAuthorization.HasRole(user, UserRoles.Admin)) return ServiceResult.Failure("forbidden", "Admin role is required."); - if (!m_StateStore.UsersById.TryGetValue(userId, out var targetUser)) + if (!stateStore.UsersById.TryGetValue(userId, out var targetUser)) return ServiceResult.Failure("user_not_found", "User was not found."); var normalizedRoles = RoleSerializer.Normalize(roles); @@ -64,16 +58,16 @@ public sealed class GameUserAdministrationService return ServiceResult.Failure("forbidden", "You cannot remove your own admin role."); targetUser.Roles = RoleSerializer.Serialize(normalizedRoles); - m_PersistenceService.PersistStateLocked(); + persistenceService.PersistStateLocked(); return ServiceResult.Success(GameDtoMapper.ToAdminUserSummary(targetUser)); } } public ServiceResult DeleteUser(string sessionToken, Guid userId) { - lock (m_StateStore.Gate) + lock (stateStore.Gate) { - var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken); + var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); @@ -83,72 +77,69 @@ public sealed class GameUserAdministrationService if (user.Id == userId) return ServiceResult.Failure("forbidden", "You cannot delete your own account."); - if (!m_StateStore.UsersById.TryGetValue(userId, out var targetUser)) + if (!stateStore.UsersById.TryGetValue(userId, out var targetUser)) return ServiceResult.Failure("user_not_found", "User was not found."); - var gmCampaignIds = m_StateStore.CampaignsById.Values.Where(campaign => campaign.GmUserId == targetUser.Id).Select(campaign => campaign.Id).ToArray(); + var gmCampaignIds = stateStore.CampaignsById.Values.Where(campaign => campaign.GmUserId == targetUser.Id).Select(campaign => campaign.Id).ToArray(); var gmCampaignIdSet = gmCampaignIds.ToHashSet(); - var preservedCharacterIds = m_StateStore.CharactersById.Values.Where(character => character.CampaignId.HasValue && gmCampaignIdSet.Contains(character.CampaignId.Value)).Select(character => character.Id).ToHashSet(); + var preservedCharacterIds = stateStore.CharactersById.Values.Where(character => character.CampaignId.HasValue && gmCampaignIdSet.Contains(character.CampaignId.Value)).Select(character => character.Id).ToHashSet(); foreach (var campaignId in gmCampaignIds) DeleteCampaignLocked(campaignId); - var ownedCharacterIds = m_StateStore.CharactersById.Values.Where(character => character.OwnerUserId == targetUser.Id && !preservedCharacterIds.Contains(character.Id)).Select(character => character.Id).ToArray(); + var ownedCharacterIds = stateStore.CharactersById.Values.Where(character => character.OwnerUserId == targetUser.Id && !preservedCharacterIds.Contains(character.Id)).Select(character => character.Id).ToArray(); foreach (var characterId in ownedCharacterIds) DeleteCharacterLocked(characterId); - m_StateStore.RollLog.RemoveAll(entry => entry.RollerUserId == targetUser.Id); + stateStore.RollLog.RemoveAll(entry => entry.RollerUserId == targetUser.Id); - var staleSessions = m_StateStore.SessionsByToken.Values.Where(session => session.UserId == targetUser.Id).Select(session => session.Token).ToArray(); + var staleSessions = stateStore.SessionsByToken.Values.Where(session => session.UserId == targetUser.Id).Select(session => session.Token).ToArray(); foreach (var token in staleSessions) - m_StateStore.SessionsByToken.Remove(token); + stateStore.SessionsByToken.Remove(token); - m_StateStore.UsersById.Remove(targetUser.Id); - m_StateStore.UserIdsByUsername.Remove(targetUser.UsernameNormalized); + stateStore.UsersById.Remove(targetUser.Id); + stateStore.UserIdsByUsername.Remove(targetUser.UsernameNormalized); - m_PersistenceService.PersistStateLocked(); + persistenceService.PersistStateLocked(); return ServiceResult.Success(true); } } private void DeleteCampaignLocked(Guid campaignId) { - if (!m_StateStore.CampaignsById.Remove(campaignId)) + if (!stateStore.CampaignsById.Remove(campaignId)) return; - var affectedCharacterIds = m_StateStore.CharactersById.Values.Where(character => character.CampaignId == campaignId).Select(character => character.Id).ToArray(); + var affectedCharacterIds = stateStore.CharactersById.Values.Where(character => character.CampaignId == campaignId).Select(character => character.Id).ToArray(); foreach (var characterId in affectedCharacterIds) - m_StateStore.CharactersById[characterId].CampaignId = null; + stateStore.CharactersById[characterId].CampaignId = null; - m_StateStore.RollLog.RemoveAll(entry => entry.CampaignId == campaignId); - m_StateStore.CampaignStateById.Remove(campaignId); + stateStore.RollLog.RemoveAll(entry => entry.CampaignId == campaignId); + stateStore.CampaignStateById.Remove(campaignId); } private void DeleteCharacterLocked(Guid characterId) { - if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character)) + if (!stateStore.CharactersById.TryGetValue(characterId, out var character)) return; var campaignId = character.CampaignId; - m_StateStore.CharactersById.Remove(characterId); + stateStore.CharactersById.Remove(characterId); - var skillGroupIds = m_StateStore.SkillGroupsById.Values.Where(group => group.CharacterId == characterId).Select(group => group.Id).ToHashSet(); + var skillGroupIds = stateStore.SkillGroupsById.Values.Where(group => group.CharacterId == characterId).Select(group => group.Id).ToHashSet(); foreach (var skillGroupId in skillGroupIds) - m_StateStore.SkillGroupsById.Remove(skillGroupId); + stateStore.SkillGroupsById.Remove(skillGroupId); - var skillIds = m_StateStore.SkillsById.Values.Where(skill => skill.CharacterId == characterId).Select(skill => skill.Id).ToHashSet(); + var skillIds = stateStore.SkillsById.Values.Where(skill => skill.CharacterId == characterId).Select(skill => skill.Id).ToHashSet(); foreach (var skillId in skillIds) - m_StateStore.SkillsById.Remove(skillId); + stateStore.SkillsById.Remove(skillId); - m_StateStore.RollLog.RemoveAll(entry => entry.CharacterId == characterId || skillIds.Contains(entry.SkillId)); + stateStore.RollLog.RemoveAll(entry => entry.CharacterId == characterId || skillIds.Contains(entry.SkillId)); - foreach (var account in m_StateStore.UsersById.Values.Where(account => account.ActiveCharacterId == characterId)) + foreach (var account in stateStore.UsersById.Values.Where(account => account.ActiveCharacterId == characterId)) account.ActiveCharacterId = null; - m_StateStore.RemoveCharacterStateLocked(campaignId, characterId); - m_StateStore.TouchRosterLocked(campaignId); + stateStore.RemoveCharacterStateLocked(campaignId, characterId); + stateStore.TouchRosterLocked(campaignId); } - - private readonly GamePersistenceService m_PersistenceService; - private readonly GameStateStore m_StateStore; } \ No newline at end of file diff --git a/RpgRoller/Services/RolemasterRollEngine.cs b/RpgRoller/Services/RolemasterRollEngine.cs index 772dec3..9bdca4a 100644 --- a/RpgRoller/Services/RolemasterRollEngine.cs +++ b/RpgRoller/Services/RolemasterRollEngine.cs @@ -3,13 +3,8 @@ using RpgRoller.Domain; namespace RpgRoller.Services; -public sealed class RolemasterRollEngine +public sealed class RolemasterRollEngine(IDiceRoller diceRoller) { - public RolemasterRollEngine(IDiceRoller diceRoller) - { - m_DiceRoller = diceRoller; - } - public (int Total, string Breakdown, IReadOnlyList Dice) Roll(DiceExpression expression, int? fumbleRange) { return expression.Kind switch @@ -26,7 +21,7 @@ public sealed class RolemasterRollEngine var total = expression.Modifier; for (var i = 0; i < expression.DiceCount; i += 1) { - var value = m_DiceRoller.Roll(expression.Sides); + var value = diceRoller.Roll(expression.Sides); diceValues[i] = value; dice[i] = CreateRolemasterDie(value, i + 1, RollDieKinds.RolemasterStandard, value); total += value; @@ -37,7 +32,7 @@ public sealed class RolemasterRollEngine private (int Total, string Breakdown, IReadOnlyList Dice) RollOpenEnded(DiceExpression expression, int fumbleRange) { - var initialRoll = m_DiceRoller.Roll(expression.Sides); + var initialRoll = diceRoller.Roll(expression.Sides); var followUpRolls = new List(); int? initialContribution = initialRoll <= fumbleRange ? null : initialRoll; var dice = new List { CreateRolemasterDie(initialRoll, 1, RollDieKinds.RolemasterOpenEndedInitial, initialContribution) }; @@ -68,7 +63,7 @@ public sealed class RolemasterRollEngine while (true) { - var roll = m_DiceRoller.Roll(100); + var roll = diceRoller.Roll(100); followUpRolls.Add(roll); dice.Add(CreateRolemasterDie(roll, sequence, subtract ? RollDieKinds.RolemasterOpenEndedLowSubtract : RollDieKinds.RolemasterOpenEndedHigh, subtract ? -roll : roll)); @@ -84,6 +79,4 @@ public sealed class RolemasterRollEngine { return new(roll, false, false, false, false, kind == RollDieKinds.RolemasterOpenEndedHigh, sequence, kind, signedContribution); } - - private readonly IDiceRoller m_DiceRoller; } \ No newline at end of file diff --git a/RpgRoller/Services/RollEngine.cs b/RpgRoller/Services/RollEngine.cs index 45d34ef..420c25b 100644 --- a/RpgRoller/Services/RollEngine.cs +++ b/RpgRoller/Services/RollEngine.cs @@ -3,27 +3,16 @@ using RpgRoller.Domain; namespace RpgRoller.Services; -public sealed class RollEngine +public sealed class RollEngine(StandardRollEngine standardRollEngine, D6RollEngine d6RollEngine, RolemasterRollEngine rolemasterRollEngine) { - public RollEngine(StandardRollEngine standardRollEngine, D6RollEngine d6RollEngine, RolemasterRollEngine rolemasterRollEngine) - { - m_StandardRollEngine = standardRollEngine; - m_D6RollEngine = d6RollEngine; - m_RolemasterRollEngine = rolemasterRollEngine; - } - public (int Total, string Breakdown, IReadOnlyList Dice) Roll(RulesetKind ruleset, DiceExpression expression, int wildDice, bool allowFumble, int? fumbleRange) { if (ruleset == RulesetKind.D6) - return m_D6RollEngine.Roll(expression, wildDice, allowFumble); + return d6RollEngine.Roll(expression, wildDice, allowFumble); if (ruleset == RulesetKind.Rolemaster) - return m_RolemasterRollEngine.Roll(expression, fumbleRange); + return rolemasterRollEngine.Roll(expression, fumbleRange); - return m_StandardRollEngine.Roll(expression); + return standardRollEngine.Roll(expression); } - - private readonly D6RollEngine m_D6RollEngine; - private readonly RolemasterRollEngine m_RolemasterRollEngine; - private readonly StandardRollEngine m_StandardRollEngine; } \ No newline at end of file diff --git a/RpgRoller/Services/SkillDefinitionValidator.cs b/RpgRoller/Services/SkillDefinitionValidator.cs index b4659fb..66360c2 100644 --- a/RpgRoller/Services/SkillDefinitionValidator.cs +++ b/RpgRoller/Services/SkillDefinitionValidator.cs @@ -14,7 +14,7 @@ public static class SkillDefinitionValidator if (!optionsValidation.Succeeded) return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Failure(optionsValidation.Error!.Code, optionsValidation.Error.Message); - return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Success((expressionValidation.Value!.Canonical, optionsValidation.Value!.WildDice, optionsValidation.Value.AllowFumble, optionsValidation.Value.FumbleRange)); + return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Success((expressionValidation.Value!.Canonical, optionsValidation.Value.WildDice, optionsValidation.Value.AllowFumble, optionsValidation.Value.FumbleRange)); } private static ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)> ValidateOptions(RulesetKind ruleset, DiceExpression expression, int wildDice, bool allowFumble, int? fumbleRange) diff --git a/RpgRoller/Services/StandardRollEngine.cs b/RpgRoller/Services/StandardRollEngine.cs index 098d244..3004160 100644 --- a/RpgRoller/Services/StandardRollEngine.cs +++ b/RpgRoller/Services/StandardRollEngine.cs @@ -3,13 +3,8 @@ using RpgRoller.Domain; namespace RpgRoller.Services; -public sealed class StandardRollEngine +public sealed class StandardRollEngine(IDiceRoller diceRoller) { - public StandardRollEngine(IDiceRoller diceRoller) - { - m_DiceRoller = diceRoller; - } - public (int Total, string Breakdown, IReadOnlyList Dice) Roll(DiceExpression expression) { var diceValues = new int[expression.DiceCount]; @@ -17,7 +12,7 @@ public sealed class StandardRollEngine var total = expression.Modifier; for (var i = 0; i < expression.DiceCount; i += 1) { - var value = m_DiceRoller.Roll(expression.Sides); + var value = diceRoller.Roll(expression.Sides); diceValues[i] = value; dice[i] = new(value, false, false, false, false, false); total += value; @@ -25,6 +20,4 @@ public sealed class StandardRollEngine return (total, RollBreakdownFormatter.BuildBreakdown(diceValues, expression.Modifier, total), dice); } - - private readonly IDiceRoller m_DiceRoller; } \ No newline at end of file