Load campaign logs incrementally
This commit is contained in:
@@ -101,7 +101,7 @@ dotnet dotnet-ef migrations add <MigrationName> --project RpgRoller/RpgRoller.cs
|
||||
- Browser interop is in `RpgRoller/wwwroot/js/rpgroller-api.js`.
|
||||
- Workspace reads are resolved server-side through scoped query services; browser interop remains for browser-only concerns such as session storage, SSE wiring, and DOM helpers.
|
||||
- Live workspace refreshes now compare separate roster, per-character sheet, and log versions so unrelated state changes do not force a full roster + sheet + log reload.
|
||||
- Workspace campaign data is loaded in bounded slices: visible campaign summaries, a selected campaign roster, a selected character sheet, and the 100 most recent visible log entries.
|
||||
- Workspace campaign data is loaded in bounded slices: visible campaign summaries, a selected campaign roster, a selected character sheet, and an incremental log window backed by `/api/campaigns/{campaignId}/log/page`.
|
||||
- OpenAPI contract source remains at `openapi/RpgRoller.json`.
|
||||
|
||||
## Test and Coverage
|
||||
|
||||
@@ -309,4 +309,46 @@ public sealed class CampaignApiTests : ApiTestBase
|
||||
Assert.False(string.IsNullOrWhiteSpace(entry.RollerDisplayName));
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CampaignLogPage_ReturnsInitialAndIncrementalResults()
|
||||
{
|
||||
using var factory = CreateFactory();
|
||||
using var gmClient = factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||
using var playerClient = factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||
|
||||
await RegisterAsync(gmClient, "gm-log-page", "Password123", "GM");
|
||||
await LoginAsync(gmClient, "gm-log-page", "Password123");
|
||||
|
||||
await RegisterAsync(playerClient, "player-log-page", "Password123", "Player");
|
||||
await LoginAsync(playerClient, "player-log-page", "Password123");
|
||||
|
||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Log Page", "d6"));
|
||||
var character = await PostAsync<CreateCharacterRequest, CharacterSummary>(playerClient, "/api/characters", new("Roller", campaign.Id));
|
||||
var skill = await PostAsync<CreateSkillRequest, SkillSummary>(playerClient, $"/api/characters/{character.Id}/skills", new("Stealth", "2D+1", 1, true));
|
||||
|
||||
var rollIds = new List<Guid>();
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
var roll = await PostAsync<RollSkillRequest, RollResult>(playerClient, $"/api/skills/{skill.Id}/roll", new("public"));
|
||||
rollIds.Add(roll.RollId);
|
||||
}
|
||||
|
||||
var initialPage = await GetAsync<CampaignLogPage>(gmClient, $"/api/campaigns/{campaign.Id}/log/page?limit=3");
|
||||
Assert.Equal(3, initialPage.Entries.Count);
|
||||
Assert.Equal(rollIds[2], initialPage.Entries[0].RollId);
|
||||
Assert.Equal(rollIds[^1], initialPage.Entries[^1].RollId);
|
||||
Assert.Equal(rollIds[^1], initialPage.Cursor);
|
||||
Assert.True(initialPage.HasMore);
|
||||
Assert.False(initialPage.ResetRequired);
|
||||
|
||||
var latestRoll = await PostAsync<RollSkillRequest, RollResult>(playerClient, $"/api/skills/{skill.Id}/roll", new("public"));
|
||||
var incrementalPage = await GetAsync<CampaignLogPage>(gmClient, $"/api/campaigns/{campaign.Id}/log/page?afterRollId={initialPage.Cursor}&limit=3");
|
||||
|
||||
Assert.Single(incrementalPage.Entries);
|
||||
Assert.Equal(latestRoll.RollId, incrementalPage.Entries[0].RollId);
|
||||
Assert.Equal(latestRoll.RollId, incrementalPage.Cursor);
|
||||
Assert.False(incrementalPage.HasMore);
|
||||
Assert.False(incrementalPage.ResetRequired);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,11 @@ public sealed class RollVisibilityApiTests : ApiTestBase
|
||||
Assert.Equal("public", observerLog[0].Visibility);
|
||||
Assert.NotEmpty(observerLog[0].Dice);
|
||||
|
||||
var observerLogPage = await GetAsync<CampaignLogPage>(observerClient, $"/api/campaigns/{campaign.Id}/log/page");
|
||||
Assert.Single(observerLogPage.Entries);
|
||||
Assert.Equal(publicRoll.RollId, observerLogPage.Entries[0].RollId);
|
||||
Assert.Equal(publicRoll.RollId, observerLogPage.Cursor);
|
||||
|
||||
await RegisterAsync(outsiderClient, "outsider", "Password123", "Outsider");
|
||||
await LoginAsync(outsiderClient, "outsider", "Password123");
|
||||
|
||||
|
||||
@@ -74,4 +74,72 @@ public sealed class ServiceSkillRollTests
|
||||
Assert.False(missingState.Succeeded);
|
||||
Assert.True(ServiceTestSupport.GetValue(state).LogVersion > 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CampaignLogPage_ReturnsInitialWindowAndIncrementalAppend()
|
||||
{
|
||||
using var harness = ServiceTestSupport.CreateHarness(6, 5, 4, 3, 2, 6, 5, 4, 3, 2, 6, 5);
|
||||
var service = harness.Service;
|
||||
|
||||
service.Register("gm-page", "Password123", "GM");
|
||||
service.Register("owner-page", "Password123", "Owner");
|
||||
|
||||
var gmSession = ServiceTestSupport.GetValue(service.Login("gm-page", "Password123")).SessionToken;
|
||||
var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-page", "Password123")).SessionToken;
|
||||
|
||||
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Paged", "d6"));
|
||||
var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Hero", campaign.Id));
|
||||
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Stealth", "2D+1", 1, true));
|
||||
|
||||
var rollIds = new List<Guid>();
|
||||
for (var i = 0; i < 5; i++)
|
||||
rollIds.Add(ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public")).RollId);
|
||||
|
||||
var initialPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, limit: 3));
|
||||
Assert.Equal(3, initialPage.Entries.Count);
|
||||
Assert.Equal(rollIds[2], initialPage.Entries[0].RollId);
|
||||
Assert.Equal(rollIds[^1], initialPage.Entries[^1].RollId);
|
||||
Assert.Equal(rollIds[^1], initialPage.Cursor);
|
||||
Assert.True(initialPage.HasMore);
|
||||
Assert.False(initialPage.ResetRequired);
|
||||
|
||||
var latestRoll = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public"));
|
||||
var incrementalPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, initialPage.Cursor, 3));
|
||||
|
||||
Assert.Single(incrementalPage.Entries);
|
||||
Assert.Equal(latestRoll.RollId, incrementalPage.Entries[0].RollId);
|
||||
Assert.Equal(latestRoll.RollId, incrementalPage.Cursor);
|
||||
Assert.False(incrementalPage.HasMore);
|
||||
Assert.False(incrementalPage.ResetRequired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CampaignLogPage_RequestsResetWhenIncrementalGapExceedsLimit()
|
||||
{
|
||||
using var harness = ServiceTestSupport.CreateHarness(6, 5, 4, 3, 2, 6, 5, 4, 3, 2, 6, 5);
|
||||
var service = harness.Service;
|
||||
|
||||
service.Register("gm-gap", "Password123", "GM");
|
||||
service.Register("owner-gap", "Password123", "Owner");
|
||||
|
||||
var gmSession = ServiceTestSupport.GetValue(service.Login("gm-gap", "Password123")).SessionToken;
|
||||
var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-gap", "Password123")).SessionToken;
|
||||
|
||||
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Gap", "d6"));
|
||||
var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Hero", campaign.Id));
|
||||
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Stealth", "2D+1", 1, true));
|
||||
|
||||
var rollIds = new List<Guid>();
|
||||
for (var i = 0; i < 6; i++)
|
||||
rollIds.Add(ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public")).RollId);
|
||||
|
||||
var gapPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, rollIds[0], 3));
|
||||
|
||||
Assert.True(gapPage.ResetRequired);
|
||||
Assert.True(gapPage.HasMore);
|
||||
Assert.Equal(3, gapPage.Entries.Count);
|
||||
Assert.Equal(rollIds[3], gapPage.Entries[0].RollId);
|
||||
Assert.Equal(rollIds[^1], gapPage.Entries[^1].RollId);
|
||||
Assert.Equal(rollIds[^1], gapPage.Cursor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ public sealed class WorkspaceQueryServiceTests
|
||||
public ServiceResult<CharacterSheet> GetCharacterSheet(string sessionToken, Guid characterId) => throw new NotSupportedException();
|
||||
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility) => throw new NotSupportedException();
|
||||
public ServiceResult<IReadOnlyList<CampaignLogEntry>> GetCampaignLog(string sessionToken, Guid campaignId) => throw new NotSupportedException();
|
||||
public ServiceResult<CampaignLogPage> GetCampaignLogPage(string sessionToken, Guid campaignId, Guid? afterRollId = null, int? limit = null) => throw new NotSupportedException();
|
||||
public ServiceResult<CampaignStateSnapshot> GetCampaignStateSnapshot(string sessionToken, Guid campaignId) => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,12 @@ internal static class CampaignEndpoints
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
group.MapGet("/campaigns/{campaignId:guid}/log/page", (Guid campaignId, Guid? afterRollId, int? limit, HttpContext context, IGameService game) =>
|
||||
{
|
||||
var result = game.GetCampaignLogPage(context.GetRequiredSessionToken(), campaignId, afterRollId, limit);
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
group.MapDelete("/campaigns/{campaignId:guid}", (Guid campaignId, HttpContext context, IGameService game) =>
|
||||
{
|
||||
var result = game.DeleteCampaign(context.GetRequiredSessionToken(), campaignId);
|
||||
|
||||
@@ -10,13 +10,15 @@ public partial class CampaignLogPanel
|
||||
{
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
var currentLastRollId = CampaignLog.LastOrDefault()?.RollId;
|
||||
if (IsCampaignDataLoading || CampaignLog.Count == 0)
|
||||
{
|
||||
LastRenderedLogCount = CampaignLog.Count;
|
||||
LastRenderedLogRollId = currentLastRollId;
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstRender || CampaignLog.Count > LastRenderedLogCount)
|
||||
if (firstRender || CampaignLog.Count > LastRenderedLogCount || currentLastRollId != LastRenderedLogRollId)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -31,6 +33,7 @@ public partial class CampaignLogPanel
|
||||
}
|
||||
|
||||
LastRenderedLogCount = CampaignLog.Count;
|
||||
LastRenderedLogRollId = currentLastRollId;
|
||||
}
|
||||
|
||||
[Inject]
|
||||
@@ -38,6 +41,7 @@ public partial class CampaignLogPanel
|
||||
|
||||
private ElementReference LogPanelRef { get; set; }
|
||||
private int LastRenderedLogCount { get; set; }
|
||||
private Guid? LastRenderedLogRollId { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool IsCampaignDataLoading { get; set; }
|
||||
|
||||
@@ -171,15 +171,28 @@ public partial class Workspace : IAsyncDisposable
|
||||
await EnsureSelectedCharacterActiveAsync();
|
||||
}
|
||||
|
||||
private async Task RefreshCampaignLogAsync()
|
||||
private async Task RefreshCampaignLogAsync(Guid? afterRollId = null)
|
||||
{
|
||||
if (!SelectedCampaignId.HasValue || !IsPlayScreen)
|
||||
{
|
||||
CampaignLog = [];
|
||||
CampaignLogCursor = null;
|
||||
return;
|
||||
}
|
||||
|
||||
CampaignLog = (await WorkspaceQuery.GetCampaignLogAsync(SelectedCampaignId.Value)).ToList();
|
||||
var page = await WorkspaceQuery.GetCampaignLogPageAsync(SelectedCampaignId.Value, afterRollId, CampaignLogWindowSize);
|
||||
if (!afterRollId.HasValue || page.ResetRequired)
|
||||
{
|
||||
CampaignLog = page.Entries.ToList();
|
||||
}
|
||||
else if (page.Entries.Count > 0)
|
||||
{
|
||||
CampaignLog.AddRange(page.Entries);
|
||||
if (CampaignLog.Count > CampaignLogWindowSize)
|
||||
CampaignLog = CampaignLog.TakeLast(CampaignLogWindowSize).ToList();
|
||||
}
|
||||
|
||||
CampaignLogCursor = page.Cursor ?? afterRollId;
|
||||
}
|
||||
|
||||
private async Task RefreshCampaignScopeAsync()
|
||||
@@ -193,6 +206,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
SelectedCharacterId = null;
|
||||
ConnectionState = "offline";
|
||||
CurrentCampaignState = null;
|
||||
CampaignLogCursor = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -661,7 +675,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
{
|
||||
LastRoll = await ApiClient.RequestAsync<RollResult>("POST", $"/api/skills/{skillId}/roll", new RollSkillRequest(RollVisibility));
|
||||
|
||||
await RefreshCampaignLogAsync();
|
||||
await RefreshCampaignLogAsync(CampaignLogCursor);
|
||||
ResetCampaignStateTracking();
|
||||
SetStatus("Roll recorded.", false);
|
||||
Announce("Roll result updated.");
|
||||
@@ -760,7 +774,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
await RefreshSelectedCharacterSheetAsync();
|
||||
|
||||
if (logChanged)
|
||||
await RefreshCampaignLogAsync();
|
||||
await RefreshCampaignLogAsync(CampaignLogCursor);
|
||||
|
||||
CurrentCampaignState = state;
|
||||
}
|
||||
@@ -936,6 +950,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
SelectedCharacterSkills = [];
|
||||
SelectedCharacterSkillGroups = [];
|
||||
CampaignLog = [];
|
||||
CampaignLogCursor = null;
|
||||
SelectedCharacterId = null;
|
||||
LastRoll = null;
|
||||
KnownUsernames = [];
|
||||
@@ -1059,6 +1074,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
private bool HasInteractiveRenderStarted { get; set; }
|
||||
private DotNetObjectReference<Workspace>? DotNetRef { get; set; }
|
||||
private CampaignStateSnapshot? CurrentCampaignState { get; set; }
|
||||
private Guid? CampaignLogCursor { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string?> LoggedOut { get; set; }
|
||||
@@ -1194,6 +1210,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
private const string CampaignSessionKey = "campaign";
|
||||
private const string MobilePanelSessionKey = "play-panel";
|
||||
private const string RollVisibilitySessionKey = "roll-visibility";
|
||||
private const int CampaignLogWindowSize = 100;
|
||||
private const int ToastDurationMs = 3200;
|
||||
|
||||
private sealed record WorkspaceToast(Guid Id, string Message, bool IsError);
|
||||
|
||||
@@ -51,6 +51,11 @@ public sealed class WorkspaceQueryService
|
||||
return Task.FromResult(GetValue(m_GameService.GetCampaignLog(GetRequiredSessionToken(), campaignId)));
|
||||
}
|
||||
|
||||
public Task<CampaignLogPage> GetCampaignLogPageAsync(Guid campaignId, Guid? afterRollId = null, int? limit = null)
|
||||
{
|
||||
return Task.FromResult(GetValue(m_GameService.GetCampaignLogPage(GetRequiredSessionToken(), campaignId, afterRollId, limit)));
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<AdminUserSummary>> GetAdminUsersAsync()
|
||||
{
|
||||
return Task.FromResult(GetValue(m_GameService.GetUsers(GetRequiredSessionToken())));
|
||||
|
||||
@@ -57,3 +57,5 @@ public sealed record CampaignLogEntry(Guid RollId, Guid CampaignId, Guid Charact
|
||||
public sealed record CharacterStateVersion(Guid CharacterId, long Version);
|
||||
|
||||
public sealed record CampaignStateSnapshot(Guid CampaignId, long TotalVersion, long RosterVersion, long LogVersion, IReadOnlyList<CharacterStateVersion> CharacterVersions);
|
||||
|
||||
public sealed record CampaignLogPage(IReadOnlyList<CampaignLogEntry> Entries, Guid? Cursor, bool HasMore, bool ResetRequired);
|
||||
|
||||
@@ -796,14 +796,8 @@ public sealed class GameService : IGameService
|
||||
return ServiceResult<IReadOnlyList<CampaignLogEntry>>.Failure(context.Error!.Code, context.Error.Message);
|
||||
|
||||
var (user, campaign) = context.Value!;
|
||||
var entries = m_RollLog
|
||||
.Where(r => r.CampaignId == campaign.Id)
|
||||
.Where(r => r.Visibility == RollVisibility.Public || r.RollerUserId == user.Id || campaign.GmUserId == user.Id)
|
||||
.OrderByDescending(r => r.TimestampUtc)
|
||||
.ThenByDescending(r => r.Id)
|
||||
.Take(CampaignLogPageSize)
|
||||
.OrderBy(r => r.TimestampUtc)
|
||||
.ThenBy(r => r.Id)
|
||||
var entries = GetVisibleCampaignLogEntriesLocked(user, campaign)
|
||||
.TakeLast(CampaignLogPageSize)
|
||||
.Select(ToLogEntry)
|
||||
.ToArray();
|
||||
|
||||
@@ -811,6 +805,46 @@ public sealed class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<CampaignLogPage> GetCampaignLogPage(string sessionToken, Guid campaignId, Guid? afterRollId = null, int? limit = null)
|
||||
{
|
||||
lock (m_Gate)
|
||||
{
|
||||
var context = ResolveContextLocked(sessionToken, campaignId);
|
||||
if (!context.Succeeded)
|
||||
return ServiceResult<CampaignLogPage>.Failure(context.Error!.Code, context.Error.Message);
|
||||
|
||||
var (user, campaign) = context.Value!;
|
||||
var pageSize = NormalizeCampaignLogPageSize(limit);
|
||||
var visibleEntries = GetVisibleCampaignLogEntriesLocked(user, campaign).ToArray();
|
||||
|
||||
if (!afterRollId.HasValue)
|
||||
{
|
||||
var initialEntries = visibleEntries.TakeLast(pageSize).Select(ToLogEntry).ToArray();
|
||||
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(initialEntries, initialEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, false));
|
||||
}
|
||||
|
||||
var afterIndex = Array.FindIndex(visibleEntries, entry => entry.Id == afterRollId.Value);
|
||||
if (afterIndex < 0)
|
||||
{
|
||||
var replacementEntries = visibleEntries.TakeLast(pageSize).Select(ToLogEntry).ToArray();
|
||||
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(replacementEntries, replacementEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, true));
|
||||
}
|
||||
|
||||
var newEntries = visibleEntries.Skip(afterIndex + 1).ToArray();
|
||||
if (newEntries.Length == 0)
|
||||
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage([], afterRollId, false, false));
|
||||
|
||||
if (newEntries.Length > pageSize)
|
||||
{
|
||||
var replacementEntries = newEntries.TakeLast(pageSize).Select(ToLogEntry).ToArray();
|
||||
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(replacementEntries, replacementEntries[^1].RollId, true, true));
|
||||
}
|
||||
|
||||
var appendedEntries = newEntries.Select(ToLogEntry).ToArray();
|
||||
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(appendedEntries, appendedEntries[^1].RollId, false, false));
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<CampaignStateSnapshot> GetCampaignStateSnapshot(string sessionToken, Guid campaignId)
|
||||
{
|
||||
lock (m_Gate)
|
||||
@@ -1070,6 +1104,15 @@ public sealed class GameService : IGameService
|
||||
return new CampaignStateSnapshot(campaign.Id, state.TotalVersion, state.RosterVersion, state.LogVersion, characterVersions);
|
||||
}
|
||||
|
||||
private IEnumerable<RollLogEntry> GetVisibleCampaignLogEntriesLocked(UserAccount user, Campaign campaign)
|
||||
{
|
||||
return m_RollLog
|
||||
.Where(r => r.CampaignId == campaign.Id)
|
||||
.Where(r => r.Visibility == RollVisibility.Public || r.RollerUserId == user.Id || campaign.GmUserId == user.Id)
|
||||
.OrderBy(r => r.TimestampUtc)
|
||||
.ThenBy(r => r.Id);
|
||||
}
|
||||
|
||||
private static SkillGroupSummary ToSkillGroupSummary(SkillGroup skillGroup)
|
||||
{
|
||||
return new(skillGroup.Id, skillGroup.CharacterId, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble);
|
||||
@@ -1437,6 +1480,14 @@ public sealed class GameService : IGameService
|
||||
return username.ToUpperInvariant();
|
||||
}
|
||||
|
||||
private static int NormalizeCampaignLogPageSize(int? limit)
|
||||
{
|
||||
if (!limit.HasValue)
|
||||
return CampaignLogPageSize;
|
||||
|
||||
return Math.Clamp(limit.Value, 1, CampaignLogPageSize);
|
||||
}
|
||||
|
||||
private static UserAccount CloneUser(UserAccount user)
|
||||
{
|
||||
return new()
|
||||
|
||||
@@ -38,6 +38,7 @@ public interface IGameService
|
||||
|
||||
ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility);
|
||||
ServiceResult<IReadOnlyList<CampaignLogEntry>> GetCampaignLog(string sessionToken, Guid campaignId);
|
||||
ServiceResult<CampaignLogPage> GetCampaignLogPage(string sessionToken, Guid campaignId, Guid? afterRollId = null, int? limit = null);
|
||||
|
||||
ServiceResult<CampaignStateSnapshot> GetCampaignStateSnapshot(string sessionToken, Guid campaignId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user