From f9e2e1cc25c72ad384aa07c7629a50ef095e922c Mon Sep 17 00:00:00 2001 From: Asgard <95163444+AsgardXIV@users.noreply.github.com> Date: Thu, 12 Jan 2023 00:50:36 -0700 Subject: [PATCH] Squashed Time/Weather --- Ktisis/Interface/Components/TimeControls.cs | 51 +++++++ .../Interface/Components/WeatherControls.cs | 124 ++++++++++++++++++ .../Interface/Windows/Workspace/Workspace.cs | 7 + Ktisis/Interop/Hooks/WorldHooks.cs | 69 ++++++++++ Ktisis/Ktisis.cs | 2 + Ktisis/Structs/FFXIV/WeatherSystem.cs | 9 ++ 6 files changed, 262 insertions(+) create mode 100644 Ktisis/Interface/Components/TimeControls.cs create mode 100644 Ktisis/Interface/Components/WeatherControls.cs create mode 100644 Ktisis/Interop/Hooks/WorldHooks.cs create mode 100644 Ktisis/Structs/FFXIV/WeatherSystem.cs diff --git a/Ktisis/Interface/Components/TimeControls.cs b/Ktisis/Interface/Components/TimeControls.cs new file mode 100644 index 000000000..c9bb31a1b --- /dev/null +++ b/Ktisis/Interface/Components/TimeControls.cs @@ -0,0 +1,51 @@ +using System; + +using ImGuiNET; + +using FFXIVClientStructs.FFXIV.Client.System.Framework; + +using Ktisis.Interop.Hooks; +using Ktisis.Structs.FFXIV; +using Ktisis.Util; + +namespace Ktisis.Interface.Components { + public static class TimeControls { + public unsafe static void Draw() { + if (ImGui.CollapsingHeader("Time Control")) { + Framework* framework = Framework.Instance(); + + ImGui.Checkbox("Lock Time", ref WorldHooks.TimeUpdateDisabled); + + bool isLocked = WorldHooks.TimeUpdateDisabled; + bool isOverridden = framework->IsEorzeaTimeOverridden; + long currentTime = isOverridden ? framework->EorzeaTimeOverride : framework->EorzeaTime; + + long timeVal = currentTime % 2764800; + long secondInDay = timeVal % 86400; + int timeOfDay = (int)(secondInDay / 60f); + int dayOfMonth = (int)(Math.Floor(timeVal / 86400f) + 1); + var displayTime = TimeSpan.FromMinutes(timeOfDay); + + int originalTime = timeOfDay; + int originalDay = dayOfMonth; + + if (!isLocked) ImGui.BeginDisabled(); + + ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X - GuiHelpers.WidthMargin() - GuiHelpers.GetRightOffset(ImGui.CalcTextSize("Day of Month").X)); + ImGui.SliderInt("Time of Day", ref timeOfDay, 0, 1439, $"{displayTime.Hours:D2}:{displayTime.Minutes:D2}"); + ImGui.SliderInt("Day of Month", ref dayOfMonth, 1, 31); + ImGui.PopItemWidth(); + + if (!isLocked) ImGui.EndDisabled(); + + + if (originalTime != timeOfDay || originalDay != dayOfMonth) { + long newTime = ((timeOfDay * 60) + (86400 * ((byte)(dayOfMonth) - 1))); + + if (isOverridden) framework->EorzeaTimeOverride = newTime; + framework->EorzeaTime = newTime; + } + } + } + } +} diff --git a/Ktisis/Interface/Components/WeatherControls.cs b/Ktisis/Interface/Components/WeatherControls.cs new file mode 100644 index 000000000..c79fb8404 --- /dev/null +++ b/Ktisis/Interface/Components/WeatherControls.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Interface; + +using ImGuiNET; + +using Lumina.Excel.GeneratedSheets; + +using Ktisis.Interop.Hooks; +using Ktisis.Util; + +namespace Ktisis.Interface.Components { + public static class WeatherControls { + private static uint CachedTerritory = 0xFFFFFFFF; + private static readonly List<(byte Id, string Name)> ZoneValidWeatherList = new(); + private static readonly Lazy> WeatherSheet = new(() => Services.DataManager.GameData.GetExcelSheet()!.Where(i => !string.IsNullOrEmpty(i.Name)).ToList()); + private static bool SearchOpen = false; + private static string SearchTerm = string.Empty; + + public unsafe static void Draw() { + if (ImGui.CollapsingHeader("Weather Control")) { + int weatherId = WorldHooks.WeatherSystem->CurrentWeather; + int originalWeatherId = weatherId; + + UpdateCache(); + + ImGui.Checkbox("Lock Weather", ref WorldHooks.WeatherUpdateDisabled); + + bool isLocked = WorldHooks.WeatherUpdateDisabled; + + if (!isLocked) ImGui.BeginDisabled(); + + ImGui.SetNextItemWidth(130f); + ImGui.InputInt("Weather ID", ref weatherId, 0, 0); + ImGui.SameLine(); + if (GuiHelpers.IconButtonTooltip(FontAwesomeIcon.Search, "Search")) + SearchOpen = true; + + ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X - GuiHelpers.WidthMargin()); + if (ImGui.BeginListBox("###weather_list")) { + + foreach (var weather in ZoneValidWeatherList) { + bool isSelected = weather.Id == weatherId; + if (ImGui.Selectable($"{weather.Name} ({weather.Id})###weather_{weather.Id}", isSelected)) { + weatherId = weather.Id; + } + } + + ImGui.EndListBox(); + } + ImGui.PopItemWidth(); + + if (isLocked && weatherId != originalWeatherId) + SetWeather((ushort)weatherId); + + if (!isLocked) ImGui.EndDisabled(); + + if (SearchOpen) + DrawWeatherSearchPopup(); + } + } + + private static void UpdateCache() { + + ushort territoryId = Services.ClientState.TerritoryType; + + if (CachedTerritory == territoryId) + return; + + ZoneValidWeatherList.Clear(); + CachedTerritory = territoryId; // We set this here so if there is a failure we don't try again until a rezone + + var territory = Services.DataManager.GameData.GetExcelSheet()!.GetRow(territoryId); + + if (territory == null) + return; + + var rate = Services.DataManager.GameData.GetExcelSheet()!.GetRow(territory.WeatherRate); + + if (rate == null) + return; + + foreach (var wr in rate!.UnkData0) { + if (wr.Weather == 0) + continue; + + var weather = WeatherSheet.Value.SingleOrDefault(i => i.RowId == wr.Weather); + + if (weather == null) + continue; + + if (ZoneValidWeatherList.Count(x => x.Id == (byte)weather.RowId) == 0) + ZoneValidWeatherList.Add(((byte)weather.RowId, weather.Name)); + + } + + ZoneValidWeatherList.Sort((x, y) => x.Id.CompareTo(y.Id)); + } + + private unsafe static void DrawWeatherSearchPopup() { + PopupSelect.HoverPopupWindow( + PopupSelect.HoverPopupWindowFlags.SelectorList | PopupSelect.HoverPopupWindowFlags.SearchBar, + WeatherSheet.Value!, + (e, input) => e.Where(t => $"{t.Name} ({t.RowId})".Contains(input, StringComparison.OrdinalIgnoreCase)), + (t, a) => { + // draw Line + bool selected = ImGui.Selectable($"{t.Name} ({t.RowId})###weather_item_{t.RowId}", a); + bool focus = ImGui.IsItemFocused(); + return (selected, focus); + }, + (t) => SetWeather((ushort)t.RowId), + () => SearchOpen = false, + ref SearchTerm, + "Weather Select", + "##weather_select", + "##weather_search"); + ; + } + + private unsafe static void SetWeather(ushort weatherId) => WorldHooks.WeatherSystem->CurrentWeather = weatherId; + } +} \ No newline at end of file diff --git a/Ktisis/Interface/Windows/Workspace/Workspace.cs b/Ktisis/Interface/Windows/Workspace/Workspace.cs index 12902e6c0..9c264aea8 100644 --- a/Ktisis/Interface/Windows/Workspace/Workspace.cs +++ b/Ktisis/Interface/Windows/Workspace/Workspace.cs @@ -106,6 +106,8 @@ public static void Draw() { SceneTab();*/ if (ImGui.BeginTabItem(Locale.GetString("Pose"))) PoseTab(target); + if (ImGui.BeginTabItem(Locale.GetString("World"))) + WorldTab(); } } @@ -214,6 +216,11 @@ private unsafe static void PoseTab(GameObject target) { ImGui.EndTabItem(); } + private unsafe static void WorldTab() { + TimeControls.Draw(); + WeatherControls.Draw(); + } + public static unsafe void DrawAdvancedDebugOptions(Actor* actor) { if(ImGui.Button("Reset Current Pose") && actor->Model != null) actor->Model->SyncModelSpace(); diff --git a/Ktisis/Interop/Hooks/WorldHooks.cs b/Ktisis/Interop/Hooks/WorldHooks.cs new file mode 100644 index 000000000..b69a53d78 --- /dev/null +++ b/Ktisis/Interop/Hooks/WorldHooks.cs @@ -0,0 +1,69 @@ +using System; + +using Dalamud.Hooking; + +using Ktisis.Events; +using Ktisis.Structs.Actor.State; +using Ktisis.Structs.FFXIV; + +namespace Ktisis.Interop.Hooks { + internal static class WorldHooks { + public static bool TimeUpdateDisabled = false; + public static bool WeatherUpdateDisabled = false; + + + internal delegate void UpdateEorzeaTimeDelegate(IntPtr a1, IntPtr a2); + internal static Hook UpdateEorzeaTimeHook = null!; + + internal delegate void UpdateTerritoryWeatherDelegate(IntPtr a1, IntPtr a2); + internal static Hook UpdateTerritoryWeatherHook = null!; + + public static unsafe WeatherSystem* WeatherSystem; + + internal unsafe static void UpdateEorzeaTime(IntPtr a1, IntPtr a2) { + if (TimeUpdateDisabled) + return; + + UpdateEorzeaTimeHook.Original(a1, a2); + } + + internal unsafe static void UpdateTerritoryWeather(IntPtr a1, IntPtr a2) { + if (WeatherUpdateDisabled) + return; + + UpdateTerritoryWeatherHook.Original(a1, a2); + } + + internal unsafe static void Init() { + var etAddress = Services.SigScanner.ScanText("48 89 5C 24 ?? 57 48 83 EC ?? 48 8B F9 48 8B DA 48 81 C1 ?? ?? ?? ?? E8 ?? ?? ?? ?? 4C 8B 87 ?? ?? ?? ??"); + UpdateEorzeaTimeHook = Hook.FromAddress(etAddress, UpdateEorzeaTime); + UpdateEorzeaTimeHook.Enable(); + + var twAddress = Services.SigScanner.ScanText("48 89 5C 24 ?? 55 56 57 48 83 EC ?? 48 8B F9 48 8D 0D ?? ?? ?? ??"); + UpdateTerritoryWeatherHook = Hook.FromAddress(twAddress, UpdateTerritoryWeather); + UpdateTerritoryWeatherHook.Enable(); + + IntPtr rawWeather = Services.SigScanner.GetStaticAddressFromSig("4C 8B 05 ?? ?? ?? ?? 41 8B 80 ?? ?? ?? ?? C1 E8 02"); + WeatherSystem = *(WeatherSystem**) rawWeather; + + EventManager.OnGPoseChange += OnGPoseChange; + } + + internal static void OnGPoseChange(ActorGposeState _state) { + if (_state == ActorGposeState.OFF) { + TimeUpdateDisabled = false; + WeatherUpdateDisabled = false; + } + } + + internal static void Dispose() { + EventManager.OnGPoseChange -= OnGPoseChange; + + UpdateEorzeaTimeHook.Disable(); + UpdateEorzeaTimeHook.Dispose(); + + UpdateTerritoryWeatherHook.Disable(); + UpdateTerritoryWeatherHook.Dispose(); + } + } +} \ No newline at end of file diff --git a/Ktisis/Ktisis.cs b/Ktisis/Ktisis.cs index a68fed749..74f8abeab 100644 --- a/Ktisis/Ktisis.cs +++ b/Ktisis/Ktisis.cs @@ -63,6 +63,7 @@ public Ktisis(DalamudPluginInterface pluginInterface) { Interop.Hooks.EventsHooks.Init(); Interop.Hooks.GuiHooks.Init(); Interop.Hooks.PoseHooks.Init(); + Interop.Hooks.WorldHooks.Init(); EventManager.OnGPoseChange += Workspace.OnEnterGposeToggle; // must be placed before ActorStateWatcher.Init() @@ -100,6 +101,7 @@ public void Dispose() { Interop.Hooks.EventsHooks.Dispose(); Interop.Hooks.GuiHooks.Dispose(); Interop.Hooks.PoseHooks.Dispose(); + Interop.Hooks.WorldHooks.Dispose(); Interop.Alloc.Dispose(); ActorStateWatcher.Instance.Dispose(); diff --git a/Ktisis/Structs/FFXIV/WeatherSystem.cs b/Ktisis/Structs/FFXIV/WeatherSystem.cs new file mode 100644 index 000000000..60338d48d --- /dev/null +++ b/Ktisis/Structs/FFXIV/WeatherSystem.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace Ktisis.Structs.FFXIV { + [StructLayout(LayoutKind.Explicit)] + public struct WeatherSystem { + [FieldOffset(0x27)] + public ushort CurrentWeather; + } +}