Extract game character service
This commit is contained in:
@@ -24,6 +24,7 @@ Backend:
|
|||||||
- `RpgRoller/Services/GameStateStore.cs`, `GameStateCloneFactory.cs`, and `GamePersistenceService.cs`: extracted runtime-state ownership and SQLite load/save boundaries used by `GameService`
|
- `RpgRoller/Services/GameStateStore.cs`, `GameStateCloneFactory.cs`, and `GamePersistenceService.cs`: extracted runtime-state ownership and SQLite load/save boundaries used by `GameService`
|
||||||
- `RpgRoller/Services/GameAuthService.cs`: extracted auth/session workflow ownership while `GameService` stays on the existing `IGameService` contract
|
- `RpgRoller/Services/GameAuthService.cs`: extracted auth/session workflow ownership while `GameService` stays on the existing `IGameService` contract
|
||||||
- `RpgRoller/Services/GameCampaignService.cs`: extracted campaign creation, visibility reads, roster reads, and deletion workflows behind the same facade contract
|
- `RpgRoller/Services/GameCampaignService.cs`: extracted campaign creation, visibility reads, roster reads, and deletion workflows behind the same facade contract
|
||||||
|
- `RpgRoller/Services/GameCharacterService.cs`: extracted character creation, transfer, activation, deletion, and owner-scoped listing workflows behind the same facade contract
|
||||||
|
|
||||||
Frontend:
|
Frontend:
|
||||||
|
|
||||||
|
|||||||
264
RpgRoller/Services/GameCharacterService.cs
Normal file
264
RpgRoller/Services/GameCharacterService.cs
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
using RpgRoller.Contracts;
|
||||||
|
using RpgRoller.Domain;
|
||||||
|
|
||||||
|
namespace RpgRoller.Services;
|
||||||
|
|
||||||
|
public sealed class GameCharacterService
|
||||||
|
{
|
||||||
|
public GameCharacterService(GameStateStore stateStore, GamePersistenceService persistenceService)
|
||||||
|
{
|
||||||
|
m_StateStore = stateStore;
|
||||||
|
m_PersistenceService = persistenceService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<CharacterSummary> CreateCharacter(string sessionToken, string name, Guid campaignId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
return ServiceResult<CharacterSummary>.Failure("invalid_character_name", "Character name is required.");
|
||||||
|
|
||||||
|
lock (m_StateStore.Gate)
|
||||||
|
{
|
||||||
|
var user = ResolveUserLocked(sessionToken);
|
||||||
|
if (user is null)
|
||||||
|
return ServiceResult<CharacterSummary>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
|
if (!m_StateStore.CampaignsById.ContainsKey(campaignId))
|
||||||
|
return ServiceResult<CharacterSummary>.Failure("campaign_not_found", "Campaign was not found.");
|
||||||
|
|
||||||
|
var character = new Character
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
OwnerUserId = user.Id,
|
||||||
|
CampaignId = campaignId,
|
||||||
|
Name = name.Trim()
|
||||||
|
};
|
||||||
|
|
||||||
|
m_StateStore.CharactersById[character.Id] = character;
|
||||||
|
AddCharacterStateLocked(character.CampaignId, character.Id);
|
||||||
|
TouchRosterLocked(character.CampaignId);
|
||||||
|
|
||||||
|
m_PersistenceService.PersistStateLocked();
|
||||||
|
return ServiceResult<CharacterSummary>.Success(ToCharacterSummary(character));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<CharacterSummary> UpdateCharacter(string sessionToken, Guid characterId, string name, Guid? campaignId, string? ownerUsername = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
return ServiceResult<CharacterSummary>.Failure("invalid_character_name", "Character name is required.");
|
||||||
|
|
||||||
|
lock (m_StateStore.Gate)
|
||||||
|
{
|
||||||
|
var user = ResolveUserLocked(sessionToken);
|
||||||
|
if (user is null)
|
||||||
|
return ServiceResult<CharacterSummary>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
|
if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character))
|
||||||
|
return ServiceResult<CharacterSummary>.Failure("character_not_found", "Character was not found.");
|
||||||
|
|
||||||
|
Campaign? targetCampaign = null;
|
||||||
|
if (campaignId.HasValue && !m_StateStore.CampaignsById.TryGetValue(campaignId.Value, out targetCampaign))
|
||||||
|
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 isSourceGm = character.CampaignId.HasValue &&
|
||||||
|
m_StateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var sourceCampaign) &&
|
||||||
|
sourceCampaign.GmUserId == user.Id;
|
||||||
|
var isTargetGm = targetCampaign is not null && targetCampaign.GmUserId == user.Id;
|
||||||
|
if (!isOwner && !isAdmin && !isSourceGm && !isTargetGm)
|
||||||
|
return ServiceResult<CharacterSummary>.Failure("forbidden", "Only the owner, GM, or admin can edit this character.");
|
||||||
|
|
||||||
|
var sourceCampaignId = character.CampaignId;
|
||||||
|
var previousOwnerUserId = character.OwnerUserId;
|
||||||
|
character.Name = name.Trim();
|
||||||
|
character.CampaignId = campaignId;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(ownerUsername))
|
||||||
|
{
|
||||||
|
var trimmedOwnerUsername = ownerUsername.Trim();
|
||||||
|
var normalizedOwnerUsername = NormalizeUsername(trimmedOwnerUsername);
|
||||||
|
if (!m_StateStore.UserIdsByUsername.TryGetValue(normalizedOwnerUsername, out var targetOwnerUserId))
|
||||||
|
return ServiceResult<CharacterSummary>.Failure("owner_not_found", "Owner username was not found.");
|
||||||
|
|
||||||
|
if (targetOwnerUserId != character.OwnerUserId && !isAdmin && !isSourceGm && !isTargetGm)
|
||||||
|
return ServiceResult<CharacterSummary>.Failure("forbidden", "Only the GM or admin can change character owner.");
|
||||||
|
|
||||||
|
character.OwnerUserId = targetOwnerUserId;
|
||||||
|
if (character.OwnerUserId != previousOwnerUserId &&
|
||||||
|
m_StateStore.UsersById.TryGetValue(previousOwnerUserId, out var previousOwner) &&
|
||||||
|
previousOwner.ActiveCharacterId == character.Id)
|
||||||
|
{
|
||||||
|
previousOwner.ActiveCharacterId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceCampaignId != character.CampaignId)
|
||||||
|
{
|
||||||
|
RemoveCharacterStateLocked(sourceCampaignId, character.Id);
|
||||||
|
AddCharacterStateLocked(character.CampaignId, character.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
TouchRosterLocked(sourceCampaignId);
|
||||||
|
if (sourceCampaignId != character.CampaignId)
|
||||||
|
TouchRosterLocked(character.CampaignId);
|
||||||
|
|
||||||
|
m_PersistenceService.PersistStateLocked();
|
||||||
|
return ServiceResult<CharacterSummary>.Success(ToCharacterSummary(character));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<bool> DeleteCharacter(string sessionToken, Guid characterId)
|
||||||
|
{
|
||||||
|
lock (m_StateStore.Gate)
|
||||||
|
{
|
||||||
|
var user = ResolveUserLocked(sessionToken);
|
||||||
|
if (user is null)
|
||||||
|
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
|
if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character))
|
||||||
|
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);
|
||||||
|
if (!isOwner && !isAdmin)
|
||||||
|
return ServiceResult<bool>.Failure("forbidden", "Only the owner or admin can delete this character.");
|
||||||
|
|
||||||
|
DeleteCharacterLocked(characterId);
|
||||||
|
m_PersistenceService.PersistStateLocked();
|
||||||
|
return ServiceResult<bool>.Success(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId)
|
||||||
|
{
|
||||||
|
lock (m_StateStore.Gate)
|
||||||
|
{
|
||||||
|
var user = ResolveUserLocked(sessionToken);
|
||||||
|
if (user is null)
|
||||||
|
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
|
if (!m_StateStore.CharactersById.TryGetValue(characterId, out var character))
|
||||||
|
return ServiceResult<bool>.Failure("character_not_found", "Character was not found.");
|
||||||
|
|
||||||
|
if (character.OwnerUserId != user.Id)
|
||||||
|
return ServiceResult<bool>.Failure("forbidden", "You can activate only your own character.");
|
||||||
|
|
||||||
|
user.ActiveCharacterId = character.Id;
|
||||||
|
m_PersistenceService.PersistStateLocked();
|
||||||
|
return ServiceResult<bool>.Success(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken)
|
||||||
|
{
|
||||||
|
lock (m_StateStore.Gate)
|
||||||
|
{
|
||||||
|
var user = ResolveUserLocked(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)
|
||||||
|
.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))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var campaignId = character.CampaignId;
|
||||||
|
m_StateStore.CharactersById.Remove(characterId);
|
||||||
|
|
||||||
|
var skillGroupIds = m_StateStore.SkillGroupsById.Values.Where(group => group.CharacterId == characterId).Select(group => group.Id).ToHashSet();
|
||||||
|
foreach (var skillGroupId in skillGroupIds)
|
||||||
|
m_StateStore.SkillGroupsById.Remove(skillGroupId);
|
||||||
|
|
||||||
|
var skillIds = m_StateStore.SkillsById.Values.Where(skill => skill.CharacterId == characterId).Select(skill => skill.Id).ToHashSet();
|
||||||
|
foreach (var skillId in skillIds)
|
||||||
|
m_StateStore.SkillsById.Remove(skillId);
|
||||||
|
|
||||||
|
m_StateStore.RollLog.RemoveAll(entry => entry.CharacterId == characterId || skillIds.Contains(entry.SkillId));
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeUsername(string username)
|
||||||
|
{
|
||||||
|
return username.ToUpperInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly GamePersistenceService m_PersistenceService;
|
||||||
|
private readonly GameStateStore m_StateStore;
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ public sealed class GameService : IGameService
|
|||||||
m_PersistenceService = new(dbContextFactory, m_StateStore);
|
m_PersistenceService = new(dbContextFactory, m_StateStore);
|
||||||
m_AuthService = new(m_StateStore, passwordHasher, m_PersistenceService);
|
m_AuthService = new(m_StateStore, passwordHasher, m_PersistenceService);
|
||||||
m_CampaignService = new(m_StateStore, m_PersistenceService);
|
m_CampaignService = new(m_StateStore, m_PersistenceService);
|
||||||
|
m_CharacterService = new(m_StateStore, m_PersistenceService);
|
||||||
m_DiceRoller = diceRoller;
|
m_DiceRoller = diceRoller;
|
||||||
LoadStateFromDatabase();
|
LoadStateFromDatabase();
|
||||||
}
|
}
|
||||||
@@ -193,155 +194,27 @@ public sealed class GameService : IGameService
|
|||||||
|
|
||||||
public ServiceResult<CharacterSummary> CreateCharacter(string sessionToken, string name, Guid campaignId)
|
public ServiceResult<CharacterSummary> CreateCharacter(string sessionToken, string name, Guid campaignId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
return m_CharacterService.CreateCharacter(sessionToken, name, campaignId);
|
||||||
return ServiceResult<CharacterSummary>.Failure("invalid_character_name", "Character name is required.");
|
|
||||||
|
|
||||||
lock (m_Gate)
|
|
||||||
{
|
|
||||||
var user = ResolveUserLocked(sessionToken);
|
|
||||||
if (user is null)
|
|
||||||
return ServiceResult<CharacterSummary>.Failure("unauthorized", "You must be logged in.");
|
|
||||||
|
|
||||||
if (!m_CampaignsById.ContainsKey(campaignId))
|
|
||||||
return ServiceResult<CharacterSummary>.Failure("campaign_not_found", "Campaign was not found.");
|
|
||||||
|
|
||||||
var character = new Character
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
OwnerUserId = user.Id,
|
|
||||||
CampaignId = campaignId,
|
|
||||||
Name = name.Trim()
|
|
||||||
};
|
|
||||||
|
|
||||||
m_CharactersById[character.Id] = character;
|
|
||||||
AddCharacterStateLocked(character.CampaignId, character.Id);
|
|
||||||
TouchRosterLocked(character.CampaignId);
|
|
||||||
|
|
||||||
PersistStateLocked();
|
|
||||||
return ServiceResult<CharacterSummary>.Success(ToCharacterSummary(character));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<CharacterSummary> UpdateCharacter(string sessionToken, Guid characterId, string name, Guid? campaignId, string? ownerUsername = null)
|
public ServiceResult<CharacterSummary> UpdateCharacter(string sessionToken, Guid characterId, string name, Guid? campaignId, string? ownerUsername = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
return m_CharacterService.UpdateCharacter(sessionToken, characterId, name, campaignId, ownerUsername);
|
||||||
return ServiceResult<CharacterSummary>.Failure("invalid_character_name", "Character name is required.");
|
|
||||||
|
|
||||||
lock (m_Gate)
|
|
||||||
{
|
|
||||||
var user = ResolveUserLocked(sessionToken);
|
|
||||||
if (user is null)
|
|
||||||
return ServiceResult<CharacterSummary>.Failure("unauthorized", "You must be logged in.");
|
|
||||||
|
|
||||||
if (!m_CharactersById.TryGetValue(characterId, out var character))
|
|
||||||
return ServiceResult<CharacterSummary>.Failure("character_not_found", "Character was not found.");
|
|
||||||
|
|
||||||
Campaign? targetCampaign = null;
|
|
||||||
if (campaignId.HasValue && !m_CampaignsById.TryGetValue(campaignId.Value, out targetCampaign))
|
|
||||||
return ServiceResult<CharacterSummary>.Failure("campaign_not_found", "Campaign was not found.");
|
|
||||||
|
|
||||||
var isOwner = character.OwnerUserId == user.Id;
|
|
||||||
var isAdmin = UserHasRoleLocked(user, UserRoles.Admin);
|
|
||||||
var isSourceGm = character.CampaignId.HasValue &&
|
|
||||||
m_CampaignsById.TryGetValue(character.CampaignId.Value, out var sourceCampaign) &&
|
|
||||||
sourceCampaign.GmUserId == user.Id;
|
|
||||||
var isTargetGm = targetCampaign is not null && targetCampaign.GmUserId == user.Id;
|
|
||||||
if (!isOwner && !isAdmin && !isSourceGm && !isTargetGm)
|
|
||||||
return ServiceResult<CharacterSummary>.Failure("forbidden", "Only the owner, GM, or admin can edit this character.");
|
|
||||||
|
|
||||||
var sourceCampaignId = character.CampaignId;
|
|
||||||
var previousOwnerUserId = character.OwnerUserId;
|
|
||||||
character.Name = name.Trim();
|
|
||||||
character.CampaignId = campaignId;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(ownerUsername))
|
|
||||||
{
|
|
||||||
var trimmedOwnerUsername = ownerUsername.Trim();
|
|
||||||
var normalizedOwnerUsername = NormalizeUsername(trimmedOwnerUsername);
|
|
||||||
if (!m_UserIdsByUsername.TryGetValue(normalizedOwnerUsername, out var targetOwnerUserId))
|
|
||||||
return ServiceResult<CharacterSummary>.Failure("owner_not_found", "Owner username was not found.");
|
|
||||||
|
|
||||||
if (targetOwnerUserId != character.OwnerUserId && !isAdmin && !isSourceGm && !isTargetGm)
|
|
||||||
return ServiceResult<CharacterSummary>.Failure("forbidden", "Only the GM or admin can change character owner.");
|
|
||||||
|
|
||||||
character.OwnerUserId = targetOwnerUserId;
|
|
||||||
if (character.OwnerUserId != previousOwnerUserId &&
|
|
||||||
m_UsersById.TryGetValue(previousOwnerUserId, out var previousOwner) &&
|
|
||||||
previousOwner.ActiveCharacterId == character.Id)
|
|
||||||
{
|
|
||||||
previousOwner.ActiveCharacterId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceCampaignId != character.CampaignId)
|
|
||||||
{
|
|
||||||
RemoveCharacterStateLocked(sourceCampaignId, character.Id);
|
|
||||||
AddCharacterStateLocked(character.CampaignId, character.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
TouchRosterLocked(sourceCampaignId);
|
|
||||||
if (sourceCampaignId != character.CampaignId)
|
|
||||||
TouchRosterLocked(character.CampaignId);
|
|
||||||
|
|
||||||
PersistStateLocked();
|
|
||||||
return ServiceResult<CharacterSummary>.Success(ToCharacterSummary(character));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<bool> DeleteCharacter(string sessionToken, Guid characterId)
|
public ServiceResult<bool> DeleteCharacter(string sessionToken, Guid characterId)
|
||||||
{
|
{
|
||||||
lock (m_Gate)
|
return m_CharacterService.DeleteCharacter(sessionToken, characterId);
|
||||||
{
|
|
||||||
var user = ResolveUserLocked(sessionToken);
|
|
||||||
if (user is null)
|
|
||||||
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
|
||||||
|
|
||||||
if (!m_CharactersById.TryGetValue(characterId, out var character))
|
|
||||||
return ServiceResult<bool>.Failure("character_not_found", "Character was not found.");
|
|
||||||
|
|
||||||
var isOwner = character.OwnerUserId == user.Id;
|
|
||||||
var isAdmin = UserHasRoleLocked(user, UserRoles.Admin);
|
|
||||||
if (!isOwner && !isAdmin)
|
|
||||||
return ServiceResult<bool>.Failure("forbidden", "Only the owner or admin can delete this character.");
|
|
||||||
|
|
||||||
DeleteCharacterLocked(characterId);
|
|
||||||
PersistStateLocked();
|
|
||||||
return ServiceResult<bool>.Success(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId)
|
public ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId)
|
||||||
{
|
{
|
||||||
lock (m_Gate)
|
return m_CharacterService.ActivateCharacter(sessionToken, characterId);
|
||||||
{
|
|
||||||
var user = ResolveUserLocked(sessionToken);
|
|
||||||
if (user is null)
|
|
||||||
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
|
||||||
|
|
||||||
if (!m_CharactersById.TryGetValue(characterId, out var character))
|
|
||||||
return ServiceResult<bool>.Failure("character_not_found", "Character was not found.");
|
|
||||||
|
|
||||||
if (character.OwnerUserId != user.Id)
|
|
||||||
return ServiceResult<bool>.Failure("forbidden", "You can activate only your own character.");
|
|
||||||
|
|
||||||
user.ActiveCharacterId = character.Id;
|
|
||||||
PersistStateLocked();
|
|
||||||
return ServiceResult<bool>.Success(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken)
|
public ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken)
|
||||||
{
|
{
|
||||||
lock (m_Gate)
|
return m_CharacterService.GetOwnCharacters(sessionToken);
|
||||||
{
|
|
||||||
var user = ResolveUserLocked(sessionToken);
|
|
||||||
if (user is null)
|
|
||||||
return ServiceResult<IReadOnlyList<CharacterSummary>>.Failure("unauthorized", "You must be logged in.");
|
|
||||||
|
|
||||||
var characters = m_CharactersById.Values.Where(c => c.OwnerUserId == user.Id).OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).Select(ToCharacterSummary).ToArray();
|
|
||||||
|
|
||||||
return ServiceResult<IReadOnlyList<CharacterSummary>>.Success(characters);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -1067,12 +940,6 @@ public sealed class GameService : IGameService
|
|||||||
return new(characterId, skillGroups, skills);
|
return new(characterId, skillGroups, skills);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CharacterSummary ToCharacterSummary(Character character)
|
|
||||||
{
|
|
||||||
var ownerDisplayName = ResolveOwnerDisplayName(character.OwnerUserId);
|
|
||||||
return new(character.Id, character.Name, character.OwnerUserId, character.CampaignId, ownerDisplayName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private CampaignStateSnapshot ToCampaignStateSnapshot(Campaign campaign)
|
private CampaignStateSnapshot ToCampaignStateSnapshot(Campaign campaign)
|
||||||
{
|
{
|
||||||
var state = GetOrCreateCampaignStateLocked(campaign.Id);
|
var state = GetOrCreateCampaignStateLocked(campaign.Id);
|
||||||
@@ -1538,11 +1405,6 @@ public sealed class GameService : IGameService
|
|||||||
m_PersistenceService.PersistStateLocked();
|
m_PersistenceService.PersistStateLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizeUsername(string username)
|
|
||||||
{
|
|
||||||
return username.ToUpperInvariant();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int NormalizeCampaignLogPageSize(int? limit)
|
private static int NormalizeCampaignLogPageSize(int? limit)
|
||||||
{
|
{
|
||||||
if (!limit.HasValue)
|
if (!limit.HasValue)
|
||||||
@@ -1560,6 +1422,7 @@ public sealed class GameService : IGameService
|
|||||||
private readonly Dictionary<Guid, Campaign> m_CampaignsById;
|
private readonly Dictionary<Guid, Campaign> m_CampaignsById;
|
||||||
private readonly Dictionary<Guid, GameCampaignStateTracker> m_CampaignStateById;
|
private readonly Dictionary<Guid, GameCampaignStateTracker> m_CampaignStateById;
|
||||||
private readonly GameCampaignService m_CampaignService;
|
private readonly GameCampaignService m_CampaignService;
|
||||||
|
private readonly GameCharacterService m_CharacterService;
|
||||||
private readonly Dictionary<Guid, Character> m_CharactersById;
|
private readonly Dictionary<Guid, Character> m_CharactersById;
|
||||||
private readonly GameAuthService m_AuthService;
|
private readonly GameAuthService m_AuthService;
|
||||||
private readonly IDiceRoller m_DiceRoller;
|
private readonly IDiceRoller m_DiceRoller;
|
||||||
|
|||||||
Reference in New Issue
Block a user