From 3f773581e2ee818131fada65d49b3af63d9e849e Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Fri, 11 Jul 2025 20:05:17 -0700 Subject: [PATCH 1/3] Progress towards syncing reset. Changes don't sync correctly afterwards --- .../JsPuzzleChange.cs | 6 +++ .../Pages/ClientSyncComponent.razor | 12 +++++ .../Pages/ClientSyncComponent.razor.cs | 54 ++++++++++++++----- .../PuzzleItemProperty.cs | 11 +++- 4 files changed, 70 insertions(+), 13 deletions(-) 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..9ce75bcb 100644 --- a/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor +++ b/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor @@ -58,6 +58,11 @@ else puzzleFrame.contentWindow.postMessage({type: "puzzlesynced", changes: changes}, "*"); } + async function onPuzzleResetSynced (reset) { + console.log ("got onPuzzleResetSynced", reset) + puzzleFrame.contentWindow.postMessage({type: "puzzleresetsynced", reset: reset}, "*"); + } + async function onSetCoopMode(mode) { puzzleFrame.contentWindow.postMessage({ type: "setCoopMode", mode: mode }, "*"); } @@ -69,6 +74,7 @@ else let puzzleFrame = null; window.addEventListener("visibilitychange", onVisibilityChangeAsync); window.addEventListener("message", async (ev) => { + console.log("got message", ev); if (!puzzleLoaded && ev.data.type === "puzzleLoad") { savedMode = ev.data.mode; console.debug(`got puzzle load message with mode ${savedMode}`); @@ -77,8 +83,14 @@ else sendSyncComponentLoad(savedMode); } else if (ev.data.type === "puzzlechanged") { + console.log("got puzzle changed message outside frame", ev.data.changes); await hostComponent.invokeMethodAsync("OnPuzzleChangedAsync", ev.data.changes); } + else if (ev.data.type === "puzzlereset") { + console.log("got puzzle reset message", ev.data.changes); + + 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..e0bb936c 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; @@ -165,6 +163,7 @@ public async void OnSyncablePuzzleLoadedAsync(string mode) [JSInvokable] public async void OnPuzzleChangedAsync(JsPuzzleChange[] puzzleChanges) { + Console.WriteLine($"PuzzleChangedAsync. SyncEnabled: {SyncEnabled} Paused: {Paused}"); if (!SyncEnabled || Paused) { return; @@ -182,7 +181,25 @@ public async void OnPuzzleChangedAsync(JsPuzzleChange[] puzzleChanges) value: change.value, channel: change.channel); - await TableClient.UpsertEntityAsync(puzzleEntry); + Console.WriteLine($"Upserting puzzle entry: {change.puzzleId} {puzzleEntry.PartitionKey} {puzzleEntry.RowKey} {puzzleEntry.Value}"); + + 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); } } @@ -235,16 +252,29 @@ private async Task SyncAsync() await foreach (PuzzleItemProperty entry in newChanges) { foundNewData = true; - ReceivedValues.Add(entry.Value); - jsChanges.Add(new JsPuzzleChange() + if (entry.IsReset) { - locationKey = entry.LocationKey, - playerId = PuzzleUserId.ToString(), - propertyKey = entry.PropertyKey, - puzzleId = Encoding.UTF8.GetString(Base64UrlTextEncoder.Decode(entry.SubPuzzleId)), - teamId = TeamId.ToString(), - value = entry.Value - }); + // 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 + { + 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 + }); + } + LastSyncUtc = entry.Timestamp > LastSyncUtc ? entry.Timestamp.Value : LastSyncUtc; DisplayLastSyncUtc = entry.Timestamp > DisplayLastSyncUtc ? entry.Timestamp.Value : DisplayLastSyncUtc; } 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; } From 93f328ea4e4c9319e18483e6d33b6964bc0874c4 Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 11 Aug 2025 22:17:55 -0700 Subject: [PATCH 2/3] Fix ordering and duplication --- .../Pages/ClientSyncComponent.razor.cs | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor.cs b/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor.cs index e0bb936c..81888b65 100644 --- a/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor.cs +++ b/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor.cs @@ -246,14 +246,46 @@ 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); + Console.WriteLine($"Removed {removedForReset} entries for reset of {entry.SubPuzzleId}"); + if (removedForReset > 0) + { + removedEntries = true; + break; + } + } + } + } while (removedEntries); + + List < JsPuzzleChange > jsChanges = new List(); + foreach (PuzzleItemProperty entry in newChanges) { foundNewData = true; if (entry.IsReset) { + Console.WriteLine($"Incoming reset for {entry.SubPuzzleId}"); // If this is a reset, we need to send a reset message to the JS side await JSRuntime.InvokeVoidAsync("onPuzzleResetSynced", new JsPuzzleReset() { @@ -263,6 +295,7 @@ private async Task SyncAsync() } else { + Console.WriteLine($"Incoming puzzle change for {entry.SubPuzzleId}"); jsChanges.Add(new JsPuzzleChange() { locationKey = entry.LocationKey, @@ -273,6 +306,10 @@ private async Task SyncAsync() value = entry.Value, channel = entry.Channel }); + + //morganb debug + await JSRuntime.InvokeVoidAsync("onPuzzleSynced", [jsChanges.ToArray()]); + jsChanges.Clear(); } LastSyncUtc = entry.Timestamp > LastSyncUtc ? entry.Timestamp.Value : LastSyncUtc; @@ -281,7 +318,8 @@ private async Task SyncAsync() if (foundNewData) { - await JSRuntime.InvokeVoidAsync("onPuzzleSynced", [jsChanges.ToArray()]); + // morganb debug + //await JSRuntime.InvokeVoidAsync("onPuzzleSynced", [jsChanges.ToArray()]); await InvokeAsync(StateHasChanged); } } From 7ea49d984530ba5db8326d2661b6891bd15fc27f Mon Sep 17 00:00:00 2001 From: Morgan Brown Date: Mon, 8 Sep 2025 19:30:26 -0700 Subject: [PATCH 3/3] Remove debug code --- .../Pages/ClientSyncComponent.razor | 5 ----- .../Pages/ClientSyncComponent.razor.cs | 6 ------ 2 files changed, 11 deletions(-) diff --git a/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor b/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor index 9ce75bcb..a9c02d1f 100644 --- a/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor +++ b/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor @@ -59,7 +59,6 @@ else } async function onPuzzleResetSynced (reset) { - console.log ("got onPuzzleResetSynced", reset) puzzleFrame.contentWindow.postMessage({type: "puzzleresetsynced", reset: reset}, "*"); } @@ -74,7 +73,6 @@ else let puzzleFrame = null; window.addEventListener("visibilitychange", onVisibilityChangeAsync); window.addEventListener("message", async (ev) => { - console.log("got message", ev); if (!puzzleLoaded && ev.data.type === "puzzleLoad") { savedMode = ev.data.mode; console.debug(`got puzzle load message with mode ${savedMode}`); @@ -83,12 +81,9 @@ else sendSyncComponentLoad(savedMode); } else if (ev.data.type === "puzzlechanged") { - console.log("got puzzle changed message outside frame", ev.data.changes); await hostComponent.invokeMethodAsync("OnPuzzleChangedAsync", ev.data.changes); } else if (ev.data.type === "puzzlereset") { - console.log("got puzzle reset message", ev.data.changes); - await hostComponent.invokeMethodAsync("OnPuzzleResetAsync", ev.data.changes); } }); diff --git a/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor.cs b/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor.cs index 81888b65..331265c0 100644 --- a/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor.cs +++ b/ClientSyncComponent/ClientSyncComponent.Client/Pages/ClientSyncComponent.razor.cs @@ -163,7 +163,6 @@ public async void OnSyncablePuzzleLoadedAsync(string mode) [JSInvokable] public async void OnPuzzleChangedAsync(JsPuzzleChange[] puzzleChanges) { - Console.WriteLine($"PuzzleChangedAsync. SyncEnabled: {SyncEnabled} Paused: {Paused}"); if (!SyncEnabled || Paused) { return; @@ -181,8 +180,6 @@ public async void OnPuzzleChangedAsync(JsPuzzleChange[] puzzleChanges) value: change.value, channel: change.channel); - Console.WriteLine($"Upserting puzzle entry: {change.puzzleId} {puzzleEntry.PartitionKey} {puzzleEntry.RowKey} {puzzleEntry.Value}"); - await TableClient.UpsertEntityAsync(puzzleEntry, TableUpdateMode.Replace); } } @@ -269,7 +266,6 @@ private async Task SyncAsync() // Remove all data entries for this sub-puzzle int removedForReset = newChanges.RemoveAll(e => e.SubPuzzleId == entry.SubPuzzleId && e.Timestamp < entry.Timestamp && !e.IsReset); - Console.WriteLine($"Removed {removedForReset} entries for reset of {entry.SubPuzzleId}"); if (removedForReset > 0) { removedEntries = true; @@ -285,7 +281,6 @@ private async Task SyncAsync() foundNewData = true; if (entry.IsReset) { - Console.WriteLine($"Incoming reset for {entry.SubPuzzleId}"); // If this is a reset, we need to send a reset message to the JS side await JSRuntime.InvokeVoidAsync("onPuzzleResetSynced", new JsPuzzleReset() { @@ -295,7 +290,6 @@ private async Task SyncAsync() } else { - Console.WriteLine($"Incoming puzzle change for {entry.SubPuzzleId}"); jsChanges.Add(new JsPuzzleChange() { locationKey = entry.LocationKey,