This commit is contained in:
Ottermandias 2023-07-03 16:39:52 +02:00
parent 60443f6a53
commit 5c003d8cd4
14 changed files with 566 additions and 198 deletions

View file

@ -10,6 +10,7 @@ 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;
@ -17,8 +18,10 @@ namespace Glamourer.Unlocks;
public class CustomizeUnlockManager : IDisposable, ISavable
{
private readonly SaveService _saveService;
private readonly ClientState _clientState;
private readonly SaveService _saveService;
private readonly ClientState _clientState;
private readonly ObjectUnlocked _event;
private readonly Dictionary<uint, long> _unlocked = new();
public readonly IReadOnlyDictionary<CustomizeData, (uint Data, string Name)> Unlockable;
@ -27,11 +30,12 @@ public class CustomizeUnlockManager : IDisposable, ISavable
=> _unlocked;
public unsafe CustomizeUnlockManager(SaveService saveService, CustomizationService customizations, DataManager gameData,
ClientState clientState)
ClientState clientState, ObjectUnlocked @event)
{
SignatureHelper.Initialise(this);
_saveService = saveService;
_clientState = clientState;
_event = @event;
Unlockable = CreateUnlockableCustomizations(customizations, gameData);
Load();
_setUnlockLinkValueHook.Enable();
@ -51,13 +55,13 @@ public class CustomizeUnlockManager : IDisposable, ISavable
// All other customizations are not unlockable.
if (data.Index is not CustomizeIndex.Hairstyle and not CustomizeIndex.FacePaint)
{
time = DateTime.MinValue;
time = DateTimeOffset.MinValue;
return true;
}
if (!Unlockable.TryGetValue(data, out var pair))
{
time = DateTime.MinValue;
time = DateTimeOffset.MinValue;
return true;
}
@ -74,8 +78,9 @@ public class CustomizeUnlockManager : IDisposable, ISavable
}
_unlocked.TryAdd(pair.Data, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
Save();
time = DateTimeOffset.UtcNow;
_event.Invoke(ObjectUnlocked.Type.Customization, pair.Data, time);
Save();
return true;
}
@ -107,7 +112,10 @@ public class CustomizeUnlockManager : IDisposable, ISavable
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)
@ -141,6 +149,7 @@ public class CustomizeUnlockManager : IDisposable, ISavable
if (id != data || !_unlocked.TryAdd(id, time))
continue;
_event.Invoke(ObjectUnlocked.Type.Customization, id, DateTimeOffset.FromUnixTimeMilliseconds(time));
Save();
break;
}
@ -161,16 +170,11 @@ public class CustomizeUnlockManager : IDisposable, ISavable
=> _saveService.QueueSave(this);
public void Save(StreamWriter writer)
{ }
=> UnlockDictionaryHelpers.Save(writer, Unlocked);
private void Load()
{
var file = ToFilename(_saveService.FileNames);
if (!File.Exists(file))
return;
_unlocked.Clear();
}
=> 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,

View file

