Files
RpgRoller/RpgRoller/Services/GameCharacterService.cs
2026-04-05 02:05:24 +02:00

184 lines
8.8 KiB
C#

using RpgRoller.Contracts;
using RpgRoller.Domain;
namespace RpgRoller.Services;
public sealed class GameCharacterService(GameStateStore stateStore, GamePersistenceService 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 (stateStore.Gate)
{
var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken);
if (user is null)
return ServiceResult<CharacterSummary>.Failure("unauthorized", "You must be logged in.");
if (!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()
};
stateStore.CharactersById[character.Id] = character;
stateStore.AddCharacterStateLocked(character.CampaignId, character.Id);
stateStore.TouchRosterLocked(character.CampaignId);
persistenceService.PersistStateLocked();
return ServiceResult<CharacterSummary>.Success(GameDtoMapper.ToCharacterSummary(stateStore, 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 (stateStore.Gate)
{
var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken);
if (user is null)
return ServiceResult<CharacterSummary>.Failure("unauthorized", "You must be logged in.");
if (!stateStore.CharactersById.TryGetValue(characterId, out var character))
return ServiceResult<CharacterSummary>.Failure("character_not_found", "Character was not found.");
Campaign? targetCampaign = null;
if (campaignId.HasValue && !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 = GameAuthorization.HasRole(user, UserRoles.Admin);
var isSourceGm = character.CampaignId.HasValue && stateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var sourceCampaign) && sourceCampaign.GmUserId == user.Id;
var isTargetGm = targetCampaign is not null && targetCampaign.GmUserId == user.Id;
if (!isOwner && !isAdmin && !isSourceGm && !isTargetGm)
return ServiceResult<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 (!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 && stateStore.UsersById.TryGetValue(previousOwnerUserId, out var previousOwner) && previousOwner.ActiveCharacterId == character.Id)
previousOwner.ActiveCharacterId = null;
}
if (sourceCampaignId != character.CampaignId)
{
stateStore.RemoveCharacterStateLocked(sourceCampaignId, character.Id);
stateStore.AddCharacterStateLocked(character.CampaignId, character.Id);
}
stateStore.TouchRosterLocked(sourceCampaignId);
if (sourceCampaignId != character.CampaignId)
stateStore.TouchRosterLocked(character.CampaignId);
persistenceService.PersistStateLocked();
return ServiceResult<CharacterSummary>.Success(GameDtoMapper.ToCharacterSummary(stateStore, character));
}
}
public ServiceResult<bool> DeleteCharacter(string sessionToken, Guid characterId)
{
lock (stateStore.Gate)
{
var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken);
if (user is null)
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
if (!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 = GameAuthorization.HasRole(user, UserRoles.Admin);
if (!isOwner && !isAdmin)
return ServiceResult<bool>.Failure("forbidden", "Only the owner or admin can delete this character.");
DeleteCharacterLocked(characterId);
persistenceService.PersistStateLocked();
return ServiceResult<bool>.Success(true);
}
}
public ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId)
{
lock (stateStore.Gate)
{
var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken);
if (user is null)
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
if (!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;
persistenceService.PersistStateLocked();
return ServiceResult<bool>.Success(true);
}
}
public ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken)
{
lock (stateStore.Gate)
{
var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken);
if (user is null)
return ServiceResult<IReadOnlyList<CharacterSummary>>.Failure("unauthorized", "You must be logged in.");
var characters = stateStore.CharactersById.Values.Where(character => character.OwnerUserId == user.Id).OrderBy(character => character.Name, StringComparer.OrdinalIgnoreCase).Select(character => GameDtoMapper.ToCharacterSummary(stateStore, character)).ToArray();
return ServiceResult<IReadOnlyList<CharacterSummary>>.Success(characters);
}
}
private void DeleteCharacterLocked(Guid characterId)
{
if (!stateStore.CharactersById.TryGetValue(characterId, out var character))
return;
var campaignId = character.CampaignId;
stateStore.CharactersById.Remove(characterId);
var skillGroupIds = stateStore.SkillGroupsById.Values.Where(group => group.CharacterId == characterId).Select(group => group.Id).ToHashSet();
foreach (var skillGroupId in skillGroupIds)
stateStore.SkillGroupsById.Remove(skillGroupId);
var skillIds = stateStore.SkillsById.Values.Where(skill => skill.CharacterId == characterId).Select(skill => skill.Id).ToHashSet();
foreach (var skillId in skillIds)
stateStore.SkillsById.Remove(skillId);
stateStore.RollLog.RemoveAll(entry => entry.CharacterId == characterId || skillIds.Contains(entry.SkillId));
foreach (var user in stateStore.UsersById.Values.Where(account => account.ActiveCharacterId == characterId))
user.ActiveCharacterId = null;
stateStore.RemoveCharacterStateLocked(campaignId, characterId);
stateStore.TouchRosterLocked(campaignId);
}
private static string NormalizeUsername(string username)
{
return username.ToUpperInvariant();
}
}