This commit is contained in:
Ottermandias 2023-07-01 12:57:23 +02:00
parent 5e410d8786
commit 8fbca07f3c
10 changed files with 453 additions and 24 deletions

View file

@ -1,11 +1,12 @@
using System.Runtime.InteropServices;
using System;
using System.Runtime.InteropServices;
namespace Glamourer.Customization;
// Any customization value can be represented in 8 bytes by its ID,
// a byte value, an optional value-id and an optional icon or color.
[StructLayout(LayoutKind.Explicit)]
public readonly struct CustomizeData
public readonly struct CustomizeData : IEquatable<CustomizeData>
{
[FieldOffset(0)]
public readonly CustomizeIndex Index;
@ -30,4 +31,15 @@ public readonly struct CustomizeData
Color = data;
CustomizeId = customizeId;
}
public bool Equals(CustomizeData other)
=> Index == other.Index
&& Value.Value == other.Value.Value
&& CustomizeId == other.CustomizeId;
public override bool Equals(object? obj)
=> obj is CustomizeData other && Equals(other);
public override int GetHashCode()
=> HashCode.Combine((int)Index, Value.Value, CustomizeId);
}

View file

@ -11,6 +11,7 @@ using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.Structs;
using Glamourer.Unlocks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Filesystem;
@ -28,6 +29,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>
private readonly DesignManager _designs;
private readonly ActorService _actors;
private readonly AutomationChanged _event;
private readonly ItemUnlockManager _unlockManager;
private readonly List<AutoDesignSet> _data = new();
private readonly Dictionary<ActorIdentifier, AutoDesignSet> _enabled = new();
@ -36,13 +38,14 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>
=> _enabled;
public AutoDesignManager(JobService jobs, ActorService actors, SaveService saveService, DesignManager designs, AutomationChanged @event,
FixedDesignMigrator migrator, DesignFileSystem fileSystem)
FixedDesignMigrator migrator, DesignFileSystem fileSystem, ItemUnlockManager unlockManager)
{
_jobs = jobs;
_actors = actors;
_saveService = saveService;
_designs = designs;
_event = @event;
_unlockManager = unlockManager;
Load();
migrator.ConsumeMigratedData(_actors, fileSystem, this);
}

View file

@ -18,6 +18,7 @@ using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Unlocks;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
@ -47,6 +48,8 @@ public unsafe class DebugTab : ITab
private readonly ActorService _actors;
private readonly CustomizationService _customization;
private readonly JobService _jobs;
private readonly CustomizeUnlockManager _customizeUnlocks;
private readonly CustomizeUnlockManager _itemUnlocks;
private readonly DesignManager _designManager;
private readonly DesignFileSystem _designFileSystem;
@ -66,7 +69,7 @@ public unsafe class DebugTab : ITab
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, PhrasingService phrasing)
AutoDesignManager autoDesignManager, JobService jobs, PhrasingService phrasing, CustomizeUnlockManager customizeUnlocks)
{
_changeCustomizeService = changeCustomizeService;
_visorService = visorService;
@ -89,6 +92,7 @@ public unsafe class DebugTab : ITab
_autoDesignManager = autoDesignManager;
_jobs = jobs;
_phrasing = phrasing;
_customizeUnlocks = customizeUnlocks;
}
public ReadOnlySpan<byte> Label
@ -106,6 +110,7 @@ public unsafe class DebugTab : ITab
DrawDesigns();
DrawState();
DrawAutoDesigns();
DrawUnlocks();
DrawIpc();
}
@ -1236,6 +1241,61 @@ public unsafe class DebugTab : ITab
#endregion
#region Unlocks
private void DrawUnlocks()
{
if (!ImGui.CollapsingHeader("Unlocks"))
return;
DrawCustomizationUnlocks();
DrawItemUnlocks();
}
private void DrawCustomizationUnlocks()
{
using var tree = ImRaii.TreeNode("Customization");
if (!tree)
return;
using var table = ImRaii.Table("customizationUnlocks", 6,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY,
new Vector2(ImGui.GetContentRegionAvail().X, 12 * ImGui.GetTextLineHeight()));
if (!table)
return;
ImGui.TableNextColumn();
var skips = ImGuiClip.GetNecessarySkips(ImGui.GetTextLineHeightWithSpacing());
ImGui.TableNextRow();
var remainder = ImGuiClip.ClippedDraw(_customizeUnlocks.Unlockable, skips, t =>
{
ImGuiUtil.DrawTableColumn(t.Key.Index.ToDefaultName());
ImGuiUtil.DrawTableColumn(t.Key.CustomizeId.ToString());
ImGuiUtil.DrawTableColumn(t.Key.Value.Value.ToString());
ImGuiUtil.DrawTableColumn(t.Value.Data.ToString());
ImGuiUtil.DrawTableColumn(t.Value.Name);
ImGuiUtil.DrawTableColumn(_customizeUnlocks.IsUnlocked(t.Key, out var time)
? time == DateTimeOffset.MaxValue ? "Always" : time.LocalDateTime.ToShortDateString()
: "Never");
}, _customizeUnlocks.Unlockable.Count);
ImGuiClip.DrawEndDummy(remainder, ImGui.GetTextLineHeight());
}
private void DrawItemUnlocks()
{
using var tree = ImRaii.TreeNode("Item");
if (!tree)
return;
using var table = ImRaii.Table("itemUnlocks", 6, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
return;
}
#endregion
#region IPC
private string _gameObjectName = string.Empty;

View file

@ -3,7 +3,6 @@ using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using static OtterGui.Raii.ImRaii;
namespace Glamourer.Interop;
@ -56,7 +55,7 @@ public unsafe class MetaService : IDisposable
var v = value == 0;
_headGearEvent.Invoke(actor, ref v);
value = (byte)(v ? 0 : 1);
Glamourer.Log.Information($"[MetaService] Hide Hat triggered with 0x{(nint)drawData:X} {id} {value} for {actor.Utf8Name}.");
Glamourer.Log.Verbose($"[MetaService] Hide Hat triggered with 0x{(nint)drawData:X} {id} {value} for {actor.Utf8Name}.");
_hideHatGearHook.Original(drawData, id, value);
}
@ -66,7 +65,7 @@ public unsafe class MetaService : IDisposable
value = !value;
_weaponEvent.Invoke(actor, ref value);
value = !value;
Glamourer.Log.Information($"[MetaService] Hide Weapon triggered with 0x{(nint)drawData:X} {value} for {actor.Utf8Name}.");
Glamourer.Log.Verbose($"[MetaService] Hide Weapon triggered with 0x{(nint)drawData:X} {value} for {actor.Utf8Name}.");
_hideWeaponsHook.Original(drawData, value);
}
}