@ -7,6 +7,7 @@ using Dalamud.Game.ClientState;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Glamourer.Events;
using Glamourer.Services;
using Lumina.Excel.GeneratedSheets;
using Cabinet = Lumina.Excel.GeneratedSheets.Cabinet;
@ -15,10 +16,12 @@ 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 SaveService _saveService;
private readonly ItemManager _items;
private readonly ClientState _clientState;
private readonly Framework _framework;
private readonly ObjectUnlocked _event;
private readonly Dictionary<uint, long> _unlocked = new();
private bool _lastArmoireState;
@ -37,65 +40,20 @@ public class ItemUnlockManager : ISavable, IDisposable
Cabinet = 0x08,
}
public readonly record struct UnlockRequirements(uint Quest1, uint Quest2, uint Achievement, ushort State, UnlockType Type)
{
public override string ToString()
{
return Type switch
{
UnlockType.Quest1 => $"Quest {Quest1}",
UnlockType.Quest1 | UnlockType.Quest2 => $"Quests {Quest1} & {Quest2}",
UnlockType.Achievement => $"Achievement {Achievement}",
UnlockType.Quest1 | UnlockType.Achievement => $"Quest {Quest1} & Achievement {Achievement}",
UnlockType.Quest1 | UnlockType.Quest2 | UnlockType.Achievement => $"Quests {Quest1} & {Quest2}, Achievement {Achievement}",
UnlockType.Cabinet => $"Cabinet {Quest1}",
_ => string.Empty,
};
}
public unsafe bool IsUnlocked(ItemUnlockManager manager)
{
if (Type == 0)
return true;
var uiState = UIState.Instance();
if (uiState == null)
return false;
bool CheckQuest(uint quest)
=> uiState->IsUnlockLinkUnlockedOrQuestCompleted(quest);
// TODO ClientStructs
bool CheckAchievement(uint achievement)
=> false;
return Type switch
{
UnlockType.Quest1 => CheckQuest(Quest1),
UnlockType.Quest1 | UnlockType.Quest2 => CheckQuest(Quest1) && CheckQuest(Quest2),
UnlockType.Achievement => CheckAchievement(Achievement),
UnlockType.Quest1 | UnlockType.Achievement => CheckQuest(Quest1) && CheckAchievement(Achievement),
UnlockType.Quest1 | UnlockType.Quest2 | UnlockType.Achievement => CheckQuest(Quest1)
&& CheckQuest(Quest2)
&& CheckAchievement(Achievement),
UnlockType.Cabinet => uiState->Cabinet.IsCabinetLoaded() && uiState->Cabinet.IsItemInCabinet((int)Quest1),
_ => false,
};
}
}
public readonly IReadOnlyDictionary<uint, UnlockRequirements> Unlockable;
public IReadOnlyDictionary<uint, long> Unlocked
=> _unlocked;
public ItemUnlockManager(SaveService saveService, ItemManager items, ClientState clientState, DataManager gameData, Framework framework)
public ItemUnlockManager(SaveService saveService, ItemManager items, ClientState clientState, DataManager gameData, Framework framework,
ObjectUnlocked @event)
{
SignatureHelper.Initialise(this);
_saveService = saveService;
_items = items;
_clientState = clientState;
_framework = framework;
_event = @event;
Unlockable = CreateUnlockData(gameData, items);
Load();
_clientState.Login += OnLogin;
@ -166,7 +124,13 @@ public class ItemUnlockManager : ISavable, IDisposable
var time = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
bool AddItem(uint itemId)
=> _items.ItemService.AwaitedService.TryGetValue(itemId, out var equip) && _unlocked.TryAdd(equip.Id, time);
{
if (!_items.ItemService.AwaitedService.TryGetValue(itemId, out var equip) || !_unlocked.TryAdd(equip.Id, time))
return false;
_event.Invoke(ObjectUnlocked.Type.Item, equip.Id, DateTimeOffset.FromUnixTimeMilliseconds(time));
return true;
}
var mirageManager = MirageManager.Instance();
var changes = false;
@ -200,7 +164,7 @@ public class ItemUnlockManager : ISavable, IDisposable
var inventoryManager = InventoryManager.Instance();
if (inventoryManager != null)
{
var type = ScannableInventories[_currentInventory];
var type = ScannableInventories[_currentInventory];
var container = inventoryManager->GetInventoryContainer(type);
if (container != null && container->Loaded != 0 && _currentInventoryIndex < container->Size)
{
@ -217,6 +181,7 @@ public class ItemUnlockManager : ISavable, IDisposable
_currentInventoryIndex = 0;
}
}
if (changes)
Save();
}
@ -239,8 +204,12 @@ public class ItemUnlockManager : ISavable, IDisposable
if (IsGameUnlocked(itemId))
{
time = DateTimeOffset.UtcNow;
_unlocked.TryAdd(itemId, time.ToUnixTimeMilliseconds());
Save();
if (_unlocked.TryAdd(itemId, time.ToUnixTimeMilliseconds()))
{
_event.Invoke(ObjectUnlocked.Type.Item, itemId, time);
Save();
}
return true;
}
@ -269,8 +238,11 @@ public class ItemUnlockManager : ISavable, IDisposable
var changes = false;
foreach (var (itemId, unlock) in Unlockable)
{
if (unlock.IsUnlocked(this))
changes |= _unlocked.TryAdd(itemId, time);
if (unlock.IsUnlocked(this) && _unlocked.TryAdd(itemId, time))
{
_event.Invoke(ObjectUnlocked.Type.Item, itemId, DateTimeOffset.FromUnixTimeMilliseconds(time));
changes = true;
}
}
// TODO inventories
@ -286,16 +258,11 @@ public class ItemUnlockManager : ISavable, IDisposable
=> _saveService.DelaySave(this, TimeSpan.FromSeconds(10));
public void Save(StreamWriter writer)
{ }
=> UnlockDictionaryHelpers.Save(writer, Unlocked);
private void Load()
{
var file = ToFilename(_saveService.FileNames);
if (!File.Exists(file))
return;
_unlocked.Clear();
}
=> UnlockDictionaryHelpers.Load(ToFilename(_saveService.FileNames), _unlocked,
id => _items.ItemService.AwaitedService.TryGetValue(id, out _), "item");
private void OnLogin(object? _, EventArgs _2)
=> Scan();

