Editor UI activity recorder for the editor watchdog. It tracks bounded observations of UI state (stage, level, picker, scroll, map positions) and records time-stamped actions into a circular buffer, coalescing continuous movement events.
using System;
namespace BlockParty;
/// <summary>Bounded UI observations for the editor watchdog. Recording never formats strings or logs.</summary>
internal sealed class EditorUiActivity
{
internal struct State
{
public bool Known, Loading, PickerOpen, PickerObserved, PickerGrid;
public string Level, Stage;
public int Rows, First, Last, MapPreviews, SearchLength;
public float Scroll, PickerX, PickerY, MapX, MapY;
public double Observed, PickerObservedAt;
public string Describe( double at ) => !Known ? "uiContext=none" :
$"uiContext=[stage={Stage} level='{Level}' loading={Loading} rows={Rows} visible=[{First},{Last}) scroll={Scroll:0.###} " +
(Stage == "LevelSelectStage" ? $"mapCamera=({MapX:0.###},{MapY:0.###}) " : "") +
$"observedAgo={Math.Max( 0, at - Observed ):0.000}s pickerOpen={PickerOpen}" +
(!PickerOpen ? "]" : !PickerObserved ? " pickerLayout=pending]" :
$" pickerView={(PickerGrid ? "search-grid" : "map")} pickerScroll=({PickerX:0.###},{PickerY:0.###}) " +
$"mapPreviews={MapPreviews} searchLength={SearchLength} pickerObservedAgo={Math.Max( 0, at - PickerObservedAt ):0.000}s]");
}
internal struct Entry
{
public double FirstAt, LastAt;
public string Action;
public int Count;
public float Input;
public State Context;
public string Describe() => $"+{FirstAt:0.000}..{LastAt:0.000}s action={Action} count={Count} input={Input:0.###} " + Context.Describe( LastAt );
}
private readonly Entry[] _entries = new Entry[64];
private int _next;
private object _owner;
private State _state;
public int Count { get; private set; }
public State Current => _state;
public Entry Oldest( int index ) => _entries[(_next - Count + index + _entries.Length) % _entries.Length];
public void ClearState() { _state = default; _owner = null; }
public void Board( object owner, double now, string level, bool loading, bool pickerOpen,
int rows, int first, int last, float scroll, string action = null, float input = 0,
string stage = "HighscoreStage", float mapX = 0, float mapY = 0 )
{
if ( !ReferenceEquals( owner, _owner ) || _state.Stage != stage ) { _state = default; _owner = owner; }
bool moved = _state.Known && (int)_state.Scroll != (int)scroll;
bool mapMoved = _state.Known && ((int)_state.MapX != (int)mapX || (int)_state.MapY != (int)mapY);
if ( pickerOpen != _state.PickerOpen ) _state.PickerObserved = false;
_state.Known = true;
_state.Stage = stage;
_state.MapX = mapX;
_state.MapY = mapY;
_state.Observed = now;
_state.Level = level;
_state.Loading = loading;
_state.PickerOpen = pickerOpen;
_state.Rows = rows;
_state.First = first;
_state.Last = last;
_state.Scroll = scroll;
if ( action is not null ) Record( now, action, input );
else
{
if ( moved ) Record( now, stage == "LevelSelectStage" ? "footer-scroll" : "leaderboard-scroll" );
if ( mapMoved ) Record( now, "map-pan" );
}
}
public void Picker( double now, bool grid, float x, float y, int previews, int searchLength, string action = null )
{
if ( !_state.Known || !_state.PickerOpen ) return;
bool first = !_state.PickerObserved;
bool moved = (int)x != (int)_state.PickerX || (int)y != (int)_state.PickerY;
bool viewChanged = grid != _state.PickerGrid;
bool previewsChanged = previews != _state.MapPreviews;
_state.PickerObserved = true;
_state.PickerObservedAt = now;
_state.PickerGrid = grid;
_state.PickerX = x;
_state.PickerY = y;
_state.MapPreviews = previews;
_state.SearchLength = searchLength;
if ( action is not null ) Record( now, action );
else if ( first || viewChanged ) Record( now, "picker-layout" );
else if ( moved ) Record( now, "picker-scroll" );
else if ( previewsChanged ) Record( now, "picker-previews" );
}
private void Record( double now, string action, float input = 0 )
{
int previous = (_next - 1 + _entries.Length) % _entries.Length;
// Coalesce only continuous movement, never repeated clicks or board requests.
bool movement = action == "leaderboard-scroll" || action == "leaderboard-wheel" || action == "picker-scroll"
|| action == "footer-scroll" || action == "footer-wheel" || action == "map-pan";
if ( movement && Count > 0 && _entries[previous].Action == action
&& _entries[previous].Context.Stage == _state.Stage
&& _entries[previous].Context.Level == _state.Level && now - _entries[previous].LastAt < 0.25 )
{
_entries[previous].LastAt = now;
_entries[previous].Context = _state;
_entries[previous].Input = input;
_entries[previous].Count++;
return;
}
_entries[_next] = new Entry { FirstAt = now, LastAt = now, Action = action, Count = 1, Input = input, Context = _state };
_next = (_next + 1) % _entries.Length;
Count = Math.Min( Count + 1, _entries.Length );
}
}