View file

@ -21,6 +21,9 @@ public class BackupService
new(fileNames.ConfigFile),
new(fileNames.DesignFileSystem),
new(fileNames.MigrationDesignFile),
new(fileNames.AutomationFile),
new(fileNames.UnlockFileCustomize),
new(fileNames.UnlockFileItems),
};
list.AddRange(fileNames.Designs());

View file

@ -13,6 +13,8 @@ public class FilenameService
public readonly string MigrationDesignFile;
public readonly string DesignDirectory;
public readonly string AutomationFile;
public readonly string UnlockFileCustomize;
public readonly string UnlockFileItems;
public FilenameService(DalamudPluginInterface pi)
{
@ -21,6 +23,8 @@ public class FilenameService
AutomationFile = Path.Combine(ConfigDirectory, "automation.json");
DesignFileSystem = Path.Combine(ConfigDirectory, "sort_order.json");
MigrationDesignFile = Path.Combine(ConfigDirectory, "Designs.json");
UnlockFileCustomize = Path.Combine(ConfigDirectory, "unlocks_customize.json");
UnlockFileItems = Path.Combine(ConfigDirectory, "unlocks_items.json");
DesignDirectory = Path.Combine(ConfigDirectory, "designs");
}

View file

@ -13,6 +13,7 @@ using Glamourer.Gui.Tabs.DesignTab;
using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.State;
using Glamourer.Unlocks;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes;
using OtterGui.Log;
@ -80,7 +81,9 @@ public static class ServiceManager
.AddSingleton<PenumbraService>()
.AddSingleton<ObjectManager>()
.AddSingleton<PenumbraAutoRedraw>()
.AddSingleton<JobService>();
.AddSingleton<JobService>()
.AddSingleton<CustomizeUnlockManager>()
.AddSingleton<ItemUnlockManager>();
private static IServiceCollection AddDesigns(this IServiceCollection services)
=> services.AddSingleton<DesignManager>()

View file

@ -20,11 +20,12 @@ public class StateEditor
private readonly UpdateSlotService _updateSlot;
private readonly VisorService _visor;
private readonly WeaponService _weapon;
private readonly MetaService _metaService;
private readonly ChangeCustomizeService _changeCustomize;
private readonly ItemManager _items;
public StateEditor(UpdateSlotService updateSlot, VisorService visor, WeaponService weapon, ChangeCustomizeService changeCustomize,
ItemManager items, PenumbraService penumbra)
ItemManager items, PenumbraService penumbra, MetaService metaService)
{
_updateSlot = updateSlot;
_visor = visor;
@ -32,6 +33,7 @@ public class StateEditor
_changeCustomize = changeCustomize;
_items = items;
_penumbra = penumbra;
_metaService = metaService;
}
/// <summary> Changing the model ID simply requires guaranteed redrawing. </summary>
@ -152,13 +154,13 @@ public class StateEditor
public unsafe void ChangeHatState(ActorData data, bool value)
{
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
actor.AsCharacter->DrawData.HideHeadgear(0, !value);
_metaService.SetHatState(actor, value);
}
/// <summary> Change the weapon-visibility state on actors. </summary>
public unsafe void ChangeWeaponState(ActorData data, bool value)
{
foreach (var actor in data.Objects.Where(a => a.IsCharacter))
actor.AsCharacter->DrawData.HideWeapons(!value);
_metaService.SetWeaponState(actor, value);
}
}