View file

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.IO;
using Dalamud.Interface.Internal.Notifications;
namespace Glamourer.Unlocks;
public static class UnlockDictionaryHelpers
{
public const int Magic = 0x00C0FFEE;
public const int Version = 1;
public static void Save(StreamWriter writer, IReadOnlyDictionary<uint, long> data)
{
// Not using by choice, as this would close the stream prematurely.
var b = new BinaryWriter(writer.BaseStream);
b.Write(Magic);
b.Write(Version);
b.Write(data.Count);
foreach (var (id, timestamp) in data)
{
b.Write(id);
b.Write(timestamp);
}
b.Flush();
}
public static void Load(string filePath, Dictionary<uint, long> data, Func<uint, bool> validate, string type)
{
data.Clear();
if (!File.Exists(filePath))
return;
try
{
using var fileStream = File.OpenRead(filePath);
using var b = new BinaryReader(fileStream);
var magic = b.ReadUInt32();
bool revertEndian;
switch (magic)
{
case 0x00C0FFEE:
revertEndian = false;
break;
case 0xEEFFC000:
revertEndian = true;
break;
default:
Glamourer.Chat.NotificationMessage($"Loading unlocked {type}s failed: Invalid magic number.", "Warning",
NotificationType.Warning);
return;
}
var version = b.ReadInt32();
var skips = 0;
var now = DateTimeOffset.UtcNow;
switch (version)
{
case Version:
var count = b.ReadInt32();
data.EnsureCapacity(count);
for (var i = 0; i < count; ++i)
{
var id = b.ReadUInt32();
var timestamp = b.ReadInt64();
if (revertEndian)
{
id = RevertEndianness(id);
timestamp = (long)RevertEndianness(timestamp);
}
var date = DateTimeOffset.FromUnixTimeMilliseconds(timestamp);
if (!validate(id)
|| date < DateTimeOffset.UnixEpoch
|| date > now
|| !data.TryAdd(id, timestamp))
++skips;
}
if (skips > 0)
Glamourer.Chat.NotificationMessage($"Skipped {skips} unlocked {type}s while loading unlocked {type}s.", "Warning",
NotificationType.Warning);
break;
default:
Glamourer.Chat.NotificationMessage($"Loading unlocked {type}s failed: Version {version} is unknown.", "Warning",
NotificationType.Warning);
return;
}
Glamourer.Log.Debug($"[UnlockManager] Loaded {data.Count} unlocked {type}s.");
}
catch (Exception ex)
{
Glamourer.Chat.NotificationMessage(ex, $"Loading unlocked {type}s failed: Unknown Error.", $"Loading unlocked {type}s failed:\n",
"Error", NotificationType.Error);
}
}
private static uint RevertEndianness(uint value)
=> ((value & 0x000000FFU) << 24) | ((value & 0x0000FF00U) << 8) | ((value & 0x00FF0000U) >> 8) | ((value & 0xFF000000U) >> 24);
private static ulong RevertEndianness(long value)
=> (((ulong)value & 0x00000000000000FFU) << 56)
| (((ulong)value & 0x000000000000FF00U) << 40)
| (((ulong)value & 0x0000000000FF0000U) << 24)
| (((ulong)value & 0x00000000FF000000U) << 8)
| (((ulong)value & 0x000000FF00000000U) >> 8)
| (((ulong)value & 0x0000FF0000000000U) >> 24)
| (((ulong)value & 0x00FF000000000000U) >> 40)
| (((ulong)value & 0xFF00000000000000U) >> 56);
}

