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