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/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/GameStateStore.cs`, `GameStateCloneFactory.cs`, and `GamePersistenceService.cs`: extracted runtime-state ownership and SQLite load/save boundaries used by `GameService`
|
||||
|
||||
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)
|
||||
{
|
||||
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_DiceRoller = diceRoller;
|
||||
LoadStateFromDatabase();
|
||||
@@ -1652,11 +1663,11 @@ public sealed class GameService : IGameService
|
||||
return m_UsersById.GetValueOrDefault(session.UserId);
|
||||
}
|
||||
|
||||
private CampaignStateTracker GetOrCreateCampaignStateLocked(Guid campaignId)
|
||||
private GameCampaignStateTracker GetOrCreateCampaignStateLocked(Guid campaignId)
|
||||
{
|
||||
if (!m_CampaignStateById.TryGetValue(campaignId, out var state))
|
||||
{
|
||||
state = new CampaignStateTracker();
|
||||
state = new GameCampaignStateTracker();
|
||||
m_CampaignStateById[campaignId] = state;
|
||||
}
|
||||
|
||||
@@ -1715,7 +1726,7 @@ public sealed class GameService : IGameService
|
||||
m_CampaignStateById.Clear();
|
||||
|
||||
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))
|
||||
AddCharacterStateLocked(character.CampaignId, character.Id);
|
||||
@@ -1723,91 +1734,14 @@ public sealed class GameService : IGameService
|
||||
|
||||
private 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();
|
||||
|
||||
m_PersistenceService.LoadStateFromDatabase();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
private 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_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();
|
||||
m_PersistenceService.PersistStateLocked();
|
||||
}
|
||||
|
||||
private static string NormalizeUsername(string username)
|
||||
@@ -1823,124 +1757,24 @@ public sealed class GameService : IGameService
|
||||
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 CampaignLogLivePageSize = 25;
|
||||
private const string CustomRollBreakdownSeparator = " => ";
|
||||
private static readonly Guid CustomRollSkillId = Guid.Empty;
|
||||
private const string CustomRollLabel = "Custom roll";
|
||||
private static readonly JsonSerializerOptions DiceJsonOptions = RpgRollerJson.CreateSerializerOptions();
|
||||
private readonly Dictionary<Guid, Campaign> m_CampaignsById = [];
|
||||
private readonly Dictionary<Guid, CampaignStateTracker> m_CampaignStateById = [];
|
||||
private readonly Dictionary<Guid, Character> m_CharactersById = [];
|
||||
private readonly IDbContextFactory<RpgRollerDbContext> m_DbContextFactory;
|
||||
private readonly Dictionary<Guid, Campaign> m_CampaignsById;
|
||||
private readonly Dictionary<Guid, GameCampaignStateTracker> m_CampaignStateById;
|
||||
private readonly Dictionary<Guid, Character> m_CharactersById;
|
||||
private readonly IDiceRoller m_DiceRoller;
|
||||
private readonly object m_Gate = new();
|
||||
private readonly object m_Gate;
|
||||
private readonly IPasswordHasher<UserAccount> m_PasswordHasher;
|
||||
private readonly List<RollLogEntry> m_RollLog = [];
|
||||
private readonly Dictionary<string, UserSession> m_SessionsByToken = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<Guid, SkillGroup> m_SkillGroupsById = [];
|
||||
private readonly Dictionary<Guid, Skill> m_SkillsById = [];
|
||||
private readonly Dictionary<string, Guid> m_UserIdsByUsername = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<Guid, UserAccount> m_UsersById = [];
|
||||
|
||||
private sealed class CampaignStateTracker
|
||||
{
|
||||
public long TotalVersion { get; set; } = 1;
|
||||
public long RosterVersion { get; set; } = 1;
|
||||
public long LogVersion { get; set; } = 1;
|
||||
public Dictionary<Guid, long> CharacterVersions { get; } = [];
|
||||
}
|
||||
private readonly GamePersistenceService m_PersistenceService;
|
||||
private readonly List<RollLogEntry> m_RollLog;
|
||||
private readonly Dictionary<string, UserSession> m_SessionsByToken;
|
||||
private readonly Dictionary<Guid, SkillGroup> m_SkillGroupsById;
|
||||
private readonly Dictionary<Guid, Skill> m_SkillsById;
|
||||
private readonly GameStateStore m_StateStore;
|
||||
private readonly Dictionary<string, Guid> m_UserIdsByUsername;
|
||||
private readonly Dictionary<Guid, UserAccount> m_UsersById;
|
||||
}
|
||||
|
||||
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