View file

@ -0,0 +1,50 @@
using FFXIVClientStructs.FFXIV.Client.Game.UI;
namespace Glamourer.Unlocks;
public readonly record struct UnlockRequirements(uint Quest1, uint Quest2, uint Achievement, ushort State, ItemUnlockManager.UnlockType Type)
{
public override string ToString()
{
return Type switch
{
ItemUnlockManager.UnlockType.Quest1 => $"Quest {Quest1}",
ItemUnlockManager.UnlockType.Quest1 | ItemUnlockManager.UnlockType.Quest2 => $"Quests {Quest1} & {Quest2}",
ItemUnlockManager.UnlockType.Achievement => $"Achievement {Achievement}",
ItemUnlockManager.UnlockType.Quest1 | ItemUnlockManager.UnlockType.Achievement => $"Quest {Quest1} & Achievement {Achievement}",
ItemUnlockManager.UnlockType.Quest1 | ItemUnlockManager.UnlockType.Quest2 | ItemUnlockManager.UnlockType.Achievement => $"Quests {Quest1} & {Quest2}, Achievement {Achievement}",
ItemUnlockManager.UnlockType.Cabinet => $"Cabinet {Quest1}",
_ => string.Empty,
};
}
public unsafe bool IsUnlocked(ItemUnlockManager manager)
{
if (Type == 0)
return true;
var uiState = UIState.Instance();
if (uiState == null)
return false;
bool CheckQuest(uint quest)
=> uiState->IsUnlockLinkUnlockedOrQuestCompleted(quest);
// TODO ClientStructs
bool CheckAchievement(uint achievement)
=> false;
return Type switch
{
ItemUnlockManager.UnlockType.Quest1 => CheckQuest(Quest1),
ItemUnlockManager.UnlockType.Quest1 | ItemUnlockManager.UnlockType.Quest2 => CheckQuest(Quest1) && CheckQuest(Quest2),
ItemUnlockManager.UnlockType.Achievement => CheckAchievement(Achievement),
ItemUnlockManager.UnlockType.Quest1 | ItemUnlockManager.UnlockType.Achievement => CheckQuest(Quest1) && CheckAchievement(Achievement),
ItemUnlockManager.UnlockType.Quest1 | ItemUnlockManager.UnlockType.Quest2 | ItemUnlockManager.UnlockType.Achievement => CheckQuest(Quest1)
&& CheckQuest(Quest2)
&& CheckAchievement(Achievement),
ItemUnlockManager.UnlockType.Cabinet => uiState->Cabinet.IsCabinetLoaded() && uiState->Cabinet.IsItemInCabinet((int)Quest1),
_ => false,
};
}
}