diff --git a/ClientSyncComponent/ClientSyncComponent.Client/JsPuzzleChange.cs b/ClientSyncComponent/ClientSyncComponent.Client/JsPuzzleChange.cs index 1b92257f..b7b949d5 100644 --- a/ClientSyncComponent/ClientSyncComponent.Client/JsPuzzleChange.cs +++ b/ClientSyncComponent/ClientSyncComponent.Client/JsPuzzleChange.cs @@ -18,5 +18,11 @@ public class JsPuzzleChange /// public string channel { get; set; } } + + public class JsPuzzleReset + { + public string[] puzzleIds { get; set; } + public string channel { get; set; } + } #pragma warning restore IDE1006 // Naming Styles } diff --git a/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor b/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor index 52cab1da..a9c02d1f 100644 --- a/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor +++ b/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor @@ -58,6 +58,10 @@ else puzzleFrame.contentWindow.postMessage({type: "puzzlesynced", changes: changes}, "*"); } + async function onPuzzleResetSynced (reset) { + puzzleFrame.contentWindow.postMessage({type: "puzzleresetsynced", reset: reset}, "*"); + } + async function onSetCoopMode(mode) { puzzleFrame.contentWindow.postMessage({ type: "setCoopMode", mode: mode }, "*"); } @@ -79,6 +83,9 @@ else else if (ev.data.type === "puzzlechanged") { await hostComponent.invokeMethodAsync("OnPuzzleChangedAsync", ev.data.changes); } + else if (ev.data.type === "puzzlereset") { + await hostComponent.invokeMethodAsync("OnPuzzleResetAsync", ev.data.changes); + } }); \ No newline at end of file diff --git a/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor.cs b/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor.cs index 3903be1a..331265c0 100644 --- a/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor.cs +++ b/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor.cs @@ -45,8 +45,6 @@ public partial class ClientSyncComponent TableClient TableClient { get; set; } - List ReceivedValues { get; set; } = new List(); - Timer Timer { get; set; } bool Paused { get; set; } = false; @@ -182,7 +180,23 @@ public async void OnPuzzleChangedAsync(JsPuzzleChange[] puzzleChanges) value: change.value, channel: change.channel); - await TableClient.UpsertEntityAsync(puzzleEntry); + await TableClient.UpsertEntityAsync(puzzleEntry, TableUpdateMode.Replace); + } + } + + [JSInvokable] + public async Task OnPuzzleResetAsync(JsPuzzleReset resets) + { + if (!SyncEnabled || Paused) + { + return; + } + + // Create a reset entry for each sub-puzzle + foreach (string subPuzzleId in resets.puzzleIds) + { + PuzzleItemProperty resetEntry = PuzzleItemProperty.CreateReset(PuzzleId, TeamId, Base64UrlTextEncoder.Encode(Encoding.UTF8.GetBytes(subPuzzleId)), PuzzleUserId, channel: resets.channel); + await TableClient.UpsertEntityAsync(resetEntry, TableUpdateMode.Replace); } } @@ -229,29 +243,77 @@ private async Task SyncAsync() { bool foundNewData = false; - var newChanges = TableClient.QueryAsync(entry => entry.PartitionKey == PuzzleItemProperty.CreatePartitionKey(PuzzleId, TeamId) && entry.Timestamp > LastSyncUtc); + var unsortedChanges = TableClient.QueryAsync(entry => entry.PartitionKey == PuzzleItemProperty.CreatePartitionKey(PuzzleId, TeamId) && entry.Timestamp > LastSyncUtc); - List jsChanges = new List(); - await foreach (PuzzleItemProperty entry in newChanges) + List newChanges = new List(); + await foreach (PuzzleItemProperty entry in unsortedChanges) + { + newChanges.Add(entry); + } + newChanges.Sort((a, b) => a.Timestamp?.CompareTo(b.Timestamp!.Value) ?? 0); + + // If a puzzle was reset, remove the earlier data for that puzzle to avoid it flashing in and then disappearing + + bool removedEntries = false; + do + { + removedEntries = false; + for (int i = 0; i < newChanges.Count; i++) + { + PuzzleItemProperty entry = newChanges[i]; + if (entry.IsReset) + { + // Remove all data entries for this sub-puzzle + int removedForReset = newChanges.RemoveAll(e => e.SubPuzzleId == entry.SubPuzzleId && + e.Timestamp < entry.Timestamp && !e.IsReset); + if (removedForReset > 0) + { + removedEntries = true; + break; + } + } + } + } while (removedEntries); + + List < JsPuzzleChange > jsChanges = new List(); + foreach (PuzzleItemProperty entry in newChanges) { foundNewData = true; - ReceivedValues.Add(entry.Value); - jsChanges.Add(new JsPuzzleChange() + if (entry.IsReset) + { + // If this is a reset, we need to send a reset message to the JS side + await JSRuntime.InvokeVoidAsync("onPuzzleResetSynced", new JsPuzzleReset() + { + puzzleIds = new[] { Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(entry.SubPuzzleId)) }, + channel = entry.Channel + }); + } + else { - locationKey = entry.LocationKey, - playerId = PuzzleUserId.ToString(), - propertyKey = entry.PropertyKey, - puzzleId = Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(entry.SubPuzzleId)), - teamId = TeamId.ToString(), - value = entry.Value - }); + jsChanges.Add(new JsPuzzleChange() + { + locationKey = entry.LocationKey, + playerId = PuzzleUserId.ToString(), + propertyKey = entry.PropertyKey, + puzzleId = Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(entry.SubPuzzleId)), + teamId = TeamId.ToString(), + value = entry.Value, + channel = entry.Channel + }); + + //morganb debug + await JSRuntime.InvokeVoidAsync("onPuzzleSynced", [jsChanges.ToArray()]); + jsChanges.Clear(); + } + LastSyncUtc = entry.Timestamp > LastSyncUtc ? entry.Timestamp.Value : LastSyncUtc; DisplayLastSyncUtc = entry.Timestamp > DisplayLastSyncUtc ? entry.Timestamp.Value : DisplayLastSyncUtc; } if (foundNewData) { - await JSRuntime.InvokeVoidAsync("onPuzzleSynced", [jsChanges.ToArray()]); + // morganb debug + //await JSRuntime.InvokeVoidAsync("onPuzzleSynced", [jsChanges.ToArray()]); await InvokeAsync(StateHasChanged); } } diff --git a/ClientSyncComponent/ClientSyncComponent.Client/PuzzleItemProperty.cs b/ClientSyncComponent/ClientSyncComponent.Client/PuzzleItemProperty.cs index 187b9080..6fd13443 100644 --- a/ClientSyncComponent/ClientSyncComponent.Client/PuzzleItemProperty.cs +++ b/ClientSyncComponent/ClientSyncComponent.Client/PuzzleItemProperty.cs @@ -12,7 +12,7 @@ public class PuzzleItemProperty : ITableEntity public PuzzleItemProperty() { } - + // todo morganb: translate from string player name to playerId public PuzzleItemProperty(int puzzleId, int teamId, int playerId, string subPuzzleId, string locationKey, string propertyKey, string value, string channel) { @@ -28,6 +28,14 @@ public PuzzleItemProperty(int puzzleId, int teamId, int playerId, string subPuzz Channel = channel; } + public static PuzzleItemProperty CreateReset(int puzzleId, int teamId, string subPuzzleId, int playerId, string channel) + { + return new PuzzleItemProperty(puzzleId, teamId, playerId, subPuzzleId, String.Empty, String.Empty, String.Empty, channel) + { + IsReset = true + }; + } + private static string CreateRowKey(string subPuzzleId, string locationKey, string propertyKey) { return $"{subPuzzleId}|{propertyKey}|{locationKey}"; @@ -49,6 +57,7 @@ public static string CreatePartitionKey(int puzzleId, int teamId) /// TODO: Implement channels beyond having the property /// public string Channel { get; set; } + public bool IsReset { get; set; } = false; // Set automatically by the Table service public DateTimeOffset? Timestamp { get; set; }