using RpgRoller.Contracts; using RpgRoller.Domain; namespace RpgRoller.Services; public sealed class GameCharacterService(GameStateStore stateStore, GamePersistenceService persistenceService) { public ServiceResult CreateCharacter(string sessionToken, string name, Guid campaignId) { if (string.IsNullOrWhiteSpace(name)) return ServiceResult.Failure("invalid_character_name", "Character name is required."); lock (stateStore.Gate) { var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); if (!stateStore.CampaignsById.ContainsKey(campaignId)) return ServiceResult.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.Success(GameDtoMapper.ToCharacterSummary(stateStore, character)); } } public ServiceResult UpdateCharacter(string sessionToken, Guid characterId, string name, Guid? campaignId, string? ownerUsername = null) { if (string.IsNullOrWhiteSpace(name)) return ServiceResult.Failure("invalid_character_name", "Character name is required."); lock (stateStore.Gate) { var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); if (!stateStore.CharactersById.TryGetValue(characterId, out var character)) return ServiceResult.Failure("character_not_found", "Character was not found."); Campaign? targetCampaign = null; if (campaignId.HasValue && !stateStore.CampaignsById.TryGetValue(campaignId.Value, out targetCampaign)) return ServiceResult.Failure("campaign_not_found", "Campaign was not found."); var isOwner = character.OwnerUserId == user.Id; var isAdmin = GameAuthorization.HasRole(user, UserRoles.Admin); var isSourceGm = character.CampaignId.HasValue && stateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var sourceCampaign) && sourceCampaign.GmUserId == user.Id; var isTargetGm = targetCampaign is not null && targetCampaign.GmUserId == user.Id; if (!isOwner && !isAdmin && !isSourceGm && !isTargetGm) return ServiceResult.Failure("forbidden", "Only the owner, GM, or admin can edit this character."); 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.Failure("owner_not_found", "Owner username was not found."); if (targetOwnerUserId != character.OwnerUserId && !isAdmin && !isSourceGm && !isTargetGm) return ServiceResult.Failure("forbidden", "Only the GM or admin can change character owner."); character.OwnerUserId = targetOwnerUserId; if (character.OwnerUserId != previousOwnerUserId && 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.Success(GameDtoMapper.ToCharacterSummary(stateStore, character)); } } public ServiceResult DeleteCharacter(string sessionToken, Guid characterId) { lock (stateStore.Gate) { var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); if (!stateStore.CharactersById.TryGetValue(characterId, out var character)) return ServiceResult.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.Failure("forbidden", "Only the owner or admin can delete this character."); DeleteCharacterLocked(characterId); persistenceService.PersistStateLocked(); return ServiceResult.Success(true); } } public ServiceResult ActivateCharacter(string sessionToken, Guid characterId) { lock (stateStore.Gate) { var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult.Failure("unauthorized", "You must be logged in."); if (!stateStore.CharactersById.TryGetValue(characterId, out var character)) return ServiceResult.Failure("character_not_found", "Character was not found."); if (character.OwnerUserId != user.Id) return ServiceResult.Failure("forbidden", "You can activate only your own character."); user.ActiveCharacterId = character.Id; persistenceService.PersistStateLocked(); return ServiceResult.Success(true); } } public ServiceResult> GetOwnCharacters(string sessionToken) { lock (stateStore.Gate) { var user = GameContextResolver.ResolveUserLocked(stateStore, sessionToken); if (user is null) return ServiceResult>.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>.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(); } }