a42cc2a97e
ThemeCrossfadeSelfTestStep walks Switch -> crossfade-observed -> mid-crossfade-switch -> crossfade-end -> restore using TryGetActiveCrossfade, returns Waiting frame-by-frame and Pass after the restore concludes. The mid-switch phase fires a second Switch within ~100ms of the first observed crossfade and asserts the lerped value is neither identity-from nor identity-to, exercising the ArmCrossfade mid-flight-origin override. QuickPickerSelfTestStep verifies the three new resource strings, the built-in theme floor (>=10), and Config.Tabs non-empty.
217 lines
8.1 KiB
C#
217 lines
8.1 KiB
C#
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Plugin.SelfTest;
|
|
using HellionChat.Themes;
|
|
|
|
namespace HellionChat.SelfTests;
|
|
|
|
// Verifies the v1.5.4 PM-1 crossfade contract: switching the active
|
|
// theme arms TryGetActiveCrossfade for ~300ms, then the registry
|
|
// returns to direct AbgrCache reads. A second switch within 100ms
|
|
// keeps the lerped path active (no identity-snap). CleanUp restores
|
|
// the initial theme so /xlperf stays idempotent.
|
|
internal sealed class ThemeCrossfadeSelfTestStep : ISelfTestStep
|
|
{
|
|
private readonly Plugin plugin;
|
|
|
|
private string? initialSlug;
|
|
private string? targetSlug;
|
|
private string? midSwitchSlug;
|
|
private long armedAtTickMs = long.MinValue;
|
|
private long midArmedAtTickMs = long.MinValue;
|
|
private bool sawCrossfade;
|
|
private bool sawMidCrossfadeSwitch;
|
|
private bool sawCrossfadeEnd;
|
|
private bool restoredInitial;
|
|
|
|
public ThemeCrossfadeSelfTestStep(Plugin plugin)
|
|
{
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
public string Name => "Hellion Chat - Theme crossfade";
|
|
|
|
public SelfTestStepResult RunStep()
|
|
{
|
|
var registry = this.plugin.ThemeRegistry;
|
|
if (registry is null)
|
|
return SelfTestStepResult.Fail;
|
|
|
|
if (this.initialSlug is null)
|
|
{
|
|
this.initialSlug = registry.Active.Slug;
|
|
this.targetSlug = PickDifferentSlug(registry, this.initialSlug);
|
|
if (this.targetSlug is null)
|
|
{
|
|
ImGui.Text("Need at least two themes available; only one built-in found.");
|
|
return SelfTestStepResult.Fail;
|
|
}
|
|
|
|
registry.Switch(this.targetSlug);
|
|
this.armedAtTickMs = Environment.TickCount64;
|
|
ImGui.Text($"Crossfade armed: {this.initialSlug} -> {this.targetSlug}");
|
|
return SelfTestStepResult.Waiting;
|
|
}
|
|
|
|
if (!this.sawCrossfade)
|
|
{
|
|
if (registry.TryGetActiveCrossfade(out _))
|
|
{
|
|
this.sawCrossfade = true;
|
|
this.midArmedAtTickMs = Environment.TickCount64;
|
|
ImGui.Text("Crossfade observed mid-window, arming mid-switch test...");
|
|
return SelfTestStepResult.Waiting;
|
|
}
|
|
|
|
// If the window already closed before we observed it, that
|
|
// is acceptable only on extremely slow frame paths; accept
|
|
// it as "saw the start" if more than 300ms have elapsed.
|
|
// Skip the mid-crossfade-switch phase in that case -- the
|
|
// lerped path is no longer active, so a second switch would
|
|
// re-arm a fresh crossfade and not exercise PM-1b's
|
|
// mid-flight-origin override.
|
|
if (Environment.TickCount64 - this.armedAtTickMs > 300)
|
|
{
|
|
this.sawCrossfade = true;
|
|
this.sawMidCrossfadeSwitch = true;
|
|
this.sawCrossfadeEnd = true;
|
|
ImGui.Text("Crossfade window closed before observation; accepting.");
|
|
return SelfTestStepResult.Waiting;
|
|
}
|
|
|
|
return SelfTestStepResult.Waiting;
|
|
}
|
|
|
|
if (!this.sawMidCrossfadeSwitch)
|
|
{
|
|
// PM-Test-3 mid-crossfade-switch phase: within ~100ms of the
|
|
// first observed crossfade, fire a second Switch to a THIRD
|
|
// theme. ArmCrossfade must compose the current lerped state
|
|
// as the new origin -- TryGetActiveCrossfade still returns
|
|
// true (lerped path stays active, no identity-snap) and the
|
|
// lerped value is neither the identity-from nor the
|
|
// identity-to of the new switch (origin shifted to the
|
|
// mid-flight cache, target is the third theme).
|
|
if (Environment.TickCount64 - this.midArmedAtTickMs < 100)
|
|
{
|
|
this.midSwitchSlug = PickDifferentSlug(
|
|
registry,
|
|
[this.initialSlug!, this.targetSlug!]
|
|
);
|
|
if (this.midSwitchSlug is null)
|
|
{
|
|
// Only two themes available -- mid-switch phase cannot
|
|
// exercise the lerped-origin path. Accept and move on
|
|
// (the v1.5.3 baseline ships >=10 built-ins, so this
|
|
// branch is defensive).
|
|
this.sawMidCrossfadeSwitch = true;
|
|
ImGui.Text("Only two themes available; skipping mid-switch assert.");
|
|
return SelfTestStepResult.Waiting;
|
|
}
|
|
|
|
var fromCache = registry.Active.AbgrCache;
|
|
registry.Switch(this.midSwitchSlug);
|
|
var toCache = registry.Active.AbgrCache;
|
|
|
|
if (!registry.TryGetActiveCrossfade(out var midLerped))
|
|
{
|
|
ImGui.Text("Mid-switch failed: TryGetActiveCrossfade returned false.");
|
|
return SelfTestStepResult.Fail;
|
|
}
|
|
|
|
// Lerped value must be neither the new identity-from
|
|
// (target cache of the first switch) nor the new
|
|
// identity-to (third theme cache) -- it must originate
|
|
// from the mid-flight composed snapshot.
|
|
if (midLerped.Equals(fromCache) || midLerped.Equals(toCache))
|
|
{
|
|
ImGui.Text("Mid-switch failed: lerped value is an identity snap.");
|
|
return SelfTestStepResult.Fail;
|
|
}
|
|
|
|
this.sawMidCrossfadeSwitch = true;
|
|
ImGui.Text(
|
|
$"Mid-switch armed: {this.targetSlug} -> {this.midSwitchSlug} (lerped origin)."
|
|
);
|
|
return SelfTestStepResult.Waiting;
|
|
}
|
|
|
|
// Window for mid-switch already elapsed; accept and continue.
|
|
this.sawMidCrossfadeSwitch = true;
|
|
ImGui.Text("Mid-switch window elapsed before fire; accepting.");
|
|
return SelfTestStepResult.Waiting;
|
|
}
|
|
|
|
if (!this.sawCrossfadeEnd)
|
|
{
|
|
if (!registry.TryGetActiveCrossfade(out _))
|
|
{
|
|
this.sawCrossfadeEnd = true;
|
|
ImGui.Text("Crossfade window closed cleanly.");
|
|
return SelfTestStepResult.Waiting;
|
|
}
|
|
return SelfTestStepResult.Waiting;
|
|
}
|
|
|
|
if (!this.restoredInitial)
|
|
{
|
|
registry.Switch(this.initialSlug);
|
|
this.restoredInitial = true;
|
|
ImGui.Text($"Restored: {this.initialSlug}");
|
|
return SelfTestStepResult.Waiting;
|
|
}
|
|
|
|
// Wait for the restore-crossfade to also conclude before
|
|
// declaring Pass, so /xlperf does not flicker out mid-fade.
|
|
if (registry.TryGetActiveCrossfade(out _))
|
|
return SelfTestStepResult.Waiting;
|
|
|
|
return SelfTestStepResult.Pass;
|
|
}
|
|
|
|
public void CleanUp()
|
|
{
|
|
// Best-effort: if anything went sideways, snap back to the
|
|
// initial slug. Switch is idempotent on same-slug.
|
|
var registry = this.plugin.ThemeRegistry;
|
|
if (registry is not null && this.initialSlug is not null)
|
|
{
|
|
registry.Switch(this.initialSlug);
|
|
}
|
|
|
|
this.initialSlug = null;
|
|
this.targetSlug = null;
|
|
this.midSwitchSlug = null;
|
|
this.armedAtTickMs = long.MinValue;
|
|
this.midArmedAtTickMs = long.MinValue;
|
|
this.sawCrossfade = false;
|
|
this.sawMidCrossfadeSwitch = false;
|
|
this.sawCrossfadeEnd = false;
|
|
this.restoredInitial = false;
|
|
}
|
|
|
|
private static string? PickDifferentSlug(ThemeRegistry registry, string activeSlug) =>
|
|
PickDifferentSlug(registry, [activeSlug]);
|
|
|
|
private static string? PickDifferentSlug(
|
|
ThemeRegistry registry,
|
|
IReadOnlyCollection<string> excludeSlugs
|
|
)
|
|
{
|
|
foreach (var theme in registry.AllBuiltIns())
|
|
{
|
|
var match = false;
|
|
foreach (var excluded in excludeSlugs)
|
|
{
|
|
if (string.Equals(theme.Slug, excluded, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
match = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!match)
|
|
return theme.Slug;
|
|
}
|
|
return null;
|
|
}
|
|
}
|