View file

@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud;
using Dalamud.Data;
using Dalamud.Game.ClientState;
using Dalamud.Hooking;
using Dalamud.Utility;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Glamourer.Customization;
using Glamourer.Services;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer.Unlocks;
public class CustomizeUnlockManager : IDisposable, ISavable
{
private readonly SaveService _saveService;
private readonly ClientState _clientState;
private readonly Dictionary<uint, long> _unlocked = new();
public readonly IReadOnlyDictionary<CustomizeData, (uint Data, string Name)> Unlockable;
public IReadOnlyDictionary<uint, long> Unlocked
=> _unlocked;
public unsafe CustomizeUnlockManager(SaveService saveService, CustomizationService customizations, DataManager gameData,
ClientState clientState)
{
SignatureHelper.Initialise(this);
_saveService = saveService;
_clientState = clientState;
Unlockable = CreateUnlockableCustomizations(customizations, gameData);
Load();
_setUnlockLinkValueHook.Enable();
_clientState.Login += OnLogin;
Scan();
}
public void Dispose()
{
_setUnlockLinkValueHook.Dispose();
_clientState.Login -= OnLogin;
}
/// <summary> Check if a customization is unlocked for Glamourer. </summary>
public bool IsUnlocked(CustomizeData data, out DateTimeOffset time)
{
if (!Unlockable.TryGetValue(data, out var pair))
{
time = DateTime.MaxValue;
return true;
}
if (_unlocked.TryGetValue(pair.Data, out var t))
{
time = DateTimeOffset.FromUnixTimeMilliseconds(t);
return true;
}
if (!IsUnlockedGame(pair.Data))
{
time = DateTimeOffset.MinValue;
return false;
}
_unlocked.TryAdd(pair.Data, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
Save();
time = DateTimeOffset.UtcNow;
return true;
}
/// <summary> Check if a customization is currently unlocked for the game state. </summary>
public unsafe bool IsUnlockedGame(uint dataId)
{
var instance = UIState.Instance();
if (instance == null)
return false;
return UIState.Instance()->IsUnlockLinkUnlocked(dataId);
}
/// <summary> Scan and update all unlockable customizations for their current game state. </summary>
public unsafe void Scan()
{
if (_clientState.LocalPlayer == null)
return;
Glamourer.Log.Debug("[UnlockManager] Scanning for new unlocked customizations.");
var instance = UIState.Instance();
if (instance == null)
return;
try
{
var count = 0;
var time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
foreach (var (_, (id, _)) in Unlockable)
{
if (instance->IsUnlockLinkUnlocked(id) && _unlocked.TryAdd(id, time))
++count;
}
if (count <= 0)
return;
Save();
Glamourer.Log.Debug($"[UnlockManager] Found {count} new unlocked customizations..");
}
catch (Exception ex)
{
Glamourer.Log.Error($"[UnlockManager] Error scanning for newly unlocked customizations:\n{ex}");
}
}
private delegate void SetUnlockLinkValueDelegate(nint uiState, uint data, byte value);
[Signature("48 83 EC ?? 8B C2 44 8B D2", DetourName = nameof(SetUnlockLinkValueDetour))]
private readonly Hook<SetUnlockLinkValueDelegate> _setUnlockLinkValueHook = null!;
private void SetUnlockLinkValueDetour(nint uiState, uint data, byte value)
{
_setUnlockLinkValueHook.Original(uiState, data, value);
try
{
if (value == 0)
return;
var time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
foreach (var (_, (id, _)) in Unlockable)
{
if (id != data || !_unlocked.TryAdd(id, time))
continue;
Save();
break;
}
}
catch (Exception ex)
{
Glamourer.Log.Error($"[UnlockManager] Error in SetUnlockLinkValue Hook:\n{ex}");
}
}
private void OnLogin(object? _, EventArgs _2)
=> Scan();
public string ToFilename(FilenameService fileNames)
=> fileNames.UnlockFileCustomize;
public void Save()
=> _saveService.QueueSave(this);
public void Save(StreamWriter writer)
{ }
private void Load()
{
var file = ToFilename(_saveService.FileNames);
if (!File.Exists(file))
return;
_unlocked.Clear();
}
/// <summary> Create a list of all unlockable hairstyles and facepaints. </summary>
private static Dictionary<CustomizeData, (uint Data, string Name)> CreateUnlockableCustomizations(CustomizationService customizations,
DataManager gameData)
{
var ret = new Dictionary<CustomizeData, (uint Data, string Name)>();
var sheet = gameData.GetExcelSheet<CharaMakeCustomize>(ClientLanguage.English)!;
foreach (var clan in customizations.AwaitedService.Clans)
{
foreach (var gender in customizations.AwaitedService.Genders)
{
var list = customizations.AwaitedService.GetList(clan, gender);
foreach (var hair in list.HairStyles)
{
var x = sheet.FirstOrDefault(f => f.FeatureID == hair.Value.Value);
if (x?.IsPurchasable == true)
{
var name = x.FeatureID == 61
? "Eternal Bond"
: x.HintItem.Value?.Name.ToDalamudString().ToString().Replace("Modern Aesthetics - ", string.Empty)
?? string.Empty;
ret.TryAdd(hair, (x.Data, name));
}
}
foreach (var paint in list.FacePaints)
{
var x = sheet.FirstOrDefault(f => f.FeatureID == paint.Value.Value);
if (x?.IsPurchasable == true)
{
var name = x.HintItem.Value?.Name.ToDalamudString().ToString().Replace("Modern Cosmetics - ", string.Empty)
?? string.Empty;
ret.TryAdd(paint, (x.Data, name));
}
}
}
}
return ret;
}
}

View file

@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.IO;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Glamourer.Services;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using Achievement = FFXIVClientStructs.FFXIV.Client.Game.UI.Achievement;
using Cabinet = Lumina.Excel.GeneratedSheets.Cabinet;
namespace Glamourer.Unlocks;
public class ItemUnlockManager : ISavable, IDisposable
{
private readonly SaveService _saveService;
private readonly ItemManager _items;
private readonly ClientState _clientState;
private readonly Framework _framework;
private readonly Dictionary<uint, long> _unlocked = new();
public enum UnlockType : byte
{
Cabinet,
Quest,
Achievement,
}
public readonly IReadOnlyDictionary<uint, (uint, UnlockType)> _unlockable;
public IReadOnlyDictionary<uint, long> Unlocked
=> _unlocked;
public ItemUnlockManager(SaveService saveService, ItemManager items, ClientState clientState, DataManager gameData, Framework framework)
{
SignatureHelper.Initialise(this);
_saveService = saveService;
_items = items;
_clientState = clientState;
_framework = framework;
_unlockable = CreateUnlockData(gameData, items);
Load();
_clientState.Login += OnLogin;
_framework.Update += OnFramework;
Scan();
}
//private Achievement.AchievementState _achievementState = Achievement.AchievementState.Invalid;
private unsafe void OnFramework(Framework _)
{
//var achievement = Achievement.Instance();
var uiState = UIState.Instance();
}
public bool IsUnlocked(uint itemId)
{
// Pseudo items are always unlocked.
if (itemId >= _items.ItemSheet.RowCount)
return true;
if (_unlocked.ContainsKey(itemId))
return true;
// TODO
return false;
}
public unsafe bool IsGameUnlocked(uint id, UnlockType type)
{
var uiState = UIState.Instance();
if (uiState == null)
return false;
return type switch
{
UnlockType.Cabinet => uiState->Cabinet.IsCabinetLoaded() && uiState->Cabinet.IsItemInCabinet((int)id),
UnlockType.Quest => uiState->IsUnlockLinkUnlockedOrQuestCompleted(id),
UnlockType.Achievement => false,
_ => false,
};
}
public void Dispose()
{
_clientState.Login -= OnLogin;
_framework.Update -= OnFramework;
}
public void Scan()
{
// TODO
}
public string ToFilename(FilenameService fileNames)
=> fileNames.UnlockFileItems;
public void Save()
=> _saveService.QueueSave(this);
public void Save(StreamWriter writer)
{ }
private void Load()
{
var file = ToFilename(_saveService.FileNames);
if (!File.Exists(file))
return;
_unlocked.Clear();
}
private void OnLogin(object? _, EventArgs _2)
=> Scan();
private static Dictionary<uint, (uint, UnlockType)> CreateUnlockData(DataManager gameData, ItemManager items)
{
var ret = new Dictionary<uint, (uint, UnlockType)>();
var cabinet = gameData.GetExcelSheet<Cabinet>()!;
foreach (var row in cabinet)
{
if (items.ItemService.AwaitedService.TryGetValue(row.Item.Row, out var item))
ret.TryAdd(item.Id, (row.RowId, UnlockType.Cabinet));
}
var gilShop = gameData.GetExcelSheet<GilShopItem>()!;
// TODO
return ret;
}
}