Extract shared game service helpers
This commit is contained in:
@@ -48,7 +48,7 @@ public sealed class GameAuthService
|
||||
m_StateStore.UserIdsByUsername[user.UsernameNormalized] = user.Id;
|
||||
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
return ServiceResult<UserSummary>.Success(ToUserSummary(user));
|
||||
return ServiceResult<UserSummary>.Success(GameDtoMapper.ToUserSummary(user));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ public sealed class GameAuthService
|
||||
|
||||
var session = CreateSession(userId);
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
return ServiceResult<(UserSummary User, string SessionToken)>.Success((ToUserSummary(user), session.Token));
|
||||
return ServiceResult<(UserSummary User, string SessionToken)>.Success((GameDtoMapper.ToUserSummary(user), session.Token));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,8 +90,8 @@ public sealed class GameAuthService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
return user is null ? null : ToUserSummary(user);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
return user is null ? null : GameDtoMapper.ToUserSummary(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ public sealed class GameAuthService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<MeResponse>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
@@ -115,7 +115,7 @@ public sealed class GameAuthService
|
||||
campaignId = activeCharacter.CampaignId;
|
||||
}
|
||||
|
||||
return ServiceResult<MeResponse>.Success(new(ToUserSummary(user), user.ActiveCharacterId, campaignId));
|
||||
return ServiceResult<MeResponse>.Success(new(GameDtoMapper.ToUserSummary(user), user.ActiveCharacterId, campaignId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,22 +133,6 @@ public sealed class GameAuthService
|
||||
return session;
|
||||
}
|
||||
|
||||
private UserAccount? ResolveUserLocked(string sessionToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sessionToken))
|
||||
return null;
|
||||
|
||||
if (!m_StateStore.SessionsByToken.TryGetValue(sessionToken, out var session))
|
||||
return null;
|
||||
|
||||
return m_StateStore.UsersById.GetValueOrDefault(session.UserId);
|
||||
}
|
||||
|
||||
private static UserSummary ToUserSummary(UserAccount user)
|
||||
{
|
||||
return new(user.Id, user.Username, user.DisplayName, RoleSerializer.Parse(user.Roles));
|
||||
}
|
||||
|
||||
private static string NormalizeUsername(string username)
|
||||
{
|
||||
return username.ToUpperInvariant();
|
||||
|
||||
36
RpgRoller/Services/GameAuthorization.cs
Normal file
36
RpgRoller/Services/GameAuthorization.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using RpgRoller.Domain;
|
||||
|
||||
namespace RpgRoller.Services;
|
||||
|
||||
public static class GameAuthorization
|
||||
{
|
||||
public static bool HasRole(UserAccount user, string role)
|
||||
{
|
||||
return RoleSerializer.HasRole(user.Roles, role);
|
||||
}
|
||||
|
||||
public static bool CanViewCampaign(GameStateStore stateStore, Guid actorUserId, Guid campaignId)
|
||||
{
|
||||
if (stateStore.UsersById.TryGetValue(actorUserId, out var user) && HasRole(user, UserRoles.Admin))
|
||||
return true;
|
||||
|
||||
var campaign = stateStore.CampaignsById[campaignId];
|
||||
if (campaign.GmUserId == actorUserId)
|
||||
return true;
|
||||
|
||||
return stateStore.CharactersById.Values.Any(character =>
|
||||
character.CampaignId == campaignId &&
|
||||
character.OwnerUserId == actorUserId);
|
||||
}
|
||||
|
||||
public static bool CanEditCharacter(Guid actorUserId, Character character, Campaign campaign)
|
||||
{
|
||||
return character.OwnerUserId == actorUserId || campaign.GmUserId == actorUserId;
|
||||
}
|
||||
|
||||
public static bool CanViewRoll(GameStateStore stateStore, Guid actorUserId, Campaign campaign, RollLogEntry entry)
|
||||
{
|
||||
return CanViewCampaign(stateStore, actorUserId, campaign.Id) &&
|
||||
(entry.Visibility == RollVisibility.Public || entry.RollerUserId == actorUserId || campaign.GmUserId == actorUserId);
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ public sealed class GameCampaignService
|
||||
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<CampaignSummary>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
@@ -37,7 +37,7 @@ public sealed class GameCampaignService
|
||||
|
||||
m_StateStore.CampaignsById[campaign.Id] = campaign;
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
return ServiceResult<CampaignSummary>.Success(ToCampaignSummary(campaign));
|
||||
return ServiceResult<CampaignSummary>.Success(GameDtoMapper.ToCampaignSummary(m_StateStore, campaign));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,27 +45,14 @@ public sealed class GameCampaignService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<IReadOnlyList<CampaignSummary>>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
IEnumerable<Campaign> visibleCampaigns;
|
||||
if (RoleSerializer.HasRole(user.Roles, UserRoles.Admin))
|
||||
{
|
||||
visibleCampaigns = m_StateStore.CampaignsById.Values;
|
||||
}
|
||||
else
|
||||
{
|
||||
var campaignIds = new HashSet<Guid>(m_StateStore.CampaignsById.Values.Where(campaign => campaign.GmUserId == user.Id).Select(campaign => campaign.Id));
|
||||
foreach (var character in m_StateStore.CharactersById.Values.Where(character => character.OwnerUserId == user.Id && character.CampaignId.HasValue))
|
||||
campaignIds.Add(character.CampaignId!.Value);
|
||||
|
||||
visibleCampaigns = campaignIds.Select(campaignId => m_StateStore.CampaignsById[campaignId]);
|
||||
}
|
||||
|
||||
var results = visibleCampaigns
|
||||
var results = m_StateStore.CampaignsById.Values
|
||||
.Where(campaign => GameAuthorization.CanViewCampaign(m_StateStore, user.Id, campaign.Id))
|
||||
.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(ToCampaignSummary)
|
||||
.Select(campaign => GameDtoMapper.ToCampaignSummary(m_StateStore, campaign))
|
||||
.ToArray();
|
||||
|
||||
return ServiceResult<IReadOnlyList<CampaignSummary>>.Success(results);
|
||||
@@ -76,13 +63,13 @@ public sealed class GameCampaignService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<IReadOnlyList<CampaignOption>>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
var options = m_StateStore.CampaignsById.Values
|
||||
.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(campaign => new CampaignOption(campaign.Id, campaign.Name))
|
||||
.Select(GameDtoMapper.ToCampaignOption)
|
||||
.ToArray();
|
||||
|
||||
return ServiceResult<IReadOnlyList<CampaignOption>>.Success(options);
|
||||
@@ -93,12 +80,12 @@ public sealed class GameCampaignService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var context = ResolveContextLocked(sessionToken, campaignId);
|
||||
var context = GameContextResolver.ResolveCampaignContextLocked(m_StateStore, sessionToken, campaignId);
|
||||
if (!context.Succeeded)
|
||||
return ServiceResult<CampaignRoster>.Failure(context.Error!.Code, context.Error.Message);
|
||||
|
||||
var (_, campaign) = context.Value;
|
||||
return ServiceResult<CampaignRoster>.Success(ToCampaignRoster(campaign));
|
||||
return ServiceResult<CampaignRoster>.Success(GameDtoMapper.ToCampaignRoster(m_StateStore, campaign));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,14 +93,14 @@ public sealed class GameCampaignService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!m_StateStore.CampaignsById.TryGetValue(campaignId, out var campaign))
|
||||
return ServiceResult<bool>.Failure("campaign_not_found", "Campaign was not found.");
|
||||
|
||||
if (campaign.GmUserId != user.Id && !RoleSerializer.HasRole(user.Roles, 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.");
|
||||
|
||||
DeleteCampaignLocked(campaignId);
|
||||
@@ -122,75 +109,6 @@ public sealed class GameCampaignService
|
||||
}
|
||||
}
|
||||
|
||||
private ServiceResult<(UserAccount User, Campaign Campaign)> ResolveContextLocked(string sessionToken, Guid campaignId)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<(UserAccount User, Campaign Campaign)>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!m_StateStore.CampaignsById.TryGetValue(campaignId, out var campaign))
|
||||
return ServiceResult<(UserAccount User, Campaign Campaign)>.Failure("campaign_not_found", "Campaign was not found.");
|
||||
|
||||
if (!CanViewCampaignLocked(user.Id, campaign.Id))
|
||||
return ServiceResult<(UserAccount User, Campaign Campaign)>.Failure("forbidden", "You are not a participant in this campaign.");
|
||||
|
||||
return ServiceResult<(UserAccount User, Campaign Campaign)>.Success((user, campaign));
|
||||
}
|
||||
|
||||
private UserAccount? ResolveUserLocked(string sessionToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sessionToken))
|
||||
return null;
|
||||
|
||||
if (!m_StateStore.SessionsByToken.TryGetValue(sessionToken, out var session))
|
||||
return null;
|
||||
|
||||
return m_StateStore.UsersById.GetValueOrDefault(session.UserId);
|
||||
}
|
||||
|
||||
private bool CanViewCampaignLocked(Guid userId, Guid campaignId)
|
||||
{
|
||||
if (m_StateStore.UsersById.TryGetValue(userId, out var user) && RoleSerializer.HasRole(user.Roles, UserRoles.Admin))
|
||||
return true;
|
||||
|
||||
var campaign = m_StateStore.CampaignsById[campaignId];
|
||||
if (campaign.GmUserId == userId)
|
||||
return true;
|
||||
|
||||
return m_StateStore.CharactersById.Values.Any(c => c.CampaignId.HasValue && c.CampaignId.Value == campaignId && c.OwnerUserId == userId);
|
||||
}
|
||||
|
||||
private CampaignSummary ToCampaignSummary(Campaign campaign)
|
||||
{
|
||||
var gm = m_StateStore.UsersById[campaign.GmUserId];
|
||||
var characterCount = m_StateStore.CharactersById.Values.Count(character => character.CampaignId == campaign.Id);
|
||||
return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), new CampaignGmSummary(gm.Id, gm.DisplayName), characterCount);
|
||||
}
|
||||
|
||||
private CampaignRoster ToCampaignRoster(Campaign campaign)
|
||||
{
|
||||
var gm = m_StateStore.UsersById[campaign.GmUserId];
|
||||
var characters = m_StateStore.CharactersById.Values
|
||||
.Where(character => character.CampaignId == campaign.Id)
|
||||
.OrderBy(character => character.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(character => new CharacterSummary(
|
||||
character.Id,
|
||||
character.Name,
|
||||
character.OwnerUserId,
|
||||
character.CampaignId,
|
||||
ResolveOwnerDisplayName(character.OwnerUserId)))
|
||||
.ToArray();
|
||||
|
||||
return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), new CampaignGmSummary(gm.Id, gm.DisplayName), characters);
|
||||
}
|
||||
|
||||
private string ResolveOwnerDisplayName(Guid ownerUserId)
|
||||
{
|
||||
return m_StateStore.UsersById.TryGetValue(ownerUserId, out var user)
|
||||
? user.DisplayName
|
||||
: "Unknown user";
|
||||
}
|
||||
|
||||
private void DeleteCampaignLocked(Guid campaignId)
|
||||
{
|
||||
if (!m_StateStore.CampaignsById.Remove(campaignId))
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class GameCharacterService
|
||||
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<CharacterSummary>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
@@ -34,11 +34,11 @@ public sealed class GameCharacterService
|
||||
};
|
||||
|
||||
m_StateStore.CharactersById[character.Id] = character;
|
||||
AddCharacterStateLocked(character.CampaignId, character.Id);
|
||||
TouchRosterLocked(character.CampaignId);
|
||||
m_StateStore.AddCharacterStateLocked(character.CampaignId, character.Id);
|
||||
m_StateStore.TouchRosterLocked(character.CampaignId);
|
||||
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
return ServiceResult<CharacterSummary>.Success(ToCharacterSummary(character));
|
||||
return ServiceResult<CharacterSummary>.Success(GameDtoMapper.ToCharacterSummary(m_StateStore, character));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ public sealed class GameCharacterService
|
||||
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<CharacterSummary>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
@@ -61,7 +61,7 @@ public sealed class GameCharacterService
|
||||
return ServiceResult<CharacterSummary>.Failure("campaign_not_found", "Campaign was not found.");
|
||||
|
||||
var isOwner = character.OwnerUserId == user.Id;
|
||||
var isAdmin = RoleSerializer.HasRole(user.Roles, 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;
|
||||
@@ -95,16 +95,16 @@ public sealed class GameCharacterService
|
||||
|
||||
if (sourceCampaignId != character.CampaignId)
|
||||
{
|
||||
RemoveCharacterStateLocked(sourceCampaignId, character.Id);
|
||||
AddCharacterStateLocked(character.CampaignId, character.Id);
|
||||
m_StateStore.RemoveCharacterStateLocked(sourceCampaignId, character.Id);
|
||||
m_StateStore.AddCharacterStateLocked(character.CampaignId, character.Id);
|
||||
}
|
||||
|
||||
TouchRosterLocked(sourceCampaignId);
|
||||
m_StateStore.TouchRosterLocked(sourceCampaignId);
|
||||
if (sourceCampaignId != character.CampaignId)
|
||||
TouchRosterLocked(character.CampaignId);
|
||||
m_StateStore.TouchRosterLocked(character.CampaignId);
|
||||
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
return ServiceResult<CharacterSummary>.Success(ToCharacterSummary(character));
|
||||
return ServiceResult<CharacterSummary>.Success(GameDtoMapper.ToCharacterSummary(m_StateStore, character));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ public sealed class GameCharacterService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
@@ -120,7 +120,7 @@ public sealed class GameCharacterService
|
||||
return ServiceResult<bool>.Failure("character_not_found", "Character was not found.");
|
||||
|
||||
var isOwner = character.OwnerUserId == user.Id;
|
||||
var isAdmin = RoleSerializer.HasRole(user.Roles, UserRoles.Admin);
|
||||
var isAdmin = GameAuthorization.HasRole(user, UserRoles.Admin);
|
||||
if (!isOwner && !isAdmin)
|
||||
return ServiceResult<bool>.Failure("forbidden", "Only the owner or admin can delete this character.");
|
||||
|
||||
@@ -134,7 +134,7 @@ public sealed class GameCharacterService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
@@ -154,69 +154,20 @@ public sealed class GameCharacterService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
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(ToCharacterSummary)
|
||||
.Select(character => GameDtoMapper.ToCharacterSummary(m_StateStore, character))
|
||||
.ToArray();
|
||||
|
||||
return ServiceResult<IReadOnlyList<CharacterSummary>>.Success(characters);
|
||||
}
|
||||
}
|
||||
|
||||
private UserAccount? ResolveUserLocked(string sessionToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sessionToken))
|
||||
return null;
|
||||
|
||||
if (!m_StateStore.SessionsByToken.TryGetValue(sessionToken, out var session))
|
||||
return null;
|
||||
|
||||
return m_StateStore.UsersById.GetValueOrDefault(session.UserId);
|
||||
}
|
||||
|
||||
private void AddCharacterStateLocked(Guid? campaignId, Guid characterId)
|
||||
{
|
||||
if (!campaignId.HasValue || !m_StateStore.CampaignsById.ContainsKey(campaignId.Value))
|
||||
return;
|
||||
|
||||
var state = GetOrCreateCampaignStateLocked(campaignId.Value);
|
||||
state.CharacterVersions[characterId] = 1;
|
||||
}
|
||||
|
||||
private void RemoveCharacterStateLocked(Guid? campaignId, Guid characterId)
|
||||
{
|
||||
if (!campaignId.HasValue || !m_StateStore.CampaignStateById.TryGetValue(campaignId.Value, out var state))
|
||||
return;
|
||||
|
||||
state.CharacterVersions.Remove(characterId);
|
||||
}
|
||||
|
||||
private void TouchRosterLocked(Guid? campaignId)
|
||||
{
|
||||
if (!campaignId.HasValue || !m_StateStore.CampaignsById.ContainsKey(campaignId.Value))
|
||||
return;
|
||||
|
||||
var state = GetOrCreateCampaignStateLocked(campaignId.Value);
|
||||
state.TotalVersion += 1;
|
||||
state.RosterVersion += 1;
|
||||
}
|
||||
|
||||
private GameCampaignStateTracker GetOrCreateCampaignStateLocked(Guid campaignId)
|
||||
{
|
||||
if (!m_StateStore.CampaignStateById.TryGetValue(campaignId, out var state))
|
||||
{
|
||||
state = new GameCampaignStateTracker();
|
||||
m_StateStore.CampaignStateById[campaignId] = state;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
private void DeleteCharacterLocked(Guid characterId)
|
||||
{
|
||||
if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character))
|
||||
@@ -238,20 +189,8 @@ public sealed class GameCharacterService
|
||||
foreach (var user in m_StateStore.UsersById.Values.Where(account => account.ActiveCharacterId == characterId))
|
||||
user.ActiveCharacterId = null;
|
||||
|
||||
RemoveCharacterStateLocked(campaignId, characterId);
|
||||
TouchRosterLocked(campaignId);
|
||||
}
|
||||
|
||||
private CharacterSummary ToCharacterSummary(Character character)
|
||||
{
|
||||
return new(character.Id, character.Name, character.OwnerUserId, character.CampaignId, ResolveOwnerDisplayName(character.OwnerUserId));
|
||||
}
|
||||
|
||||
private string ResolveOwnerDisplayName(Guid ownerUserId)
|
||||
{
|
||||
return m_StateStore.UsersById.TryGetValue(ownerUserId, out var user)
|
||||
? user.DisplayName
|
||||
: "Unknown user";
|
||||
m_StateStore.RemoveCharacterStateLocked(campaignId, characterId);
|
||||
m_StateStore.TouchRosterLocked(campaignId);
|
||||
}
|
||||
|
||||
private static string NormalizeUsername(string username)
|
||||
|
||||
48
RpgRoller/Services/GameContextResolver.cs
Normal file
48
RpgRoller/Services/GameContextResolver.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using RpgRoller.Domain;
|
||||
|
||||
namespace RpgRoller.Services;
|
||||
|
||||
public static class GameContextResolver
|
||||
{
|
||||
public static UserAccount? ResolveUserLocked(GameStateStore stateStore, string sessionToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sessionToken))
|
||||
return null;
|
||||
|
||||
if (!stateStore.SessionsByToken.TryGetValue(sessionToken, out var session))
|
||||
return null;
|
||||
|
||||
return stateStore.UsersById.GetValueOrDefault(session.UserId);
|
||||
}
|
||||
|
||||
public static ServiceResult<(UserAccount User, Campaign Campaign)> ResolveCampaignContextLocked(GameStateStore stateStore, string sessionToken, Guid campaignId)
|
||||
{
|
||||
var user = ResolveUserLocked(stateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<(UserAccount User, Campaign Campaign)>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!stateStore.CampaignsById.TryGetValue(campaignId, out var campaign))
|
||||
return ServiceResult<(UserAccount User, Campaign Campaign)>.Failure("campaign_not_found", "Campaign was not found.");
|
||||
|
||||
if (!GameAuthorization.CanViewCampaign(stateStore, user.Id, campaign.Id))
|
||||
return ServiceResult<(UserAccount User, Campaign Campaign)>.Failure("forbidden", "You are not a participant in this campaign.");
|
||||
|
||||
return ServiceResult<(UserAccount User, Campaign Campaign)>.Success((user, campaign));
|
||||
}
|
||||
|
||||
public static bool TryResolveCharacterCampaignLocked(GameStateStore stateStore, Character character, out Campaign campaign, out ServiceError? error)
|
||||
{
|
||||
campaign = default!;
|
||||
if (!character.CampaignId.HasValue ||
|
||||
!stateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var resolvedCampaign) ||
|
||||
resolvedCampaign is null)
|
||||
{
|
||||
error = new ServiceError("character_not_in_campaign", "Character is not linked to a campaign.");
|
||||
return false;
|
||||
}
|
||||
|
||||
campaign = resolvedCampaign;
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
151
RpgRoller/Services/GameDtoMapper.cs
Normal file
151
RpgRoller/Services/GameDtoMapper.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using RpgRoller.Contracts;
|
||||
using RpgRoller.Domain;
|
||||
|
||||
namespace RpgRoller.Services;
|
||||
|
||||
public static class GameDtoMapper
|
||||
{
|
||||
public static UserSummary ToUserSummary(UserAccount user)
|
||||
{
|
||||
return new(user.Id, user.Username, user.DisplayName, RoleSerializer.Parse(user.Roles));
|
||||
}
|
||||
|
||||
public static AdminUserSummary ToAdminUserSummary(UserAccount user)
|
||||
{
|
||||
return new(user.Id, user.Username, user.DisplayName, RoleSerializer.Parse(user.Roles));
|
||||
}
|
||||
|
||||
public static CampaignOption ToCampaignOption(Campaign campaign)
|
||||
{
|
||||
return new(campaign.Id, campaign.Name);
|
||||
}
|
||||
|
||||
public static CampaignSummary ToCampaignSummary(GameStateStore stateStore, Campaign campaign)
|
||||
{
|
||||
var gm = stateStore.UsersById[campaign.GmUserId];
|
||||
var characterCount = stateStore.CharactersById.Values.Count(character => character.CampaignId == campaign.Id);
|
||||
return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), new CampaignGmSummary(gm.Id, gm.DisplayName), characterCount);
|
||||
}
|
||||
|
||||
public static CampaignRoster ToCampaignRoster(GameStateStore stateStore, Campaign campaign)
|
||||
{
|
||||
var gm = stateStore.UsersById[campaign.GmUserId];
|
||||
var characters = stateStore.CharactersById.Values
|
||||
.Where(character => character.CampaignId == campaign.Id)
|
||||
.OrderBy(character => character.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(character => ToCharacterSummary(stateStore, character))
|
||||
.ToArray();
|
||||
|
||||
return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), new CampaignGmSummary(gm.Id, gm.DisplayName), characters);
|
||||
}
|
||||
|
||||
public static CharacterSummary ToCharacterSummary(GameStateStore stateStore, Character character)
|
||||
{
|
||||
return new(character.Id, character.Name, character.OwnerUserId, character.CampaignId, ResolveOwnerDisplayName(stateStore, character.OwnerUserId, "Unknown user"));
|
||||
}
|
||||
|
||||
public static CharacterSheet ToCharacterSheet(GameStateStore stateStore, Guid characterId)
|
||||
{
|
||||
var skillGroups = stateStore.SkillGroupsById.Values
|
||||
.Where(group => group.CharacterId == characterId)
|
||||
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(ToCharacterSheetSkillGroup)
|
||||
.ToArray();
|
||||
var skills = stateStore.SkillsById.Values
|
||||
.Where(skill => skill.CharacterId == characterId)
|
||||
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(ToCharacterSheetSkill)
|
||||
.ToArray();
|
||||
|
||||
return new(characterId, skillGroups, skills);
|
||||
}
|
||||
|
||||
public static SkillGroupSummary ToSkillGroupSummary(SkillGroup skillGroup)
|
||||
{
|
||||
return new(skillGroup.Id, skillGroup.CharacterId, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble, skillGroup.FumbleRange);
|
||||
}
|
||||
|
||||
public static SkillSummary ToSkillSummary(Skill skill)
|
||||
{
|
||||
return new(skill.Id, skill.CharacterId, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble, skill.FumbleRange);
|
||||
}
|
||||
|
||||
public static RollResult ToRollResult(RollLogEntry entry, IReadOnlyList<RollDieResult> dice)
|
||||
{
|
||||
return new(entry.Id, entry.CampaignId, entry.CharacterId, entry.SkillId, entry.RollerUserId, entry.Visibility == RollVisibility.Public ? "public" : "private", entry.Result, entry.Breakdown, dice, entry.TimestampUtc);
|
||||
}
|
||||
|
||||
public static CampaignLogEntry ToCampaignLogEntry(RollLogEntry entry, string characterName, string skillName, string rollerDisplayName, IReadOnlyList<RollDieResult> dice)
|
||||
{
|
||||
return new(
|
||||
entry.Id,
|
||||
entry.CampaignId,
|
||||
entry.CharacterId,
|
||||
characterName,
|
||||
entry.SkillId,
|
||||
skillName,
|
||||
entry.RollerUserId,
|
||||
rollerDisplayName,
|
||||
entry.Visibility == RollVisibility.Public ? "public" : "private",
|
||||
entry.Result,
|
||||
entry.Breakdown,
|
||||
dice,
|
||||
entry.TimestampUtc);
|
||||
}
|
||||
|
||||
public static CampaignLogListEntry ToCampaignLogListEntry(
|
||||
RollLogEntry entry,
|
||||
string characterName,
|
||||
string skillName,
|
||||
string rollerLabel,
|
||||
string visibilityLabel,
|
||||
string visibilityStyle,
|
||||
string summaryText,
|
||||
string[]? eventBadges)
|
||||
{
|
||||
return new(
|
||||
entry.Id,
|
||||
characterName,
|
||||
skillName,
|
||||
rollerLabel,
|
||||
visibilityLabel,
|
||||
visibilityStyle,
|
||||
entry.Result,
|
||||
summaryText,
|
||||
eventBadges,
|
||||
entry.TimestampUtc);
|
||||
}
|
||||
|
||||
public static CampaignRollDetail ToCampaignRollDetail(RollLogEntry entry, RollDieResult[] dice)
|
||||
{
|
||||
return new(entry.Id, entry.Breakdown, dice);
|
||||
}
|
||||
|
||||
public static CampaignStateSnapshot ToCampaignStateSnapshot(GameStateStore stateStore, Guid campaignId)
|
||||
{
|
||||
var state = stateStore.GetOrCreateCampaignStateLocked(campaignId);
|
||||
var characterVersions = state.CharacterVersions
|
||||
.OrderBy(version => version.Key)
|
||||
.Select(version => new CharacterStateVersion(version.Key, version.Value))
|
||||
.ToArray();
|
||||
|
||||
return new CampaignStateSnapshot(campaignId, state.TotalVersion, state.RosterVersion, state.LogVersion, characterVersions);
|
||||
}
|
||||
|
||||
public static string ResolveOwnerDisplayName(GameStateStore stateStore, Guid ownerUserId, string fallback)
|
||||
{
|
||||
return stateStore.UsersById.TryGetValue(ownerUserId, out var user) && !string.IsNullOrWhiteSpace(user.DisplayName)
|
||||
? user.DisplayName
|
||||
: fallback;
|
||||
}
|
||||
|
||||
private static CharacterSheetSkillGroup ToCharacterSheetSkillGroup(SkillGroup skillGroup)
|
||||
{
|
||||
return new(skillGroup.Id, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble, skillGroup.FumbleRange);
|
||||
}
|
||||
|
||||
private static CharacterSheetSkill ToCharacterSheetSkill(Skill skill)
|
||||
{
|
||||
return new(skill.Id, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble, skill.FumbleRange);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ public sealed class GameRollService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<RollResult>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
@@ -25,10 +25,10 @@ public sealed class GameRollService
|
||||
return ServiceResult<RollResult>.Failure("skill_not_found", "Skill was not found.");
|
||||
|
||||
var character = m_StateStore.CharactersById[skill.CharacterId];
|
||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||
if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError))
|
||||
return ServiceResult<RollResult>.Failure(campaignError!.Code, campaignError.Message);
|
||||
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign))
|
||||
return ServiceResult<RollResult>.Failure("forbidden", "Only the owner or GM can roll this skill.");
|
||||
|
||||
var parsedExpression = DiceRules.ParseExpression(campaign.Ruleset, skill.DiceRollDefinition);
|
||||
@@ -48,17 +48,17 @@ public sealed class GameRollService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<RollResult>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character))
|
||||
return ServiceResult<RollResult>.Failure("character_not_found", "Character was not found.");
|
||||
|
||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||
if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError))
|
||||
return ServiceResult<RollResult>.Failure(campaignError!.Code, campaignError.Message);
|
||||
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign))
|
||||
return ServiceResult<RollResult>.Failure("forbidden", "Only the owner or GM can make a custom roll for this character.");
|
||||
|
||||
var parsedExpression = DiceRules.ParseExpression(campaign.Ruleset, expression);
|
||||
@@ -79,7 +79,7 @@ public sealed class GameRollService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var context = ResolveContextLocked(sessionToken, campaignId);
|
||||
var context = GameContextResolver.ResolveCampaignContextLocked(m_StateStore, sessionToken, campaignId);
|
||||
if (!context.Succeeded)
|
||||
return ServiceResult<IReadOnlyList<CampaignLogEntry>>.Failure(context.Error!.Code, context.Error.Message);
|
||||
|
||||
@@ -97,7 +97,7 @@ public sealed class GameRollService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var context = ResolveContextLocked(sessionToken, campaignId);
|
||||
var context = GameContextResolver.ResolveCampaignContextLocked(m_StateStore, sessionToken, campaignId);
|
||||
if (!context.Succeeded)
|
||||
return ServiceResult<CampaignLogPage>.Failure(context.Error!.Code, context.Error.Message);
|
||||
|
||||
@@ -137,7 +137,7 @@ public sealed class GameRollService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<CampaignRollDetail>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
@@ -145,10 +145,10 @@ public sealed class GameRollService
|
||||
if (entry is null)
|
||||
return ServiceResult<CampaignRollDetail>.Failure("roll_not_found", "Roll was not found.");
|
||||
|
||||
if (!m_StateStore.CampaignsById.TryGetValue(entry.CampaignId, out var campaign) || !CanViewRollLocked(user, campaign, entry))
|
||||
if (!m_StateStore.CampaignsById.TryGetValue(entry.CampaignId, out var campaign) || !GameAuthorization.CanViewRoll(m_StateStore, user.Id, campaign, entry))
|
||||
return ServiceResult<CampaignRollDetail>.Failure("roll_not_found", "Roll was not found.");
|
||||
|
||||
return ServiceResult<CampaignRollDetail>.Success(new CampaignRollDetail(entry.Id, entry.Breakdown, DeserializeDice(entry.Dice).ToArray()));
|
||||
return ServiceResult<CampaignRollDetail>.Success(GameDtoMapper.ToCampaignRollDetail(entry, DeserializeDice(entry.Dice).ToArray()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,11 +156,11 @@ public sealed class GameRollService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var context = ResolveContextLocked(sessionToken, campaignId);
|
||||
var context = GameContextResolver.ResolveCampaignContextLocked(m_StateStore, sessionToken, campaignId);
|
||||
if (!context.Succeeded)
|
||||
return ServiceResult<CampaignStateSnapshot>.Failure(context.Error!.Code, context.Error.Message);
|
||||
|
||||
return ServiceResult<CampaignStateSnapshot>.Success(ToCampaignStateSnapshot(context.Value!.Campaign));
|
||||
return ServiceResult<CampaignStateSnapshot>.Success(GameDtoMapper.ToCampaignStateSnapshot(m_StateStore, context.Value!.Campaign.Id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,10 +422,10 @@ public sealed class GameRollService
|
||||
};
|
||||
|
||||
m_StateStore.RollLog.Add(entry);
|
||||
TouchLogLocked(campaign.Id);
|
||||
m_StateStore.TouchLogLocked(campaign.Id);
|
||||
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
return ServiceResult<RollResult>.Success(ToRollResult(entry, roll.Dice));
|
||||
return ServiceResult<RollResult>.Success(GameDtoMapper.ToRollResult(entry, roll.Dice));
|
||||
}
|
||||
|
||||
private static string FormatLoggedBreakdown(Guid skillId, string canonicalExpression, string breakdown)
|
||||
@@ -435,32 +435,6 @@ public sealed class GameRollService
|
||||
: breakdown;
|
||||
}
|
||||
|
||||
private ServiceResult<(UserAccount User, Campaign Campaign)> ResolveContextLocked(string sessionToken, Guid campaignId)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<(UserAccount User, Campaign Campaign)>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!m_StateStore.CampaignsById.TryGetValue(campaignId, out var campaign))
|
||||
return ServiceResult<(UserAccount User, Campaign Campaign)>.Failure("campaign_not_found", "Campaign was not found.");
|
||||
|
||||
if (!CanViewCampaignLocked(user.Id, campaign.Id))
|
||||
return ServiceResult<(UserAccount User, Campaign Campaign)>.Failure("forbidden", "You are not a participant in this campaign.");
|
||||
|
||||
return ServiceResult<(UserAccount User, Campaign Campaign)>.Success((user, campaign));
|
||||
}
|
||||
|
||||
private CampaignStateSnapshot ToCampaignStateSnapshot(Campaign campaign)
|
||||
{
|
||||
var state = GetOrCreateCampaignStateLocked(campaign.Id);
|
||||
var characterVersions = state.CharacterVersions
|
||||
.OrderBy(version => version.Key)
|
||||
.Select(version => new CharacterStateVersion(version.Key, version.Value))
|
||||
.ToArray();
|
||||
|
||||
return new CampaignStateSnapshot(campaign.Id, state.TotalVersion, state.RosterVersion, state.LogVersion, characterVersions);
|
||||
}
|
||||
|
||||
private IEnumerable<RollLogEntry> GetVisibleCampaignLogEntriesLocked(UserAccount user, Campaign campaign)
|
||||
{
|
||||
return m_StateStore.RollLog
|
||||
@@ -470,32 +444,14 @@ public sealed class GameRollService
|
||||
.ThenBy(r => r.Id);
|
||||
}
|
||||
|
||||
private static RollResult ToRollResult(RollLogEntry entry, IReadOnlyList<RollDieResult> dice)
|
||||
{
|
||||
return new(entry.Id, entry.CampaignId, entry.CharacterId, entry.SkillId, entry.RollerUserId, entry.Visibility == RollVisibility.Public ? "public" : "private", entry.Result, entry.Breakdown, dice, entry.TimestampUtc);
|
||||
}
|
||||
|
||||
private CampaignLogEntry ToLogEntry(RollLogEntry entry)
|
||||
{
|
||||
var dice = DeserializeDice(entry.Dice);
|
||||
var characterName = m_StateStore.CharactersById.TryGetValue(entry.CharacterId, out var character) ? character.Name : "Unknown character";
|
||||
var skillName = ResolveLoggedSkillName(entry);
|
||||
var rollerDisplayName = ResolveOwnerDisplayName(entry.RollerUserId);
|
||||
var rollerDisplayName = GameDtoMapper.ResolveOwnerDisplayName(m_StateStore, entry.RollerUserId, "Unknown owner");
|
||||
|
||||
return new(
|
||||
entry.Id,
|
||||
entry.CampaignId,
|
||||
entry.CharacterId,
|
||||
characterName,
|
||||
entry.SkillId,
|
||||
skillName,
|
||||
entry.RollerUserId,
|
||||
rollerDisplayName,
|
||||
entry.Visibility == RollVisibility.Public ? "public" : "private",
|
||||
entry.Result,
|
||||
entry.Breakdown,
|
||||
dice,
|
||||
entry.TimestampUtc);
|
||||
return GameDtoMapper.ToCampaignLogEntry(entry, characterName, skillName, rollerDisplayName, dice);
|
||||
}
|
||||
|
||||
private CampaignLogListEntry ToLogListEntry(UserAccount user, Campaign campaign, RollLogEntry entry)
|
||||
@@ -506,17 +462,15 @@ public sealed class GameRollService
|
||||
var loggedExpression = ResolveLoggedExpression(entry);
|
||||
var eventBadges = BuildCompactLogEventBadges(campaign, loggedExpression, dice);
|
||||
|
||||
return new(
|
||||
entry.Id,
|
||||
return GameDtoMapper.ToCampaignLogListEntry(
|
||||
entry,
|
||||
characterName,
|
||||
skillName,
|
||||
ResolveLogRollerLabel(user, campaign, entry),
|
||||
ResolveLogVisibilityLabel(user, campaign, entry),
|
||||
ResolveLogVisibilityStyle(user, campaign, entry),
|
||||
entry.Result,
|
||||
BuildCompactLogSummary(dice),
|
||||
eventBadges,
|
||||
entry.TimestampUtc);
|
||||
eventBadges);
|
||||
}
|
||||
|
||||
private static string SerializeDice(IReadOnlyList<RollDieResult> dice)
|
||||
@@ -648,12 +602,6 @@ public sealed class GameRollService
|
||||
parsedExpression.Value.Sides == 20;
|
||||
}
|
||||
|
||||
private bool CanViewRollLocked(UserAccount user, Campaign campaign, RollLogEntry entry)
|
||||
{
|
||||
return CanViewCampaignLocked(user.Id, campaign.Id) &&
|
||||
(entry.Visibility == RollVisibility.Public || entry.RollerUserId == user.Id || campaign.GmUserId == user.Id);
|
||||
}
|
||||
|
||||
private string ResolveLogRollerLabel(UserAccount user, Campaign campaign, RollLogEntry entry)
|
||||
{
|
||||
if (entry.RollerUserId == user.Id)
|
||||
@@ -662,7 +610,7 @@ public sealed class GameRollService
|
||||
if (entry.RollerUserId == campaign.GmUserId)
|
||||
return "GM";
|
||||
|
||||
return ResolveOwnerDisplayName(entry.RollerUserId);
|
||||
return GameDtoMapper.ResolveOwnerDisplayName(m_StateStore, entry.RollerUserId, "Unknown owner");
|
||||
}
|
||||
|
||||
private static string ResolveLogVisibilityLabel(UserAccount user, Campaign campaign, RollLogEntry entry)
|
||||
@@ -687,13 +635,6 @@ public sealed class GameRollService
|
||||
return campaign.GmUserId == user.Id ? "private-gm" : "private-generic";
|
||||
}
|
||||
|
||||
private string ResolveOwnerDisplayName(Guid ownerUserId)
|
||||
{
|
||||
return m_StateStore.UsersById.TryGetValue(ownerUserId, out var owner) && !string.IsNullOrWhiteSpace(owner.DisplayName)
|
||||
? owner.DisplayName
|
||||
: "Unknown owner";
|
||||
}
|
||||
|
||||
private static IReadOnlyList<RollDieResult> DeserializeDice(string serializedDice)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(serializedDice))
|
||||
@@ -709,69 +650,6 @@ public sealed class GameRollService
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanEditCharacterLocked(Guid actorUserId, Character character, Campaign campaign)
|
||||
{
|
||||
return character.OwnerUserId == actorUserId || campaign.GmUserId == actorUserId;
|
||||
}
|
||||
|
||||
private bool CanViewCampaignLocked(Guid userId, Guid campaignId)
|
||||
{
|
||||
if (m_StateStore.UsersById.TryGetValue(userId, out var user) && RoleSerializer.HasRole(user.Roles, UserRoles.Admin))
|
||||
return true;
|
||||
|
||||
var campaign = m_StateStore.CampaignsById[campaignId];
|
||||
if (campaign.GmUserId == userId)
|
||||
return true;
|
||||
|
||||
return m_StateStore.CharactersById.Values.Any(c => c.CampaignId.HasValue && c.CampaignId.Value == campaignId && c.OwnerUserId == userId);
|
||||
}
|
||||
|
||||
private UserAccount? ResolveUserLocked(string sessionToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sessionToken))
|
||||
return null;
|
||||
|
||||
if (!m_StateStore.SessionsByToken.TryGetValue(sessionToken, out var session))
|
||||
return null;
|
||||
|
||||
return m_StateStore.UsersById.GetValueOrDefault(session.UserId);
|
||||
}
|
||||
|
||||
private bool TryResolveCharacterCampaignLocked(Character character, out Campaign campaign, out ServiceError? error)
|
||||
{
|
||||
campaign = default!;
|
||||
if (!character.CampaignId.HasValue || !m_StateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var resolvedCampaign) || resolvedCampaign is null)
|
||||
{
|
||||
error = new ServiceError("character_not_in_campaign", "Character is not linked to a campaign.");
|
||||
return false;
|
||||
}
|
||||
|
||||
campaign = resolvedCampaign;
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void TouchLogLocked(Guid? campaignId)
|
||||
{
|
||||
if (!campaignId.HasValue || !m_StateStore.CampaignsById.ContainsKey(campaignId.Value))
|
||||
return;
|
||||
|
||||
var state = GetOrCreateCampaignStateLocked(campaignId.Value);
|
||||
state.TotalVersion += 1;
|
||||
state.LogVersion += 1;
|
||||
}
|
||||
|
||||
private GameCampaignStateTracker GetOrCreateCampaignStateLocked(Guid campaignId)
|
||||
{
|
||||
if (!m_StateStore.CampaignStateById.TryGetValue(campaignId, out var state))
|
||||
{
|
||||
state = new GameCampaignStateTracker();
|
||||
m_StateStore.CampaignStateById[campaignId] = state;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
private static int NormalizeCampaignLogPageSize(int? limit)
|
||||
{
|
||||
if (!limit.HasValue)
|
||||
|
||||
@@ -11,7 +11,6 @@ public sealed class GameService : IGameService
|
||||
public GameService(IDbContextFactory<RpgRollerDbContext> dbContextFactory, IPasswordHasher<UserAccount> passwordHasher, IDiceRoller diceRoller)
|
||||
{
|
||||
m_StateStore = new();
|
||||
m_Gate = m_StateStore.Gate;
|
||||
m_PersistenceService = new(dbContextFactory, m_StateStore);
|
||||
m_AuthService = new(m_StateStore, passwordHasher, m_PersistenceService);
|
||||
m_CampaignService = new(m_StateStore, m_PersistenceService);
|
||||
@@ -19,7 +18,9 @@ public sealed class GameService : IGameService
|
||||
m_RollService = new(m_StateStore, m_PersistenceService, diceRoller);
|
||||
m_SkillService = new(m_StateStore, m_PersistenceService);
|
||||
m_UserAdministrationService = new(m_StateStore, m_PersistenceService);
|
||||
LoadStateFromDatabase();
|
||||
m_PersistenceService.LoadStateFromDatabase();
|
||||
lock (m_StateStore.Gate)
|
||||
m_StateStore.RebuildCampaignStateLocked();
|
||||
}
|
||||
|
||||
public IReadOnlyList<RulesetDefinition> GetRulesets()
|
||||
@@ -187,48 +188,9 @@ public sealed class GameService : IGameService
|
||||
return m_RollService.GetCampaignStateSnapshot(sessionToken, campaignId);
|
||||
}
|
||||
|
||||
private GameCampaignStateTracker GetOrCreateCampaignStateLocked(Guid campaignId)
|
||||
{
|
||||
if (!m_StateStore.CampaignStateById.TryGetValue(campaignId, out var state))
|
||||
{
|
||||
state = new GameCampaignStateTracker();
|
||||
m_StateStore.CampaignStateById[campaignId] = state;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
private void AddCharacterStateLocked(Guid? campaignId, Guid characterId)
|
||||
{
|
||||
if (!campaignId.HasValue || !m_StateStore.CampaignsById.ContainsKey(campaignId.Value))
|
||||
return;
|
||||
|
||||
var state = GetOrCreateCampaignStateLocked(campaignId.Value);
|
||||
state.CharacterVersions[characterId] = 1;
|
||||
}
|
||||
|
||||
private void RebuildCampaignStateLocked()
|
||||
{
|
||||
m_StateStore.CampaignStateById.Clear();
|
||||
|
||||
foreach (var campaignId in m_StateStore.CampaignsById.Keys)
|
||||
m_StateStore.CampaignStateById[campaignId] = new GameCampaignStateTracker();
|
||||
|
||||
foreach (var character in m_StateStore.CharactersById.Values.Where(character => character.CampaignId.HasValue))
|
||||
AddCharacterStateLocked(character.CampaignId, character.Id);
|
||||
}
|
||||
|
||||
private void LoadStateFromDatabase()
|
||||
{
|
||||
m_PersistenceService.LoadStateFromDatabase();
|
||||
lock (m_Gate)
|
||||
RebuildCampaignStateLocked();
|
||||
}
|
||||
|
||||
private readonly GameCampaignService m_CampaignService;
|
||||
private readonly GameCharacterService m_CharacterService;
|
||||
private readonly GameAuthService m_AuthService;
|
||||
private readonly object m_Gate;
|
||||
private readonly GamePersistenceService m_PersistenceService;
|
||||
private readonly GameRollService m_RollService;
|
||||
private readonly GameSkillService m_SkillService;
|
||||
|
||||
@@ -18,17 +18,17 @@ public sealed class GameSkillService
|
||||
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<SkillGroupSummary>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character))
|
||||
return ServiceResult<SkillGroupSummary>.Failure("character_not_found", "Character was not found.");
|
||||
|
||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||
if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError))
|
||||
return ServiceResult<SkillGroupSummary>.Failure(campaignError!.Code, campaignError.Message);
|
||||
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign))
|
||||
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
||||
|
||||
var prototypeValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||
@@ -47,10 +47,10 @@ public sealed class GameSkillService
|
||||
};
|
||||
|
||||
m_StateStore.SkillGroupsById[group.Id] = group;
|
||||
TouchCharacterLocked(campaign.Id, character.Id);
|
||||
m_StateStore.TouchCharacterLocked(campaign.Id, character.Id);
|
||||
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
return ServiceResult<SkillGroupSummary>.Success(ToSkillGroupSummary(group));
|
||||
return ServiceResult<SkillGroupSummary>.Success(GameDtoMapper.ToSkillGroupSummary(group));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ public sealed class GameSkillService
|
||||
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<SkillGroupSummary>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
@@ -69,10 +69,10 @@ public sealed class GameSkillService
|
||||
return ServiceResult<SkillGroupSummary>.Failure("skill_group_not_found", "Skill group was not found.");
|
||||
|
||||
var character = m_StateStore.CharactersById[group.CharacterId];
|
||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||
if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError))
|
||||
return ServiceResult<SkillGroupSummary>.Failure(campaignError!.Code, campaignError.Message);
|
||||
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign))
|
||||
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
||||
|
||||
var prototypeValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||
@@ -84,10 +84,10 @@ public sealed class GameSkillService
|
||||
group.WildDice = prototypeValidation.Value.WildDice;
|
||||
group.AllowFumble = prototypeValidation.Value.AllowFumble;
|
||||
group.FumbleRange = prototypeValidation.Value.FumbleRange;
|
||||
TouchCharacterLocked(campaign.Id, character.Id);
|
||||
m_StateStore.TouchCharacterLocked(campaign.Id, character.Id);
|
||||
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
return ServiceResult<SkillGroupSummary>.Success(ToSkillGroupSummary(group));
|
||||
return ServiceResult<SkillGroupSummary>.Success(GameDtoMapper.ToSkillGroupSummary(group));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public sealed class GameSkillService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
@@ -103,17 +103,17 @@ public sealed class GameSkillService
|
||||
return ServiceResult<bool>.Failure("skill_group_not_found", "Skill group was not found.");
|
||||
|
||||
var character = m_StateStore.CharactersById[group.CharacterId];
|
||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||
if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError))
|
||||
return ServiceResult<bool>.Failure(campaignError!.Code, campaignError.Message);
|
||||
|
||||
if (!CanEditCharacterLocked(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.");
|
||||
|
||||
foreach (var skill in m_StateStore.SkillsById.Values.Where(skill => skill.SkillGroupId == group.Id))
|
||||
skill.SkillGroupId = null;
|
||||
|
||||
m_StateStore.SkillGroupsById.Remove(group.Id);
|
||||
TouchCharacterLocked(campaign.Id, character.Id);
|
||||
m_StateStore.TouchCharacterLocked(campaign.Id, character.Id);
|
||||
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
return ServiceResult<bool>.Success(true);
|
||||
@@ -127,17 +127,17 @@ public sealed class GameSkillService
|
||||
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<SkillSummary>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character))
|
||||
return ServiceResult<SkillSummary>.Failure("character_not_found", "Character was not found.");
|
||||
|
||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||
if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError))
|
||||
return ServiceResult<SkillSummary>.Failure(campaignError!.Code, campaignError.Message);
|
||||
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign))
|
||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||
|
||||
var skillValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||
@@ -161,10 +161,10 @@ public sealed class GameSkillService
|
||||
};
|
||||
|
||||
m_StateStore.SkillsById[skill.Id] = skill;
|
||||
TouchCharacterLocked(campaign.Id, character.Id);
|
||||
m_StateStore.TouchCharacterLocked(campaign.Id, character.Id);
|
||||
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
return ServiceResult<SkillSummary>.Success(ToSkillSummary(skill));
|
||||
return ServiceResult<SkillSummary>.Success(GameDtoMapper.ToSkillSummary(skill));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ public sealed class GameSkillService
|
||||
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<SkillSummary>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
@@ -183,10 +183,10 @@ public sealed class GameSkillService
|
||||
return ServiceResult<SkillSummary>.Failure("skill_not_found", "Skill was not found.");
|
||||
|
||||
var character = m_StateStore.CharactersById[skill.CharacterId];
|
||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||
if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError))
|
||||
return ServiceResult<SkillSummary>.Failure(campaignError!.Code, campaignError.Message);
|
||||
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign))
|
||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||
|
||||
var skillValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||
@@ -203,10 +203,10 @@ public sealed class GameSkillService
|
||||
skill.AllowFumble = skillValidation.Value.AllowFumble;
|
||||
skill.FumbleRange = skillValidation.Value.FumbleRange;
|
||||
skill.SkillGroupId = resolvedSkillGroupId.Value;
|
||||
TouchCharacterLocked(campaign.Id, character.Id);
|
||||
m_StateStore.TouchCharacterLocked(campaign.Id, character.Id);
|
||||
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
return ServiceResult<SkillSummary>.Success(ToSkillSummary(skill));
|
||||
return ServiceResult<SkillSummary>.Success(GameDtoMapper.ToSkillSummary(skill));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +214,7 @@ public sealed class GameSkillService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
@@ -222,14 +222,14 @@ public sealed class GameSkillService
|
||||
return ServiceResult<bool>.Failure("skill_not_found", "Skill was not found.");
|
||||
|
||||
var character = m_StateStore.CharactersById[skill.CharacterId];
|
||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||
if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError))
|
||||
return ServiceResult<bool>.Failure(campaignError!.Code, campaignError.Message);
|
||||
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign))
|
||||
return ServiceResult<bool>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||
|
||||
m_StateStore.SkillsById.Remove(skill.Id);
|
||||
TouchCharacterLocked(campaign.Id, character.Id);
|
||||
m_StateStore.TouchCharacterLocked(campaign.Id, character.Id);
|
||||
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
return ServiceResult<bool>.Success(true);
|
||||
@@ -240,65 +240,23 @@ public sealed class GameSkillService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<CharacterSheet>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character))
|
||||
return ServiceResult<CharacterSheet>.Failure("character_not_found", "Character was not found.");
|
||||
|
||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||
if (!GameContextResolver.TryResolveCharacterCampaignLocked(m_StateStore, character, out var campaign, out var campaignError))
|
||||
return ServiceResult<CharacterSheet>.Failure(campaignError!.Code, campaignError.Message);
|
||||
|
||||
if (!CanViewCampaignLocked(user.Id, campaign.Id))
|
||||
if (!GameAuthorization.CanViewCampaign(m_StateStore, user.Id, campaign.Id))
|
||||
return ServiceResult<CharacterSheet>.Failure("forbidden", "You are not a participant in this campaign.");
|
||||
|
||||
return ServiceResult<CharacterSheet>.Success(ToCharacterSheet(character.Id));
|
||||
return ServiceResult<CharacterSheet>.Success(GameDtoMapper.ToCharacterSheet(m_StateStore, character.Id));
|
||||
}
|
||||
}
|
||||
|
||||
private UserAccount? ResolveUserLocked(string sessionToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sessionToken))
|
||||
return null;
|
||||
|
||||
if (!m_StateStore.SessionsByToken.TryGetValue(sessionToken, out var session))
|
||||
return null;
|
||||
|
||||
return m_StateStore.UsersById.GetValueOrDefault(session.UserId);
|
||||
}
|
||||
|
||||
private bool TryResolveCharacterCampaignLocked(Character character, out Campaign campaign, out ServiceError? error)
|
||||
{
|
||||
campaign = default!;
|
||||
if (!character.CampaignId.HasValue || !m_StateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var resolvedCampaign) || resolvedCampaign is null)
|
||||
{
|
||||
error = new ServiceError("character_not_in_campaign", "Character is not linked to a campaign.");
|
||||
return false;
|
||||
}
|
||||
|
||||
campaign = resolvedCampaign;
|
||||
error = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool CanEditCharacterLocked(Guid actorUserId, Character character, Campaign campaign)
|
||||
{
|
||||
return character.OwnerUserId == actorUserId || campaign.GmUserId == actorUserId;
|
||||
}
|
||||
|
||||
private bool CanViewCampaignLocked(Guid userId, Guid campaignId)
|
||||
{
|
||||
if (m_StateStore.UsersById.TryGetValue(userId, out var user) && RoleSerializer.HasRole(user.Roles, UserRoles.Admin))
|
||||
return true;
|
||||
|
||||
var campaign = m_StateStore.CampaignsById[campaignId];
|
||||
if (campaign.GmUserId == userId)
|
||||
return true;
|
||||
|
||||
return m_StateStore.CharactersById.Values.Any(c => c.CampaignId.HasValue && c.CampaignId.Value == campaignId && c.OwnerUserId == userId);
|
||||
}
|
||||
|
||||
private ServiceResult<Guid?> ResolveSkillGroupForSkillChangeLocked(Guid? requestedSkillGroupId, Guid characterId)
|
||||
{
|
||||
if (!requestedSkillGroupId.HasValue)
|
||||
@@ -313,63 +271,6 @@ public sealed class GameSkillService
|
||||
return ServiceResult<Guid?>.Success(skillGroup.Id);
|
||||
}
|
||||
|
||||
private CharacterSheet ToCharacterSheet(Guid characterId)
|
||||
{
|
||||
var skillGroups = m_StateStore.SkillGroupsById.Values
|
||||
.Where(group => group.CharacterId == characterId)
|
||||
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(ToCharacterSheetSkillGroup)
|
||||
.ToArray();
|
||||
var skills = m_StateStore.SkillsById.Values
|
||||
.Where(skill => skill.CharacterId == characterId)
|
||||
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(ToCharacterSheetSkill)
|
||||
.ToArray();
|
||||
|
||||
return new(characterId, skillGroups, skills);
|
||||
}
|
||||
|
||||
private static CharacterSheetSkillGroup ToCharacterSheetSkillGroup(SkillGroup skillGroup)
|
||||
{
|
||||
return new(skillGroup.Id, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble, skillGroup.FumbleRange);
|
||||
}
|
||||
|
||||
private static CharacterSheetSkill ToCharacterSheetSkill(Skill skill)
|
||||
{
|
||||
return new(skill.Id, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble, skill.FumbleRange);
|
||||
}
|
||||
|
||||
private static SkillGroupSummary ToSkillGroupSummary(SkillGroup skillGroup)
|
||||
{
|
||||
return new(skillGroup.Id, skillGroup.CharacterId, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble, skillGroup.FumbleRange);
|
||||
}
|
||||
|
||||
private static SkillSummary ToSkillSummary(Skill skill)
|
||||
{
|
||||
return new(skill.Id, skill.CharacterId, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble, skill.FumbleRange);
|
||||
}
|
||||
|
||||
private void TouchCharacterLocked(Guid? campaignId, Guid characterId)
|
||||
{
|
||||
if (!campaignId.HasValue || !m_StateStore.CampaignsById.ContainsKey(campaignId.Value))
|
||||
return;
|
||||
|
||||
var state = GetOrCreateCampaignStateLocked(campaignId.Value);
|
||||
state.TotalVersion += 1;
|
||||
state.CharacterVersions[characterId] = state.CharacterVersions.GetValueOrDefault(characterId, 1) + 1;
|
||||
}
|
||||
|
||||
private GameCampaignStateTracker GetOrCreateCampaignStateLocked(Guid campaignId)
|
||||
{
|
||||
if (!m_StateStore.CampaignStateById.TryGetValue(campaignId, out var state))
|
||||
{
|
||||
state = new GameCampaignStateTracker();
|
||||
m_StateStore.CampaignStateById[campaignId] = state;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
private readonly GamePersistenceService m_PersistenceService;
|
||||
private readonly GameStateStore m_StateStore;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,75 @@ public sealed class GameStateStore
|
||||
public Dictionary<Guid, Skill> SkillsById { get; } = [];
|
||||
public Dictionary<string, Guid> UserIdsByUsername { get; } = new(StringComparer.Ordinal);
|
||||
public Dictionary<Guid, UserAccount> UsersById { get; } = [];
|
||||
|
||||
public GameCampaignStateTracker GetOrCreateCampaignStateLocked(Guid campaignId)
|
||||
{
|
||||
if (!CampaignStateById.TryGetValue(campaignId, out var state))
|
||||
{
|
||||
state = new GameCampaignStateTracker();
|
||||
CampaignStateById[campaignId] = state;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
public void RebuildCampaignStateLocked()
|
||||
{
|
||||
CampaignStateById.Clear();
|
||||
|
||||
foreach (var campaignId in CampaignsById.Keys)
|
||||
CampaignStateById[campaignId] = new GameCampaignStateTracker();
|
||||
|
||||
foreach (var character in CharactersById.Values.Where(character => character.CampaignId.HasValue))
|
||||
AddCharacterStateLocked(character.CampaignId, character.Id);
|
||||
}
|
||||
|
||||
public void AddCharacterStateLocked(Guid? campaignId, Guid characterId)
|
||||
{
|
||||
if (!campaignId.HasValue || !CampaignsById.ContainsKey(campaignId.Value))
|
||||
return;
|
||||
|
||||
var state = GetOrCreateCampaignStateLocked(campaignId.Value);
|
||||
state.CharacterVersions[characterId] = 1;
|
||||
}
|
||||
|
||||
public void RemoveCharacterStateLocked(Guid? campaignId, Guid characterId)
|
||||
{
|
||||
if (!campaignId.HasValue || !CampaignStateById.TryGetValue(campaignId.Value, out var state))
|
||||
return;
|
||||
|
||||
state.CharacterVersions.Remove(characterId);
|
||||
}
|
||||
|
||||
public void TouchRosterLocked(Guid? campaignId)
|
||||
{
|
||||
if (!campaignId.HasValue || !CampaignsById.ContainsKey(campaignId.Value))
|
||||
return;
|
||||
|
||||
var state = GetOrCreateCampaignStateLocked(campaignId.Value);
|
||||
state.TotalVersion += 1;
|
||||
state.RosterVersion += 1;
|
||||
}
|
||||
|
||||
public void TouchCharacterLocked(Guid? campaignId, Guid characterId)
|
||||
{
|
||||
if (!campaignId.HasValue || !CampaignsById.ContainsKey(campaignId.Value))
|
||||
return;
|
||||
|
||||
var state = GetOrCreateCampaignStateLocked(campaignId.Value);
|
||||
state.TotalVersion += 1;
|
||||
state.CharacterVersions[characterId] = state.CharacterVersions.GetValueOrDefault(characterId, 1) + 1;
|
||||
}
|
||||
|
||||
public void TouchLogLocked(Guid? campaignId)
|
||||
{
|
||||
if (!campaignId.HasValue || !CampaignsById.ContainsKey(campaignId.Value))
|
||||
return;
|
||||
|
||||
var state = GetOrCreateCampaignStateLocked(campaignId.Value);
|
||||
state.TotalVersion += 1;
|
||||
state.LogVersion += 1;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GameCampaignStateTracker
|
||||
|
||||
@@ -15,7 +15,7 @@ public sealed class GameUserAdministrationService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<IReadOnlyList<string>>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
@@ -32,16 +32,16 @@ public sealed class GameUserAdministrationService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<IReadOnlyList<AdminUserSummary>>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!UserHasRoleLocked(user, UserRoles.Admin))
|
||||
if (!GameAuthorization.HasRole(user, UserRoles.Admin))
|
||||
return ServiceResult<IReadOnlyList<AdminUserSummary>>.Failure("forbidden", "Admin role is required.");
|
||||
|
||||
var users = m_StateStore.UsersById.Values
|
||||
.OrderBy(account => account.Username, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(ToAdminUserSummary)
|
||||
.Select(GameDtoMapper.ToAdminUserSummary)
|
||||
.ToArray();
|
||||
|
||||
return ServiceResult<IReadOnlyList<AdminUserSummary>>.Success(users);
|
||||
@@ -52,11 +52,11 @@ public sealed class GameUserAdministrationService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<AdminUserSummary>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!UserHasRoleLocked(user, UserRoles.Admin))
|
||||
if (!GameAuthorization.HasRole(user, UserRoles.Admin))
|
||||
return ServiceResult<AdminUserSummary>.Failure("forbidden", "Admin role is required.");
|
||||
|
||||
if (!m_StateStore.UsersById.TryGetValue(userId, out var targetUser))
|
||||
@@ -71,7 +71,7 @@ public sealed class GameUserAdministrationService
|
||||
|
||||
targetUser.Roles = RoleSerializer.Serialize(normalizedRoles);
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
return ServiceResult<AdminUserSummary>.Success(ToAdminUserSummary(targetUser));
|
||||
return ServiceResult<AdminUserSummary>.Success(GameDtoMapper.ToAdminUserSummary(targetUser));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,11 +79,11 @@ public sealed class GameUserAdministrationService
|
||||
{
|
||||
lock (m_StateStore.Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
var user = GameContextResolver.ResolveUserLocked(m_StateStore, sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!UserHasRoleLocked(user, UserRoles.Admin))
|
||||
if (!GameAuthorization.HasRole(user, UserRoles.Admin))
|
||||
return ServiceResult<bool>.Failure("forbidden", "Admin role is required.");
|
||||
|
||||
if (user.Id == userId)
|
||||
@@ -129,27 +129,6 @@ public sealed class GameUserAdministrationService
|
||||
}
|
||||
}
|
||||
|
||||
private UserAccount? ResolveUserLocked(string sessionToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sessionToken))
|
||||
return null;
|
||||
|
||||
if (!m_StateStore.SessionsByToken.TryGetValue(sessionToken, out var session))
|
||||
return null;
|
||||
|
||||
return m_StateStore.UsersById.GetValueOrDefault(session.UserId);
|
||||
}
|
||||
|
||||
private static bool UserHasRoleLocked(UserAccount user, string role)
|
||||
{
|
||||
return RoleSerializer.HasRole(user.Roles, role);
|
||||
}
|
||||
|
||||
private static AdminUserSummary ToAdminUserSummary(UserAccount user)
|
||||
{
|
||||
return new(user.Id, user.Username, user.DisplayName, RoleSerializer.Parse(user.Roles));
|
||||
}
|
||||
|
||||
private void DeleteCampaignLocked(Guid campaignId)
|
||||
{
|
||||
if (!m_StateStore.CampaignsById.Remove(campaignId))
|
||||
@@ -193,37 +172,8 @@ public sealed class GameUserAdministrationService
|
||||
foreach (var account in m_StateStore.UsersById.Values.Where(account => account.ActiveCharacterId == characterId))
|
||||
account.ActiveCharacterId = null;
|
||||
|
||||
RemoveCharacterStateLocked(campaignId, characterId);
|
||||
TouchRosterLocked(campaignId);
|
||||
}
|
||||
|
||||
private void RemoveCharacterStateLocked(Guid? campaignId, Guid characterId)
|
||||
{
|
||||
if (!campaignId.HasValue || !m_StateStore.CampaignStateById.TryGetValue(campaignId.Value, out var state))
|
||||
return;
|
||||
|
||||
state.CharacterVersions.Remove(characterId);
|
||||
}
|
||||
|
||||
private void TouchRosterLocked(Guid? campaignId)
|
||||
{
|
||||
if (!campaignId.HasValue || !m_StateStore.CampaignsById.ContainsKey(campaignId.Value))
|
||||
return;
|
||||
|
||||
var state = GetOrCreateCampaignStateLocked(campaignId.Value);
|
||||
state.TotalVersion += 1;
|
||||
state.RosterVersion += 1;
|
||||
}
|
||||
|
||||
private GameCampaignStateTracker GetOrCreateCampaignStateLocked(Guid campaignId)
|
||||
{
|
||||
if (!m_StateStore.CampaignStateById.TryGetValue(campaignId, out var state))
|
||||
{
|
||||
state = new GameCampaignStateTracker();
|
||||
m_StateStore.CampaignStateById[campaignId] = state;
|
||||
}
|
||||
|
||||
return state;
|
||||
m_StateStore.RemoveCharacterStateLocked(campaignId, characterId);
|
||||
m_StateStore.TouchRosterLocked(campaignId);
|
||||
}
|
||||
|
||||
private readonly GamePersistenceService m_PersistenceService;
|
||||
|
||||
Reference in New Issue
Block a user