diff --git a/Glamourer/Configuration.cs b/Glamourer/Configuration.cs index be62f75..5ae955f 100644 --- a/Glamourer/Configuration.cs +++ b/Glamourer/Configuration.cs @@ -27,6 +27,7 @@ public class Configuration : IPluginConfiguration, ISavable public bool HideApplyCheckmarks { get; set; } = false; public bool SmallEquip { get; set; } = false; public bool UnlockedItemMode { get; set; } = false; + public byte DisableFestivals { get; set; } = 1; public bool EnableGameContextMenu { get; set; } = true; public MainWindow.TabType SelectedTab { get; set; } = MainWindow.TabType.Settings; public DoubleModifier DeleteDesignModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); diff --git a/Glamourer/Gui/GenericPopupWindow.cs b/Glamourer/Gui/GenericPopupWindow.cs new file mode 100644 index 0000000..7cbe5c5 --- /dev/null +++ b/Glamourer/Gui/GenericPopupWindow.cs @@ -0,0 +1,73 @@ +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Windowing; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; + +namespace Glamourer.Gui; + +public class GenericPopupWindow : Window +{ + private Configuration _config; + public bool OpenFestivalPopup { get; internal set; } = false; + + public GenericPopupWindow(Configuration config) + : base("Glamourer Popups", + ImGuiWindowFlags.NoBringToFrontOnFocus + | ImGuiWindowFlags.NoDecoration + | ImGuiWindowFlags.NoInputs + | ImGuiWindowFlags.NoSavedSettings + | ImGuiWindowFlags.NoBackground + | ImGuiWindowFlags.NoMove + | ImGuiWindowFlags.NoNav + | ImGuiWindowFlags.NoTitleBar, true) + { + _config = config; + IsOpen = true; + } + + public override void Draw() + { + if (OpenFestivalPopup) + { + ImGui.OpenPopup("FestivalPopup"); + OpenFestivalPopup = false; + } + + DrawFestivalPopup(); + } + + + private void DrawFestivalPopup() + { + var viewportSize = ImGui.GetWindowViewport().Size; + ImGui.SetNextWindowSize(new Vector2(viewportSize.X / 5, viewportSize.Y / 7)); + ImGui.SetNextWindowPos(viewportSize / 2, ImGuiCond.Always, new Vector2(0.5f)); + using var popup = ImRaii.Popup("FestivalPopup", ImGuiWindowFlags.Modal); + if (!popup) + return; + + ImGuiUtil.TextWrapped( + "Glamourer has some festival-specific behaviour that is turned on by default. You can always turn this behaviour on or off in the general settings, and choose your current preference now."); + + var buttonWidth = new Vector2(150 * ImGuiHelpers.GlobalScale, 0); + var yPos = ImGui.GetWindowHeight() - 2 * ImGui.GetFrameHeight(); + var xPos = (ImGui.GetWindowWidth() - ImGui.GetStyle().ItemSpacing.X) / 2 - buttonWidth.X; + ImGui.SetCursorPos(new Vector2(xPos, yPos)); + if (ImGui.Button("Let's Check It Out!", buttonWidth)) + { + _config.DisableFestivals = 0; + _config.Save(); + ImGui.CloseCurrentPopup(); + } + + ImGui.SameLine(); + if (ImGui.Button("Not Right Now.", buttonWidth)) + { + _config.DisableFestivals = 2; + _config.Save(); + ImGui.CloseCurrentPopup(); + } + } +} diff --git a/Glamourer/Gui/GlamourerWindowSystem.cs b/Glamourer/Gui/GlamourerWindowSystem.cs index f9046d6..da0422f 100644 --- a/Glamourer/Gui/GlamourerWindowSystem.cs +++ b/Glamourer/Gui/GlamourerWindowSystem.cs @@ -11,12 +11,13 @@ public class GlamourerWindowSystem : IDisposable private readonly MainWindow _ui; private readonly PenumbraChangedItemTooltip _penumbraTooltip; - public GlamourerWindowSystem(UiBuilder uiBuilder, MainWindow ui, PenumbraChangedItemTooltip penumbraTooltip) + public GlamourerWindowSystem(UiBuilder uiBuilder, MainWindow ui, GenericPopupWindow popups, PenumbraChangedItemTooltip penumbraTooltip) { _uiBuilder = uiBuilder; _ui = ui; _penumbraTooltip = penumbraTooltip; _windowSystem.AddWindow(ui); + _windowSystem.AddWindow(popups); _uiBuilder.Draw += _windowSystem.Draw; _uiBuilder.OpenConfigUi += _ui.Toggle; } @@ -26,4 +27,4 @@ public class GlamourerWindowSystem : IDisposable _uiBuilder.Draw -= _windowSystem.Draw; _uiBuilder.OpenConfigUi -= _ui.Toggle; } -} +} \ No newline at end of file diff --git a/Glamourer/Gui/Tabs/DebugTab.cs b/Glamourer/Gui/Tabs/DebugTab.cs index 66d19fd..1220f47 100644 --- a/Glamourer/Gui/Tabs/DebugTab.cs +++ b/Glamourer/Gui/Tabs/DebugTab.cs @@ -66,19 +66,21 @@ public unsafe class DebugTab : ITab private readonly PenumbraChangedItemTooltip _penumbraTooltip; private readonly StateManager _state; + private readonly FunModule _funModule; private int _gameObjectIndex; public bool IsVisible => _config.DebugMode; - public DebugTab(ChangeCustomizeService changeCustomizeService, VisorService visorService, UpdateSlotService updateSlotService, WeaponService weaponService, PenumbraService penumbra, + public DebugTab(ChangeCustomizeService changeCustomizeService, VisorService visorService, UpdateSlotService updateSlotService, + WeaponService weaponService, PenumbraService penumbra, ActorService actors, ItemManager items, CustomizationService customization, ObjectManager objectManager, DesignFileSystem designFileSystem, DesignManager designManager, StateManager state, Configuration config, PenumbraChangedItemTooltip penumbraTooltip, MetaService metaService, GlamourerIpc ipc, DalamudPluginInterface pluginInterface, AutoDesignManager autoDesignManager, JobService jobs, CodeService code, CustomizeUnlockManager customizeUnlocks, ItemUnlockManager itemUnlocks, DesignConverter designConverter, DatFileService datFileService, InventoryService inventoryService, - HumanModelList humans) + HumanModelList humans, FunModule funModule) { _changeCustomizeService = changeCustomizeService; _visorService = visorService; @@ -106,6 +108,7 @@ public unsafe class DebugTab : ITab _datFileService = datFileService; _inventoryService = inventoryService; _humans = humans; + _funModule = funModule; } public ReadOnlySpan Label @@ -125,6 +128,7 @@ public unsafe class DebugTab : ITab DrawAutoDesigns(); DrawInventory(); DrawUnlocks(); + DrawFun(); DrawIpc(); } @@ -1569,6 +1573,35 @@ public unsafe class DebugTab : ITab #endregion + #region Fun + + private void DrawFun() + { + if (!ImGui.CollapsingHeader("Fun Module")) + return; + + ImGui.TextUnformatted($"Current Festival: {_funModule.CurrentFestival}"); + ImGui.TextUnformatted($"Festivals Enabled: {_config.DisableFestivals switch { 1 => "Undecided", 0 => "Enabled", _ => "Disabled" }}"); + ImGui.TextUnformatted($"Popup Open: {ImGui.IsPopupOpen("FestivalPopup", ImGuiPopupFlags.AnyPopup)}"); + if (ImGui.Button("Force Christmas")) + _funModule.ForceFestival(FunModule.FestivalType.Christmas); + if (ImGui.Button("Force Halloween")) + _funModule.ForceFestival(FunModule.FestivalType.Halloween); + if (ImGui.Button("Force April First")) + _funModule.ForceFestival(FunModule.FestivalType.AprilFirst); + if (ImGui.Button("Force None")) + _funModule.ForceFestival(FunModule.FestivalType.None); + if (ImGui.Button("Revert")) + _funModule.ResetFestival(); + if (ImGui.Button("Reset Popup")) + { + _config.DisableFestivals = 1; + _config.Save(); + } + } + + #endregion + #region IPC private string _gameObjectName = string.Empty; @@ -1600,7 +1633,8 @@ public unsafe class DebugTab : ITab ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelGetAllCustomizationFromCharacter); ImGui.TableNextColumn(); - base64 = GlamourerIpc.GetAllCustomizationFromCharacterSubscriber(_pluginInterface).Invoke(_objectManager.Objects[_gameObjectIndex] as Character); + base64 = GlamourerIpc.GetAllCustomizationFromCharacterSubscriber(_pluginInterface) + .Invoke(_objectManager.Objects[_gameObjectIndex] as Character); if (base64 != null) ImGuiUtil.CopyOnClickSelectable(base64); else @@ -1624,7 +1658,8 @@ public unsafe class DebugTab : ITab ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyAllToCharacter); ImGui.TableNextColumn(); if (ImGui.Button("Apply##AllCharacter")) - GlamourerIpc.ApplyAllToCharacterSubscriber(_pluginInterface).Invoke(_base64Apply, _objectManager.Objects[_gameObjectIndex] as Character); + GlamourerIpc.ApplyAllToCharacterSubscriber(_pluginInterface) + .Invoke(_base64Apply, _objectManager.Objects[_gameObjectIndex] as Character); ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyOnlyEquipment); ImGui.TableNextColumn(); diff --git a/Glamourer/Gui/Tabs/SettingsTab.cs b/Glamourer/Gui/Tabs/SettingsTab.cs index 23af523..0afc6d4 100644 --- a/Glamourer/Gui/Tabs/SettingsTab.cs +++ b/Glamourer/Gui/Tabs/SettingsTab.cs @@ -53,6 +53,9 @@ public class SettingsTab : ITab Checkbox("Unlocked Item Mode", "Enable this if you want automatically applied designs to only consider items and customizations you have actually unlocked once, and skip those you have not.", _config.UnlockedItemMode, v => _config.UnlockedItemMode = v); + Checkbox("Enable Festival Easter-Eggs", + "Glamourer may do some fun things on specific dates. Disable this if you do not want your experience disrupted by this.", + _config.DisableFestivals == 0, v => _config.DisableFestivals = v ? (byte)0 : (byte)2); Checkbox("Auto-Reload Gear", "Automatically reload equipment pieces on your own character when changing any mod options in Penumbra in their associated collection.", _config.AutoRedrawEquipOnChanges, _autoRedraw.SetState); diff --git a/Glamourer/Services/ServiceManager.cs b/Glamourer/Services/ServiceManager.cs index 4a02824..9851501 100644 --- a/Glamourer/Services/ServiceManager.cs +++ b/Glamourer/Services/ServiceManager.cs @@ -120,6 +120,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Glamourer/State/FunEquipSet.cs b/Glamourer/State/FunEquipSet.cs new file mode 100644 index 0000000..f6c5253 --- /dev/null +++ b/Glamourer/State/FunEquipSet.cs @@ -0,0 +1,95 @@ +using System; +using Penumbra.GameData.Structs; + +namespace Glamourer.State; + +internal class FunEquipSet +{ + public static FunEquipSet? GetSet(FunModule.FestivalType type) + { + return type switch + { + FunModule.FestivalType.Halloween => Halloween, + FunModule.FestivalType.Christmas => Christmas, + FunModule.FestivalType.AprilFirst => AprilFirst, + _ => null, + }; + } + + public readonly record struct Group(CharacterArmor Head, CharacterArmor Body, CharacterArmor Hands, CharacterArmor Legs, + CharacterArmor Feet, StainId[]? Stains = null) + { + public Group(ushort headS, byte headV, ushort bodyS, byte bodyV, ushort handsS, byte handsV, ushort legsS, byte legsV, ushort feetS, + byte feetV, StainId[]? stains = null) + : this(new CharacterArmor(headS, headV, 0), new CharacterArmor(bodyS, bodyV, 0), new CharacterArmor(handsS, handsV, 0), + new CharacterArmor(legsS, legsV, 0), new CharacterArmor(feetS, feetV, 0), stains) + { } + } + + private readonly Group[] _groups; + + public void Apply(StainId[] allStains, Random rng, Span armor) + { + var groupIdx = rng.Next(0, _groups.Length); + var group = _groups[groupIdx]; + var stains = group.Stains ?? allStains; + var stainIdx = rng.Next(0, stains.Length); + var stain = stains[stainIdx]; + Glamourer.Log.Verbose($"Chose group {groupIdx} and stain {stainIdx} for festival costume."); + if (group.Head.Set != 0) + armor[0] = group.Head.With(stain); + if (group.Body.Set != 0) + armor[1] = group.Body.With(stain); + if (group.Hands.Set != 0) + armor[2] = group.Hands.With(stain); + if (group.Legs.Set != 0) + armor[3] = group.Legs.With(stain); + if (group.Feet.Set != 0) + armor[4] = group.Feet.With(stain); + } + + public static readonly FunEquipSet Christmas = new + ( + new Group(6170, 1, 6170, 1, 6170, 1, 6170, 1, 6170, 1), // Unorthodox Saint + new Group(6006, 1, 6170, 1, 6170, 1, 6170, 1, 6170, 1), // Unorthodox Saint + new Group(6007, 1, 6170, 1, 6170, 1, 6170, 1, 6170, 1), // Unorthodox Saint + new Group(0058, 1, 0058, 1, 6005, 1, 0000, 0, 6005, 1), // Reindeer + new Group(6005, 1, 0058, 1, 6005, 1, 0000, 0, 6005, 1), // Reindeer + new Group(0231, 1, 0231, 1, 0279, 1, 0231, 1, 0231, 1), // Starlight + new Group(0231, 1, 6030, 1, 0279, 1, 0231, 1, 0231, 1), // Starlight + new Group(0053, 1, 0053, 1, 0279, 1, 0279, 1, 0053, 1), // Sweet Dream + new Group(0136, 1, 0136, 1, 0136, 1, 0000, 0, 0000, 0) // Snowman + ); + + public static readonly FunEquipSet Halloween = new + ( + new Group(0316, 1, 0316, 1, 0316, 1, 0279, 1, 0316, 1), // Witch + new Group(6047, 1, 6047, 1, 6047, 1, 6047, 1, 6047, 1), // Werewolf + new Group(6148, 1, 6148, 1, 6148, 1, 6148, 1, 6148, 1), // Wake Doctor + new Group(6117, 1, 6117, 1, 6117, 1, 6117, 1, 6117, 1), // Clown + new Group(0000, 0, 0137, 1, 0000, 0, 0000, 0, 0000, 0), // Howling Spirit + new Group(0000, 0, 0137, 2, 0000, 0, 0000, 0, 0000, 0), // Wailing Spirit + new Group(0232, 1, 0232, 1, 0279, 1, 0232, 1, 0232, 1), // Eerie Attire + new Group(0232, 1, 6036, 1, 0279, 1, 0232, 1, 0232, 1) // Vampire + ); + + public static readonly FunEquipSet AprilFirst = new + ( + new Group(6133, 1, 6133, 1, 0000, 0, 0000, 0, 0000, 0), // Gaja + new Group(6121, 1, 6121, 1, 0000, 0, 0000, 0, 0000, 0), // Chicken + new Group(6103, 1, 6103, 1, 0000, 0, 0000, 0, 0000, 0), // Rabbit + new Group(6103, 1, 6103, 2, 0000, 0, 0000, 0, 0000, 0), // Rabbit + new Group(6089, 1, 6089, 1, 0000, 0, 0000, 0, 0000, 0), // Toad + new Group(0241, 1, 0046, 1, 0000, 0, 0279, 1, 6169, 3), // Chocobo + new Group(0046, 1, 0046, 1, 0000, 0, 0279, 1, 6169, 3), // Chocobo + new Group(0058, 1, 0058, 1, 0000, 0, 0000, 0, 0000, 0), // Reindeer + new Group(0136, 1, 0136, 1, 0136, 1, 0000, 0, 0000, 0), // Snowman + new Group(0159, 1, 0000, 0, 0000, 0, 0000, 0, 0000, 0), // Slime Crown + new Group(6117, 1, 6117, 1, 6117, 1, 6117, 1, 6117, 1), // Clown + new Group(6169, 3, 6169, 3, 0279, 1, 6169, 3, 6169, 3), // Chocobo Pajama + new Group(6169, 2, 6169, 2, 0279, 2, 6169, 2, 6169, 2) // Cactuar Pajama + ); + + private FunEquipSet(params Group[] groups) + => _groups = groups; +} diff --git a/Glamourer/State/FunModule.cs b/Glamourer/State/FunModule.cs index fef1ca4..5bd6381 100644 --- a/Glamourer/State/FunModule.cs +++ b/Glamourer/State/FunModule.cs @@ -2,31 +2,80 @@ using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using Glamourer.Customization; +using Glamourer.Gui; using Glamourer.Interop.Structs; using Glamourer.Services; +using OtterGui.Classes; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using CustomizeIndex = Glamourer.Customization.CustomizeIndex; namespace Glamourer.State; -public unsafe class FunModule +public unsafe class FunModule : IDisposable { + public enum FestivalType + { + None, + Halloween, + Christmas, + AprilFirst, + } + private readonly ItemManager _items; private readonly CustomizationService _customizations; + private readonly Configuration _config; private readonly CodeService _codes; private readonly Random _rng; + private readonly GenericPopupWindow _popupWindow; private readonly StainId[] _stains; - public FunModule(CodeService codes, CustomizationService customizations, ItemManager items) + public FestivalType CurrentFestival { get; private set; } = FestivalType.None; + private FunEquipSet? _festivalSet; + + private void OnDayChange(int day, int month, int _) + { + CurrentFestival = (day, month) switch + { + (1, 4) => FestivalType.AprilFirst, + (24, 12) => FestivalType.Christmas, + (25, 12) => FestivalType.Christmas, + (26, 12) => FestivalType.Christmas, + (31, 10) => FestivalType.Halloween, + (01, 11) => FestivalType.Halloween, + _ => FestivalType.None, + }; + _festivalSet = FunEquipSet.GetSet(CurrentFestival); + _popupWindow.OpenFestivalPopup = _festivalSet != null && _config.DisableFestivals == 1; + } + + internal void ForceFestival(FestivalType type) + { + CurrentFestival = type; + _festivalSet = FunEquipSet.GetSet(CurrentFestival); + _popupWindow.OpenFestivalPopup = _festivalSet != null && _config.DisableFestivals == 1; + } + + internal void ResetFestival() + => OnDayChange(DateTime.UtcNow.Day, DateTime.UtcNow.Month, DateTime.UtcNow.Year); + + public FunModule(CodeService codes, CustomizationService customizations, ItemManager items, Configuration config, + GenericPopupWindow popupWindow) { _codes = codes; _customizations = customizations; _items = items; + _config = config; + _popupWindow = popupWindow; _rng = new Random(); _stains = _items.Stains.Keys.Prepend((StainId)0).ToArray(); + ResetFestival(); + DayChangeTracker.DayChanged += OnDayChange; } + public void Dispose() + => DayChangeTracker.DayChanged -= OnDayChange; + public void ApplyFun(Actor actor, ref CharacterArmor armor, EquipSlot slot) { if (actor.AsObject->ObjectKind is not (byte)ObjectKind.Player || !actor.IsCharacter) @@ -47,8 +96,15 @@ public unsafe class FunModule if (actor.AsCharacter->CharacterData.ModelCharaId != 0) return; - ApplyEmperor(armor); - ApplyClown(armor); + if (_config.DisableFestivals == 0 && _festivalSet != null) + { + _festivalSet.Apply(_stains, _rng, armor); + } + else + { + ApplyEmperor(armor); + ApplyClown(armor); + } ApplyOops(ref customize); ApplyIndividual(ref customize);