diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
index 6350da4aa..b2a11d774 100644
--- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs
+++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
@@ -290,6 +290,8 @@ namespace Dalamud.Configuration.Internal
///
public bool ShowTsm { get; set; } = true;
+ public bool Fools22 { get; set; } = true;
+
///
/// Load a configuration from the provided path.
///
diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs
index 537ffa516..4ad19e5fa 100644
--- a/Dalamud/Dalamud.cs
+++ b/Dalamud/Dalamud.cs
@@ -53,6 +53,8 @@ namespace Dalamud
#endregion
+ private Fools22? fools22;
+
///
/// Initializes a new instance of the class.
///
@@ -270,6 +272,15 @@ namespace Dalamud
Troubleshooting.LogTroubleshooting();
+ try
+ {
+ this.fools22 = new Fools22();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Fools22 load failed.");
+ }
+
Log.Information("Dalamud is ready.");
}
catch (Exception ex)
@@ -360,6 +371,8 @@ namespace Dalamud
this.processMonoHook?.Dispose();
+ this.fools22?.Dispose();
+
Log.Debug("Dalamud::Dispose() OK!");
}
catch (Exception ex)
diff --git a/Dalamud/Fools22.cs b/Dalamud/Fools22.cs
new file mode 100644
index 000000000..c384f3641
--- /dev/null
+++ b/Dalamud/Fools22.cs
@@ -0,0 +1,315 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Media;
+using System.Numerics;
+
+using Dalamud.Configuration.Internal;
+using Dalamud.Data;
+using Dalamud.Game;
+using Dalamud.Game.ClientState.Conditions;
+using Dalamud.Game.Gui;
+using Dalamud.Game.Text;
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.Interface;
+using Dalamud.Interface.Animation;
+using Dalamud.Interface.Animation.EasingFunctions;
+using Dalamud.Interface.Internal;
+using Dalamud.Utility;
+using ImGuiNET;
+using ImGuiScene;
+using Lumina.Excel.GeneratedSheets;
+using Serilog;
+using Condition = Dalamud.Game.ClientState.Conditions.Condition;
+
+namespace Dalamud;
+
+public class Fools22 : IDisposable
+{
+ private readonly TextureWrap erDeathBgTexture;
+ private readonly TextureWrap erNormalDeathTexture;
+ private readonly TextureWrap erCraftFailedTexture;
+
+ private readonly string synthesisFailsMessage;
+
+ private readonly Stopwatch time = new Stopwatch();
+ private readonly SoundPlayer player;
+
+ private bool assetsReady = false;
+
+ private AnimationState currentState = AnimationState.NotPlaying;
+ private DeathType currentDeathType = DeathType.Death;
+
+ private Easing alphaEasing;
+ private Easing scaleEasing;
+
+ private bool lastFrameUnconscious = false;
+
+ private int msFadeInTime = 1000;
+ private int msFadeOutTime = 2000;
+ private int msWaitTime = 1600;
+
+ private TextureWrap TextTexture => this.currentDeathType switch
+ {
+ DeathType.Death => this.erNormalDeathTexture,
+ DeathType.CraftFailed => this.erCraftFailedTexture,
+ };
+
+ private enum AnimationState
+ {
+ NotPlaying,
+ FadeIn,
+ Wait,
+ FadeOut,
+ }
+
+ private enum DeathType
+ {
+ Death,
+ CraftFailed,
+ }
+
+ public Fools22()
+ {
+ var dalamud = Service.Get();
+ var interfaceManager = Service.Get();
+ var framework = Service.Get();
+ var chatGui = Service.Get();
+ var dataMgr = Service.Get();
+
+ this.erDeathBgTexture = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "fools22", "er_death_bg.png"))!;
+ this.erNormalDeathTexture = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "fools22", "er_normal_death.png"))!;
+ this.erCraftFailedTexture = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "fools22", "er_craft_failed.png"))!;
+
+ var soundBytes = File.ReadAllBytes(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "fools22", "snd_death_er.wav"));
+ this.player = new SoundPlayer(new MemoryStream(soundBytes));
+
+ if (this.erDeathBgTexture == null || this.erNormalDeathTexture == null || this.erCraftFailedTexture == null)
+ {
+ Log.Error("Fools22: Failed to load images");
+ return;
+ }
+
+ this.synthesisFailsMessage = dataMgr.GetExcelSheet()!.GetRow(1160)!.Text.ToDalamudString().TextValue;
+
+ this.assetsReady = true;
+
+ interfaceManager.Draw += this.Draw;
+ framework.Update += this.FrameworkOnUpdate;
+ chatGui.ChatMessage += this.ChatGuiOnChatMessage;
+ }
+
+ private void ChatGuiOnChatMessage(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool ishandled)
+ {
+ if (message.TextValue.Contains(this.synthesisFailsMessage))
+ {
+ this.PlayAnimation(DeathType.CraftFailed);
+ Log.Information("Fools22: Craft failed");
+ }
+ }
+
+ private void FrameworkOnUpdate(Framework framework)
+ {
+ var condition = Service.Get();
+ var isUnconscious = condition[ConditionFlag.Unconscious];
+
+ if (isUnconscious && !this.lastFrameUnconscious)
+ {
+ this.PlayAnimation(DeathType.Death);
+ Log.Information("Fools22: Player died");
+ }
+
+ this.lastFrameUnconscious = isUnconscious;
+ }
+
+ private void Draw()
+ {
+#if DEBUG
+ if (ImGui.Begin("fools test"))
+ {
+ if (ImGui.Button("play death"))
+ {
+ this.PlayAnimation(DeathType.Death);
+ }
+
+ if (ImGui.Button("play craft failed"))
+ {
+ this.PlayAnimation(DeathType.CraftFailed);
+ }
+
+ ImGui.InputInt("fade in time", ref this.msFadeInTime);
+ ImGui.InputInt("fade out time", ref this.msFadeOutTime);
+ ImGui.InputInt("wait time", ref this.msWaitTime);
+
+ ImGui.TextUnformatted("state: " + this.currentState);
+ ImGui.TextUnformatted("time: " + this.time.ElapsedMilliseconds);
+
+ ImGui.TextUnformatted("scale: " + this.scaleEasing?.EasedPoint.X);
+ }
+
+ ImGui.End();
+#endif
+ var vpSize = ImGuiHelpers.MainViewport.Size;
+
+ ImGui.SetNextWindowPos(new Vector2(0, 0), ImGuiCond.Always);
+ ImGui.SetNextWindowSize(new Vector2(vpSize.X, vpSize.Y), ImGuiCond.Always);
+ ImGuiHelpers.ForceNextWindowMainViewport();
+
+ ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0);
+ ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0);
+ ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0, 0));
+ ImGui.PushStyleColor(ImGuiCol.WindowBg, new Vector4(0, 0, 0, 0));
+ ImGui.PushStyleColor(ImGuiCol.Border, new Vector4(0, 0, 0, 0));
+ ImGui.PushStyleColor(ImGuiCol.BorderShadow, new Vector4(0, 0, 0, 0));
+
+ if (ImGui.Begin("fools22", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoNavFocus
+ | ImGuiWindowFlags.NoMouseInputs | ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoScrollbar))
+ {
+ if (this.currentState != AnimationState.NotPlaying)
+ {
+ this.alphaEasing?.Update();
+ this.scaleEasing?.Update();
+ }
+
+ switch (this.currentState)
+ {
+ case AnimationState.FadeIn:
+ this.FadeIn(vpSize);
+ break;
+ case AnimationState.Wait:
+ this.Wait(vpSize);
+ break;
+ case AnimationState.FadeOut:
+ this.FadeOut(vpSize);
+ break;
+ }
+ }
+
+ ImGui.End();
+
+ ImGui.PopStyleColor(3);
+ ImGui.PopStyleVar(3);
+ }
+
+ private static void AdjustCursorAndDraw(Vector2 vpSize, TextureWrap tex, float scale = 1.0f)
+ {
+ var width = vpSize.X;
+ var height = tex.Height / (float)tex.Width * width;
+
+ if (height < vpSize.Y)
+ {
+ height = vpSize.Y;
+ width = tex.Width / (float)tex.Height * height;
+ ImGui.SetCursorPosX((vpSize.X - width) / 2);
+ }
+ else
+ {
+ ImGui.SetCursorPosY((vpSize.Y - height) / 2);
+ }
+
+ var scaledSize = new Vector2(width, height) * scale;
+ var difference = scaledSize - vpSize;
+
+ var cursor = ImGui.GetCursorPos();
+ ImGui.SetCursorPos(cursor - (difference / 2));
+
+ ImGui.Image(tex.ImGuiHandle, scaledSize);
+ }
+
+ private void FadeIn(Vector2 vpSize)
+ {
+ if (this.time.ElapsedMilliseconds > this.msFadeInTime)
+ this.currentState = AnimationState.Wait;
+
+ ImGui.PushStyleVar(ImGuiStyleVar.Alpha, (float)this.alphaEasing.Value);
+
+ AdjustCursorAndDraw(vpSize, this.erDeathBgTexture);
+ AdjustCursorAndDraw(vpSize, this.TextTexture, this.scaleEasing.EasedPoint.X);
+
+ ImGui.PopStyleVar();
+ }
+
+ private void Wait(Vector2 vpSize)
+ {
+ if (this.time.ElapsedMilliseconds > this.msFadeInTime + this.msWaitTime)
+ {
+ this.currentState = AnimationState.FadeOut;
+ this.alphaEasing = new InOutCubic(TimeSpan.FromMilliseconds(this.msFadeOutTime));
+ this.alphaEasing.Start();
+ }
+
+ AdjustCursorAndDraw(vpSize, this.erDeathBgTexture);
+ AdjustCursorAndDraw(vpSize, this.TextTexture, this.scaleEasing.EasedPoint.X);
+ }
+
+ private void FadeOut(Vector2 vpSize)
+ {
+ if (this.time.ElapsedMilliseconds > this.msFadeInTime + this.msWaitTime + this.msFadeOutTime)
+ {
+ this.currentState = AnimationState.NotPlaying;
+ this.time.Stop();
+ }
+
+ ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1 - (float)this.alphaEasing.Value);
+
+ AdjustCursorAndDraw(vpSize, this.erDeathBgTexture);
+ AdjustCursorAndDraw(vpSize, this.TextTexture, this.scaleEasing.EasedPoint.X);
+
+ ImGui.PopStyleVar();
+ }
+
+ private void PlayAnimation(DeathType type)
+ {
+ if (!this.CheckFoolsApplicable())
+ return;
+
+ if (this.currentState != AnimationState.NotPlaying)
+ return;
+
+ this.currentDeathType = type;
+
+ this.currentState = AnimationState.FadeIn;
+ this.alphaEasing = new InOutCubic(TimeSpan.FromMilliseconds(this.msFadeInTime));
+ this.alphaEasing.Start();
+
+ this.scaleEasing = new OutCubic(TimeSpan.FromMilliseconds(this.msFadeInTime + this.msWaitTime + this.msFadeOutTime))
+ {
+ Point1 = new Vector2(0.95f, 0.95f),
+ Point2 = new Vector2(1.05f, 1.05f),
+ };
+ this.scaleEasing.Start();
+
+ this.time.Reset();
+ this.time.Start();
+
+ this.player.Play();
+ }
+
+ private bool CheckFoolsApplicable()
+ {
+ var config = Service.Get();
+
+ if (!config.Fools22)
+ return false;
+
+ if (!(DateTime.Now.Month == 4 && DateTime.Now.Day == 1))
+ return false;
+
+ var timeZone = TimeZoneInfo.Local;
+ var offset = timeZone.GetUtcOffset(DateTime.UtcNow);
+
+ Log.Information("Fools22: UTC offset: {0}", offset);
+
+ return this.assetsReady;
+ }
+
+ public void Dispose()
+ {
+ this.erDeathBgTexture.Dispose();
+ this.erNormalDeathTexture.Dispose();
+ this.erCraftFailedTexture.Dispose();
+
+ this.player.Dispose();
+ }
+
+}
diff --git a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs
index 686839d0f..9ff94f42e 100644
--- a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs
@@ -49,6 +49,8 @@ namespace Dalamud.Interface.Internal.Windows
private bool doFocus;
private bool doTsm;
+ private bool doFools22;
+
private List? dtrOrder;
private List? dtrIgnore;
private int dtrSpacing;
@@ -104,6 +106,8 @@ namespace Dalamud.Interface.Internal.Windows
this.doFocus = configuration.IsFocusManagementEnabled;
this.doTsm = configuration.ShowTsm;
+ this.doFools22 = configuration.Fools22;
+
this.dtrSpacing = configuration.DtrSpacing;
this.dtrSwapDirection = configuration.DtrSwapDirection;
@@ -236,6 +240,12 @@ namespace Dalamud.Interface.Internal.Windows
private void DrawGeneralTab()
{
+ if (DateTime.Now.Month == 4 && DateTime.Now.Day == 1)
+ {
+ ImGui.Checkbox(Loc.Localize("Fools22", "Enable April Fools 2022"), ref this.doFools22);
+ ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("Fools22Hint", "Enables the April Fools 2022 funny joke. Turn this off if you don't think it's funny."));
+ }
+
ImGui.Text(Loc.Localize("DalamudSettingsLanguage", "Language"));
ImGui.Combo("##XlLangCombo", ref this.langIndex, this.locLanguages, this.locLanguages.Length);
ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsLanguageHint", "Select the language Dalamud will be displayed in."));
@@ -843,6 +853,8 @@ namespace Dalamud.Interface.Internal.Windows
configuration.IsFocusManagementEnabled = this.doFocus;
configuration.ShowTsm = this.doTsm;
+ configuration.Fools22 = this.doFools22;
+
configuration.UseAxisFontsFromGame = this.doUseAxisFontsFromGame;
configuration.FontGamma = this.fontGamma;