mirror of
https://github.com/Ottermandias/Glamourer.git
synced 2026-02-18 13:37:44 +01:00
.
This commit is contained in:
parent
60443f6a53
commit
5c003d8cd4
14 changed files with 566 additions and 198 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
113
Glamourer/Unlocks/UnlockDictionaryHelpers.cs
Normal file
113
Glamourer/Unlocks/UnlockDictionaryHelpers.cs
Normal 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);
|
||||
}
|
||||
50
Glamourer/Unlocks/UnlockRequirements.cs
Normal file
50
Glamourer/Unlocks/UnlockRequirements.cs
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue