Glamourer/Glamourer/Unlocks/CustomizeUnlockManager.cs

217 lines
7.6 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Services;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer.Unlocks;
public class CustomizeUnlockManager : IDisposable, ISavable
{
private readonly SaveService _saveService;
private readonly IClientState _clientState;
private readonly ObjectUnlocked _event;
private readonly Dictionary<uint, long> _unlocked = new();
public readonly IReadOnlyDictionary<CustomizeData, (uint Data, string Name)> Unlockable;
public IReadOnlyDictionary<uint, long> Unlocked
=> _unlocked;
public CustomizeUnlockManager(SaveService saveService, CustomizationService customizations, IDataManager gameData,
IClientState clientState, ObjectUnlocked @event)
{
SignatureHelper.Initialise(this);
_saveService = saveService;
_clientState = clientState;
_event = @event;
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)
{
// All other customizations are not unlockable.
if (data.Index is not CustomizeIndex.Hairstyle and not CustomizeIndex.FacePaint)
{
time = DateTimeOffset.MinValue;
return true;
}
if (!Unlockable.TryGetValue(data, out var pair))
{
time = DateTimeOffset.MinValue;
return true;
}
if (_unlocked.TryGetValue(pair.Data, out var t))
{
time = DateTimeOffset.FromUnixTimeMilliseconds(t);
return true;
}
if (!IsUnlockedGame(pair.Data))
{
time = DateTimeOffset.MaxValue;
return false;
}
_unlocked.TryAdd(pair.Data, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
time = DateTimeOffset.UtcNow;
_event.Invoke(ObjectUnlocked.Type.Customization, pair.Data, time);
Save();
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))
{
_event.Invoke(ObjectUnlocked.Type.Customization, id, DateTimeOffset.FromUnixTimeMilliseconds(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;
_event.Invoke(ObjectUnlocked.Type.Customization, id, DateTimeOffset.FromUnixTimeMilliseconds(time));
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)
=> UnlockDictionaryHelpers.Save(writer, Unlocked);
private void Load()
=> UnlockDictionaryHelpers.Load(ToFilename(_saveService.FileNames), _unlocked, id => Unlockable.Any(c => c.Value.Data == id),
"customization");
/// <summary> Create a list of all unlockable hairstyles and facepaints. </summary>
private static Dictionary<CustomizeData, (uint Data, string Name)> CreateUnlockableCustomizations(CustomizationService customizations,
IDataManager 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;
}
}