Extract game state persistence
This commit is contained in:
@@ -21,6 +21,7 @@ Backend:
|
|||||||
- `RpgRoller/Api/`: endpoint mapping modules and auth/session filter helpers
|
- `RpgRoller/Api/`: endpoint mapping modules and auth/session filter helpers
|
||||||
- `RpgRoller/Services/`: game workflows with explicit method parameters (no API DTO dependencies)
|
- `RpgRoller/Services/`: game workflows with explicit method parameters (no API DTO dependencies)
|
||||||
- `RpgRoller/Services/SkillDefinitionValidator.cs`, `RoleSerializer.cs`, `RollVisibilityParser.cs`, and `CustomRollOptionsResolver.cs`: extracted pure backend rule helpers used by `GameService`
|
- `RpgRoller/Services/SkillDefinitionValidator.cs`, `RoleSerializer.cs`, `RollVisibilityParser.cs`, and `CustomRollOptionsResolver.cs`: extracted pure backend rule helpers used by `GameService`
|
||||||
|
- `RpgRoller/Services/GameStateStore.cs`, `GameStateCloneFactory.cs`, and `GamePersistenceService.cs`: extracted runtime-state ownership and SQLite load/save boundaries used by `GameService`
|
||||||
|
|
||||||
Frontend:
|
Frontend:
|
||||||
|
|
||||||
|
|||||||
62
RpgRoller.Tests/Services/ServiceStateInfrastructureTests.cs
Normal file
62
RpgRoller.Tests/Services/ServiceStateInfrastructureTests.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
namespace RpgRoller.Tests;
|
||||||
|
|
||||||
|
public sealed class ServiceStateInfrastructureTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GameStateStore_StartsWithEmptyMutableCollections()
|
||||||
|
{
|
||||||
|
var store = new GameStateStore();
|
||||||
|
|
||||||
|
Assert.NotNull(store.Gate);
|
||||||
|
Assert.Empty(store.UsersById);
|
||||||
|
Assert.Empty(store.UserIdsByUsername);
|
||||||
|
Assert.Empty(store.SessionsByToken);
|
||||||
|
Assert.Empty(store.CampaignsById);
|
||||||
|
Assert.Empty(store.CampaignStateById);
|
||||||
|
Assert.Empty(store.CharactersById);
|
||||||
|
Assert.Empty(store.SkillGroupsById);
|
||||||
|
Assert.Empty(store.SkillsById);
|
||||||
|
Assert.Empty(store.RollLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GameStateCloneFactory_ProducesDetachedCopies()
|
||||||
|
{
|
||||||
|
var user = new UserAccount
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Username = "user",
|
||||||
|
UsernameNormalized = "USER",
|
||||||
|
PasswordHash = "hash",
|
||||||
|
DisplayName = "User",
|
||||||
|
Roles = "admin",
|
||||||
|
ActiveCharacterId = Guid.NewGuid()
|
||||||
|
};
|
||||||
|
var session = new UserSession { Token = "token", UserId = user.Id, CreatedAtUtc = DateTimeOffset.UtcNow };
|
||||||
|
var campaign = new Campaign { Id = Guid.NewGuid(), GmUserId = user.Id, Name = "Main", Ruleset = RulesetKind.D6, Version = 3 };
|
||||||
|
var character = new Character { Id = Guid.NewGuid(), OwnerUserId = user.Id, CampaignId = campaign.Id, Name = "Hero" };
|
||||||
|
var skillGroup = new SkillGroup { Id = Guid.NewGuid(), CharacterId = character.Id, Name = "Group", DiceRollDefinition = "2D+1", WildDice = 1, AllowFumble = true, FumbleRange = null };
|
||||||
|
var skill = new Skill { Id = Guid.NewGuid(), CharacterId = character.Id, SkillGroupId = skillGroup.Id, Name = "Skill", DiceRollDefinition = "2D+2", WildDice = 1, AllowFumble = true, FumbleRange = null };
|
||||||
|
var logEntry = new RollLogEntry
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
CampaignId = campaign.Id,
|
||||||
|
CharacterId = character.Id,
|
||||||
|
SkillId = skill.Id,
|
||||||
|
RollerUserId = user.Id,
|
||||||
|
Visibility = RollVisibility.Public,
|
||||||
|
Result = 12,
|
||||||
|
Breakdown = "6 + 6 = 12",
|
||||||
|
Dice = "[]",
|
||||||
|
TimestampUtc = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.NotSame(user, GameStateCloneFactory.CloneUser(user));
|
||||||
|
Assert.NotSame(session, GameStateCloneFactory.CloneSession(session));
|
||||||
|
Assert.NotSame(campaign, GameStateCloneFactory.CloneCampaign(campaign));
|
||||||
|
Assert.NotSame(character, GameStateCloneFactory.CloneCharacter(character));
|
||||||
|
Assert.NotSame(skillGroup, GameStateCloneFactory.CloneSkillGroup(skillGroup));
|
||||||
|
Assert.NotSame(skill, GameStateCloneFactory.CloneSkill(skill));
|
||||||
|
Assert.NotSame(logEntry, GameStateCloneFactory.CloneRollLogEntry(logEntry));
|
||||||
|
}
|
||||||
|
}
|
||||||
109
RpgRoller/Services/GamePersistenceService.cs
Normal file
109
RpgRoller/Services/GamePersistenceService.cs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using RpgRoller.Data;
|
||||||
|
using RpgRoller.Domain;
|
||||||
|
|
||||||
|
namespace RpgRoller.Services;
|
||||||
|
|
||||||
|
public sealed class GamePersistenceService
|
||||||
|
{
|
||||||
|
public GamePersistenceService(IDbContextFactory<RpgRollerDbContext> dbContextFactory, GameStateStore stateStore)
|
||||||
|
{
|
||||||
|
m_DbContextFactory = dbContextFactory;
|
||||||
|
m_StateStore = stateStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadStateFromDatabase()
|
||||||
|
{
|
||||||
|
using var db = m_DbContextFactory.CreateDbContext();
|
||||||
|
var users = db.Users.AsNoTracking().ToList();
|
||||||
|
var sessions = db.Sessions.AsNoTracking().ToList();
|
||||||
|
var campaigns = db.Campaigns.AsNoTracking().ToList();
|
||||||
|
var characters = db.Characters.AsNoTracking().ToList();
|
||||||
|
var skillGroups = db.SkillGroups.AsNoTracking().ToList();
|
||||||
|
var skills = db.Skills.AsNoTracking().ToList();
|
||||||
|
var logEntries = db.RollLogEntries.AsNoTracking().ToList().OrderBy(x => x.TimestampUtc).ThenBy(x => x.Id).ToList();
|
||||||
|
|
||||||
|
lock (m_StateStore.Gate)
|
||||||
|
{
|
||||||
|
m_StateStore.UsersById.Clear();
|
||||||
|
m_StateStore.UserIdsByUsername.Clear();
|
||||||
|
m_StateStore.SessionsByToken.Clear();
|
||||||
|
m_StateStore.CampaignsById.Clear();
|
||||||
|
m_StateStore.CharactersById.Clear();
|
||||||
|
m_StateStore.SkillGroupsById.Clear();
|
||||||
|
m_StateStore.SkillsById.Clear();
|
||||||
|
m_StateStore.RollLog.Clear();
|
||||||
|
|
||||||
|
foreach (var user in users)
|
||||||
|
{
|
||||||
|
var normalizedUsername = string.IsNullOrWhiteSpace(user.UsernameNormalized) ? NormalizeUsername(user.Username) : user.UsernameNormalized;
|
||||||
|
|
||||||
|
var storedUser = new UserAccount
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
Username = user.Username,
|
||||||
|
UsernameNormalized = normalizedUsername,
|
||||||
|
PasswordHash = user.PasswordHash,
|
||||||
|
DisplayName = user.DisplayName,
|
||||||
|
Roles = string.IsNullOrWhiteSpace(user.Roles) ? string.Empty : RoleSerializer.Serialize(RoleSerializer.Parse(user.Roles)),
|
||||||
|
ActiveCharacterId = user.ActiveCharacterId
|
||||||
|
};
|
||||||
|
m_StateStore.UsersById[storedUser.Id] = storedUser;
|
||||||
|
m_StateStore.UserIdsByUsername[normalizedUsername] = storedUser.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var session in sessions)
|
||||||
|
{
|
||||||
|
if (m_StateStore.UsersById.ContainsKey(session.UserId))
|
||||||
|
m_StateStore.SessionsByToken[session.Token] = GameStateCloneFactory.CloneSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var campaign in campaigns)
|
||||||
|
m_StateStore.CampaignsById[campaign.Id] = GameStateCloneFactory.CloneCampaign(campaign);
|
||||||
|
|
||||||
|
foreach (var character in characters)
|
||||||
|
m_StateStore.CharactersById[character.Id] = GameStateCloneFactory.CloneCharacter(character);
|
||||||
|
|
||||||
|
foreach (var skillGroup in skillGroups)
|
||||||
|
m_StateStore.SkillGroupsById[skillGroup.Id] = GameStateCloneFactory.CloneSkillGroup(skillGroup);
|
||||||
|
|
||||||
|
foreach (var skill in skills)
|
||||||
|
m_StateStore.SkillsById[skill.Id] = GameStateCloneFactory.CloneSkill(skill);
|
||||||
|
|
||||||
|
m_StateStore.RollLog.AddRange(logEntries.Select(GameStateCloneFactory.CloneRollLogEntry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PersistStateLocked()
|
||||||
|
{
|
||||||
|
using var db = m_DbContextFactory.CreateDbContext();
|
||||||
|
using var transaction = db.Database.BeginTransaction();
|
||||||
|
|
||||||
|
db.RollLogEntries.ExecuteDelete();
|
||||||
|
db.Skills.ExecuteDelete();
|
||||||
|
db.SkillGroups.ExecuteDelete();
|
||||||
|
db.Characters.ExecuteDelete();
|
||||||
|
db.Campaigns.ExecuteDelete();
|
||||||
|
db.Sessions.ExecuteDelete();
|
||||||
|
db.Users.ExecuteDelete();
|
||||||
|
|
||||||
|
db.Users.AddRange(m_StateStore.UsersById.Values.Select(GameStateCloneFactory.CloneUser));
|
||||||
|
db.Sessions.AddRange(m_StateStore.SessionsByToken.Values.Select(GameStateCloneFactory.CloneSession));
|
||||||
|
db.Campaigns.AddRange(m_StateStore.CampaignsById.Values.Select(GameStateCloneFactory.CloneCampaign));
|
||||||
|
db.Characters.AddRange(m_StateStore.CharactersById.Values.Select(GameStateCloneFactory.CloneCharacter));
|
||||||
|
db.SkillGroups.AddRange(m_StateStore.SkillGroupsById.Values.Select(GameStateCloneFactory.CloneSkillGroup));
|
||||||
|
db.Skills.AddRange(m_StateStore.SkillsById.Values.Select(GameStateCloneFactory.CloneSkill));
|
||||||
|
db.RollLogEntries.AddRange(m_StateStore.RollLog.Select(GameStateCloneFactory.CloneRollLogEntry));
|
||||||
|
|
||||||
|
db.SaveChanges();
|
||||||
|
transaction.Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeUsername(string username)
|
||||||
|
{
|
||||||
|
return username.ToUpperInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IDbContextFactory<RpgRollerDbContext> m_DbContextFactory;
|
||||||
|
private readonly GameStateStore m_StateStore;
|
||||||
|
}
|
||||||
@@ -11,7 +11,18 @@ public sealed class GameService : IGameService
|
|||||||
{
|
{
|
||||||
public GameService(IDbContextFactory<RpgRollerDbContext> dbContextFactory, IPasswordHasher<UserAccount> passwordHasher, IDiceRoller diceRoller)
|
public GameService(IDbContextFactory<RpgRollerDbContext> dbContextFactory, IPasswordHasher<UserAccount> passwordHasher, IDiceRoller diceRoller)
|
||||||
{
|
{
|
||||||
m_DbContextFactory = dbContextFactory;
|
m_StateStore = new();
|
||||||
|
m_CampaignsById = m_StateStore.CampaignsById;
|
||||||
|
m_CampaignStateById = m_StateStore.CampaignStateById;
|
||||||
|
m_CharactersById = m_StateStore.CharactersById;
|
||||||
|
m_Gate = m_StateStore.Gate;
|
||||||
|
m_RollLog = m_StateStore.RollLog;
|
||||||
|
m_SessionsByToken = m_StateStore.SessionsByToken;
|
||||||
|
m_SkillGroupsById = m_StateStore.SkillGroupsById;
|
||||||
|
m_SkillsById = m_StateStore.SkillsById;
|
||||||
|
m_UserIdsByUsername = m_StateStore.UserIdsByUsername;
|
||||||
|
m_UsersById = m_StateStore.UsersById;
|
||||||
|
m_PersistenceService = new(dbContextFactory, m_StateStore);
|
||||||
m_PasswordHasher = passwordHasher;
|
m_PasswordHasher = passwordHasher;
|
||||||
m_DiceRoller = diceRoller;
|
m_DiceRoller = diceRoller;
|
||||||
LoadStateFromDatabase();
|
LoadStateFromDatabase();
|
||||||
@@ -1652,11 +1663,11 @@ public sealed class GameService : IGameService
|
|||||||
return m_UsersById.GetValueOrDefault(session.UserId);
|
return m_UsersById.GetValueOrDefault(session.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CampaignStateTracker GetOrCreateCampaignStateLocked(Guid campaignId)
|
private GameCampaignStateTracker GetOrCreateCampaignStateLocked(Guid campaignId)
|
||||||
{
|
{
|
||||||
if (!m_CampaignStateById.TryGetValue(campaignId, out var state))
|
if (!m_CampaignStateById.TryGetValue(campaignId, out var state))
|
||||||
{
|
{
|
||||||
state = new CampaignStateTracker();
|
state = new GameCampaignStateTracker();
|
||||||
m_CampaignStateById[campaignId] = state;
|
m_CampaignStateById[campaignId] = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1715,7 +1726,7 @@ public sealed class GameService : IGameService
|
|||||||
m_CampaignStateById.Clear();
|
m_CampaignStateById.Clear();
|
||||||
|
|
||||||
foreach (var campaignId in m_CampaignsById.Keys)
|
foreach (var campaignId in m_CampaignsById.Keys)
|
||||||
m_CampaignStateById[campaignId] = new CampaignStateTracker();
|
m_CampaignStateById[campaignId] = new GameCampaignStateTracker();
|
||||||
|
|
||||||
foreach (var character in m_CharactersById.Values.Where(character => character.CampaignId.HasValue))
|
foreach (var character in m_CharactersById.Values.Where(character => character.CampaignId.HasValue))
|
||||||
AddCharacterStateLocked(character.CampaignId, character.Id);
|
AddCharacterStateLocked(character.CampaignId, character.Id);
|
||||||
@@ -1723,91 +1734,14 @@ public sealed class GameService : IGameService
|
|||||||
|
|
||||||
private void LoadStateFromDatabase()
|
private void LoadStateFromDatabase()
|
||||||
{
|
{
|
||||||
using var db = m_DbContextFactory.CreateDbContext();
|
m_PersistenceService.LoadStateFromDatabase();
|
||||||
var users = db.Users.AsNoTracking().ToList();
|
|
||||||
var sessions = db.Sessions.AsNoTracking().ToList();
|
|
||||||
var campaigns = db.Campaigns.AsNoTracking().ToList();
|
|
||||||
var characters = db.Characters.AsNoTracking().ToList();
|
|
||||||
var skillGroups = db.SkillGroups.AsNoTracking().ToList();
|
|
||||||
var skills = db.Skills.AsNoTracking().ToList();
|
|
||||||
var logEntries = db.RollLogEntries.AsNoTracking().ToList().OrderBy(x => x.TimestampUtc).ThenBy(x => x.Id).ToList();
|
|
||||||
|
|
||||||
lock (m_Gate)
|
lock (m_Gate)
|
||||||
{
|
|
||||||
m_UsersById.Clear();
|
|
||||||
m_UserIdsByUsername.Clear();
|
|
||||||
m_SessionsByToken.Clear();
|
|
||||||
m_CampaignsById.Clear();
|
|
||||||
m_CharactersById.Clear();
|
|
||||||
m_SkillGroupsById.Clear();
|
|
||||||
m_SkillsById.Clear();
|
|
||||||
m_RollLog.Clear();
|
|
||||||
|
|
||||||
foreach (var user in users)
|
|
||||||
{
|
|
||||||
var normalizedUsername = string.IsNullOrWhiteSpace(user.UsernameNormalized) ? NormalizeUsername(user.Username) : user.UsernameNormalized;
|
|
||||||
|
|
||||||
var storedUser = new UserAccount
|
|
||||||
{
|
|
||||||
Id = user.Id,
|
|
||||||
Username = user.Username,
|
|
||||||
UsernameNormalized = normalizedUsername,
|
|
||||||
PasswordHash = user.PasswordHash,
|
|
||||||
DisplayName = user.DisplayName,
|
|
||||||
Roles = string.IsNullOrWhiteSpace(user.Roles) ? string.Empty : RoleSerializer.Serialize(RoleSerializer.Parse(user.Roles)),
|
|
||||||
ActiveCharacterId = user.ActiveCharacterId
|
|
||||||
};
|
|
||||||
m_UsersById[storedUser.Id] = storedUser;
|
|
||||||
|
|
||||||
m_UserIdsByUsername[normalizedUsername] = storedUser.Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var session in sessions)
|
|
||||||
{
|
|
||||||
if (m_UsersById.ContainsKey(session.UserId))
|
|
||||||
m_SessionsByToken[session.Token] = CloneSession(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var campaign in campaigns)
|
|
||||||
m_CampaignsById[campaign.Id] = CloneCampaign(campaign);
|
|
||||||
|
|
||||||
foreach (var character in characters)
|
|
||||||
m_CharactersById[character.Id] = CloneCharacter(character);
|
|
||||||
|
|
||||||
foreach (var skillGroup in skillGroups)
|
|
||||||
m_SkillGroupsById[skillGroup.Id] = CloneSkillGroup(skillGroup);
|
|
||||||
|
|
||||||
foreach (var skill in skills)
|
|
||||||
m_SkillsById[skill.Id] = CloneSkill(skill);
|
|
||||||
|
|
||||||
m_RollLog.AddRange(logEntries.Select(CloneRollLogEntry));
|
|
||||||
RebuildCampaignStateLocked();
|
RebuildCampaignStateLocked();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PersistStateLocked()
|
private void PersistStateLocked()
|
||||||
{
|
{
|
||||||
using var db = m_DbContextFactory.CreateDbContext();
|
m_PersistenceService.PersistStateLocked();
|
||||||
using var transaction = db.Database.BeginTransaction();
|
|
||||||
|
|
||||||
db.RollLogEntries.ExecuteDelete();
|
|
||||||
db.Skills.ExecuteDelete();
|
|
||||||
db.SkillGroups.ExecuteDelete();
|
|
||||||
db.Characters.ExecuteDelete();
|
|
||||||
db.Campaigns.ExecuteDelete();
|
|
||||||
db.Sessions.ExecuteDelete();
|
|
||||||
db.Users.ExecuteDelete();
|
|
||||||
|
|
||||||
db.Users.AddRange(m_UsersById.Values.Select(CloneUser));
|
|
||||||
db.Sessions.AddRange(m_SessionsByToken.Values.Select(CloneSession));
|
|
||||||
db.Campaigns.AddRange(m_CampaignsById.Values.Select(CloneCampaign));
|
|
||||||
db.Characters.AddRange(m_CharactersById.Values.Select(CloneCharacter));
|
|
||||||
db.SkillGroups.AddRange(m_SkillGroupsById.Values.Select(CloneSkillGroup));
|
|
||||||
db.Skills.AddRange(m_SkillsById.Values.Select(CloneSkill));
|
|
||||||
db.RollLogEntries.AddRange(m_RollLog.Select(CloneRollLogEntry));
|
|
||||||
|
|
||||||
db.SaveChanges();
|
|
||||||
transaction.Commit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string NormalizeUsername(string username)
|
private static string NormalizeUsername(string username)
|
||||||
@@ -1823,124 +1757,24 @@ public sealed class GameService : IGameService
|
|||||||
return Math.Clamp(limit.Value, 1, CampaignLogLivePageSize);
|
return Math.Clamp(limit.Value, 1, CampaignLogLivePageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static UserAccount CloneUser(UserAccount user)
|
|
||||||
{
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
Id = user.Id,
|
|
||||||
Username = user.Username,
|
|
||||||
UsernameNormalized = user.UsernameNormalized,
|
|
||||||
PasswordHash = user.PasswordHash,
|
|
||||||
DisplayName = user.DisplayName,
|
|
||||||
Roles = user.Roles,
|
|
||||||
ActiveCharacterId = user.ActiveCharacterId
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static UserSession CloneSession(UserSession session)
|
|
||||||
{
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
Token = session.Token,
|
|
||||||
UserId = session.UserId,
|
|
||||||
CreatedAtUtc = session.CreatedAtUtc
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Campaign CloneCampaign(Campaign campaign)
|
|
||||||
{
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
Id = campaign.Id,
|
|
||||||
GmUserId = campaign.GmUserId,
|
|
||||||
Name = campaign.Name,
|
|
||||||
Ruleset = campaign.Ruleset,
|
|
||||||
Version = campaign.Version
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Character CloneCharacter(Character character)
|
|
||||||
{
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
Id = character.Id,
|
|
||||||
OwnerUserId = character.OwnerUserId,
|
|
||||||
CampaignId = character.CampaignId,
|
|
||||||
Name = character.Name
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Skill CloneSkill(Skill skill)
|
|
||||||
{
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
Id = skill.Id,
|
|
||||||
CharacterId = skill.CharacterId,
|
|
||||||
SkillGroupId = skill.SkillGroupId,
|
|
||||||
Name = skill.Name,
|
|
||||||
DiceRollDefinition = skill.DiceRollDefinition,
|
|
||||||
WildDice = skill.WildDice,
|
|
||||||
AllowFumble = skill.AllowFumble,
|
|
||||||
FumbleRange = skill.FumbleRange
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SkillGroup CloneSkillGroup(SkillGroup skillGroup)
|
|
||||||
{
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
Id = skillGroup.Id,
|
|
||||||
CharacterId = skillGroup.CharacterId,
|
|
||||||
Name = skillGroup.Name,
|
|
||||||
DiceRollDefinition = skillGroup.DiceRollDefinition,
|
|
||||||
WildDice = skillGroup.WildDice,
|
|
||||||
AllowFumble = skillGroup.AllowFumble,
|
|
||||||
FumbleRange = skillGroup.FumbleRange
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static RollLogEntry CloneRollLogEntry(RollLogEntry entry)
|
|
||||||
{
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
Id = entry.Id,
|
|
||||||
CampaignId = entry.CampaignId,
|
|
||||||
CharacterId = entry.CharacterId,
|
|
||||||
SkillId = entry.SkillId,
|
|
||||||
RollerUserId = entry.RollerUserId,
|
|
||||||
Visibility = entry.Visibility,
|
|
||||||
Result = entry.Result,
|
|
||||||
Breakdown = entry.Breakdown,
|
|
||||||
Dice = entry.Dice,
|
|
||||||
TimestampUtc = entry.TimestampUtc
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private const int CampaignLogHistoryWindowSize = 100;
|
private const int CampaignLogHistoryWindowSize = 100;
|
||||||
private const int CampaignLogLivePageSize = 25;
|
private const int CampaignLogLivePageSize = 25;
|
||||||
private const string CustomRollBreakdownSeparator = " => ";
|
private const string CustomRollBreakdownSeparator = " => ";
|
||||||
private static readonly Guid CustomRollSkillId = Guid.Empty;
|
private static readonly Guid CustomRollSkillId = Guid.Empty;
|
||||||
private const string CustomRollLabel = "Custom roll";
|
private const string CustomRollLabel = "Custom roll";
|
||||||
private static readonly JsonSerializerOptions DiceJsonOptions = RpgRollerJson.CreateSerializerOptions();
|
private static readonly JsonSerializerOptions DiceJsonOptions = RpgRollerJson.CreateSerializerOptions();
|
||||||
private readonly Dictionary<Guid, Campaign> m_CampaignsById = [];
|
private readonly Dictionary<Guid, Campaign> m_CampaignsById;
|
||||||
private readonly Dictionary<Guid, CampaignStateTracker> m_CampaignStateById = [];
|
private readonly Dictionary<Guid, GameCampaignStateTracker> m_CampaignStateById;
|
||||||
private readonly Dictionary<Guid, Character> m_CharactersById = [];
|
private readonly Dictionary<Guid, Character> m_CharactersById;
|
||||||
private readonly IDbContextFactory<RpgRollerDbContext> m_DbContextFactory;
|
|
||||||
private readonly IDiceRoller m_DiceRoller;
|
private readonly IDiceRoller m_DiceRoller;
|
||||||
private readonly object m_Gate = new();
|
private readonly object m_Gate;
|
||||||
private readonly IPasswordHasher<UserAccount> m_PasswordHasher;
|
private readonly IPasswordHasher<UserAccount> m_PasswordHasher;
|
||||||
private readonly List<RollLogEntry> m_RollLog = [];
|
private readonly GamePersistenceService m_PersistenceService;
|
||||||
private readonly Dictionary<string, UserSession> m_SessionsByToken = new(StringComparer.Ordinal);
|
private readonly List<RollLogEntry> m_RollLog;
|
||||||
private readonly Dictionary<Guid, SkillGroup> m_SkillGroupsById = [];
|
private readonly Dictionary<string, UserSession> m_SessionsByToken;
|
||||||
private readonly Dictionary<Guid, Skill> m_SkillsById = [];
|
private readonly Dictionary<Guid, SkillGroup> m_SkillGroupsById;
|
||||||
private readonly Dictionary<string, Guid> m_UserIdsByUsername = new(StringComparer.Ordinal);
|
private readonly Dictionary<Guid, Skill> m_SkillsById;
|
||||||
private readonly Dictionary<Guid, UserAccount> m_UsersById = [];
|
private readonly GameStateStore m_StateStore;
|
||||||
|
private readonly Dictionary<string, Guid> m_UserIdsByUsername;
|
||||||
private sealed class CampaignStateTracker
|
private readonly Dictionary<Guid, UserAccount> m_UsersById;
|
||||||
{
|
|
||||||
public long TotalVersion { get; set; } = 1;
|
|
||||||
public long RosterVersion { get; set; } = 1;
|
|
||||||
public long LogVersion { get; set; } = 1;
|
|
||||||
public Dictionary<Guid, long> CharacterVersions { get; } = [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
99
RpgRoller/Services/GameStateCloneFactory.cs
Normal file
99
RpgRoller/Services/GameStateCloneFactory.cs
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
using RpgRoller.Domain;
|
||||||
|
|
||||||
|
namespace RpgRoller.Services;
|
||||||
|
|
||||||
|
public static class GameStateCloneFactory
|
||||||
|
{
|
||||||
|
public static UserAccount CloneUser(UserAccount user)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = user.Id,
|
||||||
|
Username = user.Username,
|
||||||
|
UsernameNormalized = user.UsernameNormalized,
|
||||||
|
PasswordHash = user.PasswordHash,
|
||||||
|
DisplayName = user.DisplayName,
|
||||||
|
Roles = user.Roles,
|
||||||
|
ActiveCharacterId = user.ActiveCharacterId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UserSession CloneSession(UserSession session)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Token = session.Token,
|
||||||
|
UserId = session.UserId,
|
||||||
|
CreatedAtUtc = session.CreatedAtUtc
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Campaign CloneCampaign(Campaign campaign)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = campaign.Id,
|
||||||
|
GmUserId = campaign.GmUserId,
|
||||||
|
Name = campaign.Name,
|
||||||
|
Ruleset = campaign.Ruleset,
|
||||||
|
Version = campaign.Version
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Character CloneCharacter(Character character)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = character.Id,
|
||||||
|
OwnerUserId = character.OwnerUserId,
|
||||||
|
CampaignId = character.CampaignId,
|
||||||
|
Name = character.Name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Skill CloneSkill(Skill skill)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = skill.Id,
|
||||||
|
CharacterId = skill.CharacterId,
|
||||||
|
SkillGroupId = skill.SkillGroupId,
|
||||||
|
Name = skill.Name,
|
||||||
|
DiceRollDefinition = skill.DiceRollDefinition,
|
||||||
|
WildDice = skill.WildDice,
|
||||||
|
AllowFumble = skill.AllowFumble,
|
||||||
|
FumbleRange = skill.FumbleRange
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SkillGroup CloneSkillGroup(SkillGroup skillGroup)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = skillGroup.Id,
|
||||||
|
CharacterId = skillGroup.CharacterId,
|
||||||
|
Name = skillGroup.Name,
|
||||||
|
DiceRollDefinition = skillGroup.DiceRollDefinition,
|
||||||
|
WildDice = skillGroup.WildDice,
|
||||||
|
AllowFumble = skillGroup.AllowFumble,
|
||||||
|
FumbleRange = skillGroup.FumbleRange
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RollLogEntry CloneRollLogEntry(RollLogEntry entry)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Id = entry.Id,
|
||||||
|
CampaignId = entry.CampaignId,
|
||||||
|
CharacterId = entry.CharacterId,
|
||||||
|
SkillId = entry.SkillId,
|
||||||
|
RollerUserId = entry.RollerUserId,
|
||||||
|
Visibility = entry.Visibility,
|
||||||
|
Result = entry.Result,
|
||||||
|
Breakdown = entry.Breakdown,
|
||||||
|
Dice = entry.Dice,
|
||||||
|
TimestampUtc = entry.TimestampUtc
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
25
RpgRoller/Services/GameStateStore.cs
Normal file
25
RpgRoller/Services/GameStateStore.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using RpgRoller.Domain;
|
||||||
|
|
||||||
|
namespace RpgRoller.Services;
|
||||||
|
|
||||||
|
public sealed class GameStateStore
|
||||||
|
{
|
||||||
|
public object Gate { get; } = new();
|
||||||
|
public Dictionary<Guid, Campaign> CampaignsById { get; } = [];
|
||||||
|
public Dictionary<Guid, GameCampaignStateTracker> CampaignStateById { get; } = [];
|
||||||
|
public Dictionary<Guid, Character> CharactersById { get; } = [];
|
||||||
|
public List<RollLogEntry> RollLog { get; } = [];
|
||||||
|
public Dictionary<string, UserSession> SessionsByToken { get; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<Guid, SkillGroup> SkillGroupsById { get; } = [];
|
||||||
|
public Dictionary<Guid, Skill> SkillsById { get; } = [];
|
||||||
|
public Dictionary<string, Guid> UserIdsByUsername { get; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<Guid, UserAccount> UsersById { get; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GameCampaignStateTracker
|
||||||
|
{
|
||||||
|
public long TotalVersion { get; set; } = 1;
|
||||||
|
public long RosterVersion { get; set; } = 1;
|
||||||
|
public long LogVersion { get; set; } = 1;
|
||||||
|
public Dictionary<Guid, long> CharacterVersions { get; } = [];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user