Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions apps/utility-css/src/Services/PracticeLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@ public class PracticeLibrary
private readonly Dictionary<ulong, List<LineupRecord>> _lineups = new();
private string _map = "";

// What this server changed in a player's library itself -- a .save, a
// .delete -- and when, by client id. A fetch that went out before the
// change, or before the panel had it, answers with the library as it was;
// applied as-is it dropped the lineup somebody had just saved, or brought
// back the one they had just deleted. Kept is null for a removal.
private sealed class LocalChange
{
public DateTime At;
public LineupRecord? Kept;
}

private readonly Dictionary<ulong, Dictionary<string, LocalChange>> _local = new();

// How long a local change outranks the panel's answer once a fetch has
// started after it. A save is one round trip away from being on the panel;
// past this, a lineup missing from the answer is one the website removed.
private static readonly TimeSpan LocalHold = TimeSpan.FromMinutes(2);

public PracticeLibrary(UtilityApiClient api, ILogger<PracticeLibrary> logger)
{
_api = api;
Expand All @@ -33,6 +51,7 @@ public void SetMap(string map)

_map = map;
_lineups.Clear();
_local.Clear();
}

public IReadOnlyList<LineupRecord> For(ulong steamId)
Expand All @@ -57,6 +76,7 @@ public void Add(ulong steamId, LineupRecord lineup)

lineups.RemoveAll(existing => existing.client_id == lineup.client_id);
lineups.Add(lineup);
Touch(steamId, lineup.client_id, lineup);
}

public void Remove(ulong steamId, LineupRecord lineup)
Expand All @@ -65,6 +85,92 @@ public void Remove(ulong steamId, LineupRecord lineup)
{
lineups.RemoveAll(existing => existing.client_id == lineup.client_id);
}

Touch(steamId, lineup.client_id, null);
}

private void Touch(ulong steamId, string clientId, LineupRecord? kept)
{
if (!_local.TryGetValue(steamId, out Dictionary<string, LocalChange>? changes))
{
changes = new Dictionary<string, LocalChange>();
_local[steamId] = changes;
}

changes[clientId] = new LocalChange { At = DateTime.UtcNow, Kept = kept };
}

// The panel's answer, with this server's own changes laid back over it
// while the answer cannot have seen them yet. A change is forgotten once
// an answer agrees with it, or once it is old enough that disagreeing
// means somebody else changed it since.
private List<LineupRecord> MergeLocal(ulong steamId, DateTime startedAt, List<LineupRecord> answer)
{
if (!_local.TryGetValue(steamId, out Dictionary<string, LocalChange>? changes))
{
return answer;
}

DateTime now = DateTime.UtcNow;
var merged = new List<LineupRecord>(answer);

foreach ((string clientId, LocalChange change) in changes.ToList())
{
int index = merged.FindIndex(row => row.client_id == clientId);

// Made after the request left: the answer cannot know about it.
bool newer = change.At >= startedAt;
bool holding = newer || now - change.At < LocalHold;

if (change.Kept == null)
{
if (index < 0)
{
if (!newer)
{
changes.Remove(clientId);
}
}
else if (holding)
{
merged.RemoveAt(index);
}
else
{
changes.Remove(clientId);
}

continue;
}

if (index >= 0)
{
if (newer)
{
merged[index] = change.Kept;
}
else
{
changes.Remove(clientId);
}

continue;
}

// Not on the panel yet. One with no id never reached it at all --
// it is waiting in the retry queue -- so it stays for as long as it
// stays in this server's hands.
if (holding || string.IsNullOrEmpty(change.Kept.id))
{
merged.Add(change.Kept);
}
else
{
changes.Remove(clientId);
}
}

return merged;
}

// A library row carries no flight path and no measured bloom, so neither
Expand Down Expand Up @@ -103,6 +209,7 @@ public void EnsureTrajectory(LineupRecord lineup, ulong steamId, Action<LineupRe
public void Refresh(ulong steamId, Action<int>? done = null)
{
string map = _map;
DateTime startedAt = DateTime.UtcNow;

_ = Task.Run(async () =>
{
Expand All @@ -124,6 +231,8 @@ public void Refresh(ulong steamId, Action<int>? done = null)
return;
}

lineups = MergeLocal(steamId, startedAt, lineups);

_lineups[steamId] = lineups;
done?.Invoke(lineups.Count);
});
Expand Down
66 changes: 51 additions & 15 deletions apps/utility-css/src/Services/PracticeReplay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -461,19 +461,12 @@ private void ShowMarkers(LineupRecord lineup)
Vec3 stance = lineup.release.feet_position;
Vec3 landing = lineup.detonation_position;

// No text on the stance or over the landing: the rings and their
// colour already say it, and words hung in the air above the smoke
// only got in the way of seeing it. The name lives at the aim point,
// where it tells several throws off one spot apart.
Ring(stance, 18f, color, 1.5f);
Label(
new Vec3(stance.x, stance.y, stance.z + 12f),
$"STAND\n{lineup.name}",
color
);

Ring(landing, 26f, color, 2f);
Label(
new Vec3(landing.x, landing.y, landing.z + 16f),
lineup.utility_type.ToUpperInvariant(),
color
);

// Where to look, placed along the recorded aim at the distance the
// throw actually travelled, so it sits on the thing being aimed at
Expand All @@ -494,8 +487,41 @@ private void ShowMarkers(LineupRecord lineup)
eye.z + (float)(-Math.Sin(pitch)) * reach
);

Label(aim, "AIM", color);
Ring(aim, 10f, color, 1f);
// Sized by distance from the eye so it covers the same slice of
// the view at any range. A fixed 10-unit ring was fine far off but
// swallowed the screen on a short throw, with nothing in it to
// pinpoint; past ~220 units it is the same ring as before.
float away = new Vec3(aim.x - eye.x, aim.y - eye.y, aim.z - eye.z).Length();
float radius = Math.Clamp(away * 0.045f, 0.25f, 10f);
float width = Math.Clamp(away * 0.0045f, 0.02f, 1f);

Ring(aim, radius, color, width);

// Shrinks with the ring up close so the name never covers the
// point it is naming. Unchanged from ~220 units out.
float labelScale = Math.Min(1f, radius / 10f);
Label(
new Vec3(aim.x, aim.y, aim.z + radius + 6f * labelScale),
lineup.name,
color,
0.15f * labelScale
);

// The exact point, so there is something precise to cover.
float dot = Math.Max(radius * 0.08f, 0.05f);

AddMarkerBeam(
new Vec3(aim.x - dot, aim.y, aim.z + 2f),
new Vec3(aim.x + dot, aim.y, aim.z + 2f),
color,
width * 1.6f
);
AddMarkerBeam(
new Vec3(aim.x, aim.y - dot, aim.z + 2f),
new Vec3(aim.x, aim.y + dot, aim.z + 2f),
color,
width * 1.6f
);
}
}

Expand Down Expand Up @@ -530,7 +556,17 @@ private void Ring(Vec3 center, float radius, Color color, float width)
}
}

private void Label(Vec3 at, string text, Color color)
private void AddMarkerBeam(Vec3 start, Vec3 end, Color color, float width)
{
CEnvBeam? beam = CreateBeam(start, end, color, width);

if (beam != null)
{
_markerBeams.Add(beam);
}
}

private void Label(Vec3 at, string text, Color color, float unitsPerPx)
{
try
{
Expand All @@ -548,7 +584,7 @@ private void Label(Vec3 at, string text, Color color)
label.FontSize = 60;
label.FontName = "Arial Black";
label.Fullbright = true;
label.WorldUnitsPerPx = 0.15f;
label.WorldUnitsPerPx = unitsPerPx;
label.Enabled = true;
label.JustifyHorizontal = PointWorldTextJustifyHorizontal_t
.POINT_WORLD_TEXT_JUSTIFY_HORIZONTAL_CENTER;
Expand Down
34 changes: 34 additions & 0 deletions apps/utility-css/src/UtilityPracticePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,13 @@ public override void Unload(bool hotReload)
_playbook.Reset();
_drill.Reset();
_system.Reset();

// One last try at what the panel has not taken yet: it lives only in
// this instance's memory. Not waited for -- this is the game thread.
if (_api.QueuedCount > 0)
{
_ = Task.Run(() => _api.Drain());
}
}

private void OnSecond()
Expand All @@ -158,6 +165,33 @@ private void OnSecond()
// A no-op once the map has answered; see MapCalloutsReporter.Report.
_callouts.Report(_session.Map);
DrainPendingMapLoad();
DrainQueued();
}

// The retry queue only used to move when something else got through -- a
// later save, a scored throw, a map change -- so a panel that came back
// while nobody was saving left everything queued until the next one.
private const int DrainEverySeconds = 30;

private int _drainTicks;

private void DrainQueued()
{
if (++_drainTicks < DrainEverySeconds)
{
return;
}

_drainTicks = 0;

if (_api.QueuedCount == 0)
{
return;
}

// Off the game thread like every other call to the panel; Drain lets
// only one pass run at a time, so a slow one is never stacked on.
_ = Task.Run(() => _api.Drain());
}

// Nobody stays dead on a practice server. Rejoining while dead, falling off
Expand Down
Loading
Loading