This commit is contained in:
Ottermandias 2023-06-09 17:57:40 +02:00
parent 7710cfadfa
commit 2d6fd6015d
88 changed files with 2304 additions and 383 deletions

View file

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.IO;
using OtterGui.Classes;
using OtterGui.Log;
namespace Glamourer.Services;
public class BackupService
{
public BackupService(Logger logger, FilenameService fileNames)
{
var files = GlamourerFiles(fileNames);
Backup.CreateBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files);
}
/// <summary> Collect all relevant files for glamourer configuration. </summary>
private static IReadOnlyList<FileInfo> GlamourerFiles(FilenameService fileNames)
{
var list = new List<FileInfo>(16)
{
new(fileNames.ConfigFile),
new(fileNames.DesignFileSystem),
new(fileNames.MigrationDesignFile),
};
list.AddRange(fileNames.Designs());
return list;
}
}

View file

@ -0,0 +1,36 @@
using System;
using Dalamud.Game.Command;
using Glamourer.Gui;
namespace Glamourer.Services;
public class CommandService : IDisposable
{
private const string HelpString = "[Copy|Apply|Save],[Name or PlaceHolder],<Name for Save>";
private const string MainCommandString = "/glamourer";
private const string ApplyCommandString = "/glamour";
private readonly CommandManager _commands;
private readonly Interface _interface;
public CommandService(CommandManager commands, Interface ui)
{
_commands = commands;
_interface = ui;
_commands.AddHandler(MainCommandString, new CommandInfo(OnGlamourer) { HelpMessage = "Open or close the Glamourer window." });
_commands.AddHandler(ApplyCommandString, new CommandInfo(OnGlamour) { HelpMessage = $"Use Glamourer Functions: {HelpString}" });
}
public void Dispose()
{
_commands.RemoveHandler(MainCommandString);
_commands.RemoveHandler(ApplyCommandString);
}
private void OnGlamourer(string command, string arguments)
=> _interface.Toggle();
private void OnGlamour(string command, string arguments)
{ }
}

View file

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.IO;
using Dalamud.Plugin;
using Glamourer.Designs;
namespace Glamourer.Services;
public class FilenameService
{
public readonly string ConfigDirectory;
public readonly string ConfigFile;
public readonly string DesignFileSystem;
public readonly string MigrationDesignFile;
public readonly string DesignDirectory;
public FilenameService(DalamudPluginInterface pi)
{
ConfigDirectory = pi.ConfigDirectory.FullName;
ConfigFile = pi.ConfigFile.FullName;
DesignFileSystem = Path.Combine(ConfigDirectory, "sort_order.json");
MigrationDesignFile = Path.Combine(ConfigDirectory, "Designs.json");
DesignDirectory = Path.Combine(ConfigDirectory, "designs");
}
public IEnumerable<FileInfo> Designs()
{
if (!Directory.Exists(DesignDirectory))
yield break;
foreach (var file in Directory.EnumerateFiles(DesignDirectory, "*.json", SearchOption.TopDirectoryOnly))
yield return new FileInfo(file);
}
public string DesignFile(Design design)
=> DesignFile(design.Identifier.ToString());
public string DesignFile(string identifier)
=> Path.Combine(DesignDirectory, $"{identifier}.json");
}

View file

@ -0,0 +1,189 @@
using System;
using System.Diagnostics;
using System.Linq;
using Dalamud.Data;
using Dalamud.Plugin;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using Lumina.Text;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.Services;
public class ItemManager : IDisposable
{
public const string Nothing = "Nothing";
public const string SmallClothesNpc = "Smallclothes (NPC)";
public const ushort SmallClothesNpcModel = 9903;
private readonly Configuration _config;
public readonly IdentifierService IdentifierService;
public readonly ExcelSheet<Item> ItemSheet;
public readonly StainData Stains;
public readonly ItemService ItemService;
public readonly RestrictedGear RestrictedGear;
public ItemManager(DalamudPluginInterface pi, DataManager gameData, IdentifierService identifierService, ItemService itemService, Configuration config)
{
_config = config;
ItemSheet = gameData.GetExcelSheet<Item>()!;
IdentifierService = identifierService;
Stains = new StainData(pi, gameData, gameData.Language);
ItemService = itemService;
RestrictedGear = new RestrictedGear(pi, gameData.Language, gameData);
DefaultSword = ItemSheet.GetRow(1601)!; // Weathered Shortsword
}
public void Dispose()
{
Stains.Dispose();
RestrictedGear.Dispose();
}
public (bool, CharacterArmor) ResolveRestrictedGear(CharacterArmor armor, EquipSlot slot, Race race, Gender gender)
{
if (_config.UseRestrictedGearProtection)
return RestrictedGear.ResolveRestricted(armor, slot, race, gender);
return (false, armor);
}
public readonly Item DefaultSword;
public static uint NothingId(EquipSlot slot)
=> uint.MaxValue - 128 - (uint)slot.ToSlot();
public static uint SmallclothesId(EquipSlot slot)
=> uint.MaxValue - 256 - (uint)slot.ToSlot();
public static uint NothingId(FullEquipType type)
=> uint.MaxValue - 384 - (uint)type;
public static Designs.Item NothingItem(EquipSlot slot)
{
Debug.Assert(slot.IsEquipment() || slot.IsAccessory(), $"Called {nameof(NothingItem)} on {slot}.");
return new Designs.Item(Nothing, NothingId(slot), CharacterArmor.Empty);
}
public static Designs.Weapon NothingItem(FullEquipType type)
{
Debug.Assert(type.ToSlot() == EquipSlot.OffHand, $"Called {nameof(NothingItem)} on {type}.");
return new Designs.Weapon(Nothing, NothingId(type), CharacterWeapon.Empty, type);
}
public static Designs.Item SmallClothesItem(EquipSlot slot)
{
Debug.Assert(slot.IsEquipment(), $"Called {nameof(SmallClothesItem)} on {slot}.");
return new Designs.Item(SmallClothesNpc, SmallclothesId(slot), new CharacterArmor(SmallClothesNpcModel, 1, 0));
}
public (bool Valid, SetId Id, byte Variant, string ItemName) Resolve(EquipSlot slot, uint itemId, Item? item = null)
{
slot = slot.ToSlot();
if (itemId == NothingId(slot))
return (true, 0, 0, Nothing);
if (itemId == SmallclothesId(slot))
return (true, SmallClothesNpcModel, 1, SmallClothesNpc);
if (item == null || item.RowId != itemId)
item = ItemSheet.GetRow(itemId);
if (item == null)
return (false, 0, 0, string.Intern($"Unknown #{itemId}"));
if (item.ToEquipType().ToSlot() != slot)
return (false, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"));
return (true, (SetId)item.ModelMain, (byte)(item.ModelMain >> 16), string.Intern(item.Name.ToDalamudString().TextValue));
}
public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId, Item? item = null)
{
if (item == null || item.RowId != itemId)
item = ItemSheet.GetRow(itemId);
if (item == null)
return (false, 0, 0, 0, string.Intern($"Unknown #{itemId}"), FullEquipType.Unknown);
var type = item.ToEquipType();
if (type.ToSlot() != EquipSlot.MainHand)
return (false, 0, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"), type);
return (true, (SetId)item.ModelMain, (WeaponType)(item.ModelMain >> 16), (byte)(item.ModelMain >> 32),
string.Intern(item.Name.ToDalamudString().TextValue), type);
}
public (bool Valid, SetId Id, WeaponType Weapon, byte Variant, string ItemName, FullEquipType Type) Resolve(uint itemId,
FullEquipType mainType, Item? item = null)
{
var offType = mainType.Offhand();
if (itemId == NothingId(offType))
return (true, 0, 0, 0, Nothing, offType);
if (item == null || item.RowId != itemId)
item = ItemSheet.GetRow(itemId);
if (item == null)
return (false, 0, 0, 0, string.Intern($"Unknown #{itemId}"), FullEquipType.Unknown);
var type = item.ToEquipType();
if (offType != type)
return (false, 0, 0, 0, string.Intern($"Invalid ({item.Name.ToDalamudString()})"), type);
var (m, w, v) = offType.ToSlot() == EquipSlot.MainHand
? ((SetId)item.ModelSub, (WeaponType)(item.ModelSub >> 16), (byte)(item.ModelSub >> 32))
: ((SetId)item.ModelMain, (WeaponType)(item.ModelMain >> 16), (byte)(item.ModelMain >> 32));
return (true, m, w, v, string.Intern(item.Name.ToDalamudString().TextValue), type);
}
public (bool Valid, uint ItemId, string ItemName) Identify(EquipSlot slot, SetId id, byte variant)
{
slot = slot.ToSlot();
if (!slot.IsEquipmentPiece())
return (false, 0, string.Intern($"Unknown ({id.Value}-{variant})"));
switch (id.Value)
{
case 0: return (true, NothingId(slot), Nothing);
case SmallClothesNpcModel: return (true, SmallclothesId(slot), SmallClothesNpc);
default:
var item = IdentifierService.AwaitedService.Identify(id, variant, slot).FirstOrDefault();
return item == null
? (false, 0, string.Intern($"Unknown ({id.Value}-{variant})"))
: (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue));
}
}
public (bool Valid, uint ItemId, string ItemName, FullEquipType Type) Identify(EquipSlot slot, SetId id, WeaponType type, byte variant,
FullEquipType mainhandType = FullEquipType.Unknown)
{
switch (slot)
{
case EquipSlot.MainHand:
{
var item = IdentifierService.AwaitedService.Identify(id, type, variant, slot).FirstOrDefault();
return item != null
? (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue), item.ToEquipType())
: (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), mainhandType);
}
case EquipSlot.OffHand:
{
var weaponType = mainhandType.Offhand();
if (id.Value == 0)
return (true, NothingId(weaponType), Nothing, weaponType);
var item = IdentifierService.AwaitedService.Identify(id, type, variant, slot).FirstOrDefault();
return item != null
? (true, item.RowId, string.Intern(item.Name.ToDalamudString().TextValue), item.ToEquipType())
: (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"),
weaponType);
}
default: return (false, 0, string.Intern($"Unknown ({id.Value}-{type.Value}-{variant})"), FullEquipType.Unknown);
}
}
}

View file

@ -0,0 +1,97 @@
using System;
using System.IO;
using System.Text;
using OtterGui.Classes;
using OtterGui.Log;
namespace Glamourer.Services;
/// <summary>
/// Any file type that we want to save via SaveService.
/// </summary>
public interface ISavable
{
/// <summary> The full file name of a given object. </summary>
public string ToFilename(FilenameService fileNames);
/// <summary> Write the objects data to the given stream writer. </summary>
public void Save(StreamWriter writer);
/// <summary> An arbitrary message printed to Debug before saving. </summary>
public string LogName(string fileName)
=> fileName;
public string TypeName
=> GetType().Name;
}
public class SaveService
{
private readonly Logger _log;
private readonly FrameworkManager _framework;
public readonly FilenameService FileNames;
public SaveService(Logger log, FrameworkManager framework, FilenameService fileNames)
{
_log = log;
_framework = framework;
FileNames = fileNames;
}
/// <summary> Queue a save for the next framework tick. </summary>
public void QueueSave(ISavable value)
{
var file = value.ToFilename(FileNames);
_framework.RegisterOnTick(value.GetType().Name + file, () =>
{
ImmediateSave(value);
});
}
/// <summary> Immediately trigger a save. </summary>
public void ImmediateSave(ISavable value)
{
var name = value.ToFilename(FileNames);
try
{
if (name.Length == 0)
{
throw new Exception("Invalid object returned empty filename.");
}
_log.Debug($"Saving {value.TypeName} {value.LogName(name)}...");
var file = new FileInfo(name);
file.Directory?.Create();
using var s = file.Exists ? file.Open(FileMode.Truncate) : file.Open(FileMode.CreateNew);
using var w = new StreamWriter(s, Encoding.UTF8);
value.Save(w);
}
catch (Exception ex)
{
_log.Error($"Could not save {value.GetType().Name} {value.LogName(name)}:\n{ex}");
}
}
public void ImmediateDelete(ISavable value)
{
var name = value.ToFilename(FileNames);
try
{
if (name.Length == 0)
{
throw new Exception("Invalid object returned empty filename.");
}
if (!File.Exists(name))
return;
_log.Information($"Deleting {value.GetType().Name} {value.LogName(name)}...");
File.Delete(name);
}
catch (Exception ex)
{
_log.Error($"Could not delete {value.GetType().Name} {value.LogName(name)}:\n{ex}");
}
}
}

View file

@ -0,0 +1,80 @@
using Dalamud.Plugin;
using Glamourer.Api;
using Glamourer.Designs;
using Glamourer.Gui;
using Glamourer.Interop;
using Glamourer.State;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes;
using OtterGui.Log;
namespace Glamourer.Services;
public static class ServiceManager
{
public static ServiceProvider CreateProvider(DalamudPluginInterface pi, Logger log)
{
var services = new ServiceCollection()
.AddSingleton(log)
.AddDalamud(pi)
.AddMeta()
.AddConfig()
.AddPenumbra()
.AddInterop()
.AddGameData()
.AddDesigns()
.AddInterface()
.AddApi();
return services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true });
}
private static IServiceCollection AddDalamud(this IServiceCollection services, DalamudPluginInterface pi)
{
new DalamudServices(pi).AddServices(services);
return services;
}
private static IServiceCollection AddMeta(this IServiceCollection services)
=> services.AddSingleton<FilenameService>()
.AddSingleton<SaveService>()
.AddSingleton<FrameworkManager>()
.AddSingleton<ChatService>();
private static IServiceCollection AddConfig(this IServiceCollection services)
=> services.AddSingleton<Configuration>()
.AddSingleton<BackupService>();
private static IServiceCollection AddPenumbra(this IServiceCollection services)
=> services.AddSingleton<PenumbraAttach>();
private static IServiceCollection AddGameData(this IServiceCollection services)
=> services.AddSingleton<IdentifierService>()
.AddSingleton<ActorService>()
.AddSingleton<ItemService>()
.AddSingleton<ItemManager>()
.AddSingleton<CustomizationService>();
private static IServiceCollection AddInterop(this IServiceCollection services)
=> services.AddSingleton<ChangeCustomizeService>()
.AddSingleton<JobService>()
.AddSingleton<UpdateSlotService>()
.AddSingleton<VisorService>()
.AddSingleton<WeaponService>()
.AddSingleton<ObjectManager>();
private static IServiceCollection AddDesigns(this IServiceCollection services)
=> services.AddSingleton<DesignManager>()
.AddSingleton<DesignFileSystem>()
.AddSingleton<ActiveDesign.Manager>()
.AddSingleton<FixedDesignManager>()
.AddSingleton<RedrawManager>();
private static IServiceCollection AddInterface(this IServiceCollection services)
=> services.AddSingleton<Interface>()
.AddSingleton<GlamourerWindowSystem>();
private static IServiceCollection AddApi(this IServiceCollection services)
=> services.AddSingleton<CommandService>()
.AddSingleton<Glamourer.GlamourerIpc>();
}

View file

@ -0,0 +1,105 @@
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState;
using Dalamud.Game.Gui;
using Dalamud.Plugin;
using Penumbra.GameData.Actors;
using System;
using System.Threading.Tasks;
using Dalamud.Game;
using Glamourer.Api;
using Glamourer.Customization;
using Penumbra.GameData.Data;
using Penumbra.GameData;
namespace Glamourer.Services;
public abstract class AsyncServiceWrapper<T>
{
public string Name { get; }
public T? Service { get; private set; }
public T AwaitedService
{
get
{
_task?.Wait();
return Service!;
}
}
public bool Valid
=> Service != null && !_isDisposed;
public event Action? FinishedCreation;
private Task? _task;
private bool _isDisposed;
protected AsyncServiceWrapper(string name, Func<T> factory)
{
Name = name;
_task = Task.Run(() =>
{
var service = factory();
if (_isDisposed)
{
if (service is IDisposable d)
d.Dispose();
}
else
{
Service = service;
Glamourer.Log.Verbose($"[{Name}] Created.");
_task = null;
}
});
_task.ContinueWith((t, x) =>
{
if (!_isDisposed)
FinishedCreation?.Invoke();
}, null);
}
public void Dispose()
{
if (_isDisposed)
return;
_isDisposed = true;
_task = null;
if (Service is IDisposable d)
d.Dispose();
Glamourer.Log.Verbose($"[{Name}] Disposed.");
}
}
public sealed class IdentifierService : AsyncServiceWrapper<IObjectIdentifier>
{
public IdentifierService(DalamudPluginInterface pi, DataManager data)
: base(nameof(IdentifierService), () => Penumbra.GameData.GameData.GetIdentifier(pi, data))
{ }
}
public sealed class ItemService : AsyncServiceWrapper<ItemData>
{
public ItemService(DalamudPluginInterface pi, DataManager gameData)
: base(nameof(ItemService), () => new ItemData(pi, gameData, gameData.Language))
{ }
}
public sealed class ActorService : AsyncServiceWrapper<ActorManager>
{
public ActorService(DalamudPluginInterface pi, ObjectTable objects, ClientState clientState, Framework framework, DataManager gameData,
GameGui gui, PenumbraAttach penumbra)
: base(nameof(ActorService),
() => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)penumbra.CutsceneParent(idx)))
{ }
}
public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationManager>
{
public CustomizationService(DalamudPluginInterface pi, DataManager gameData)
: base(nameof(CustomizationService), () => CustomizationManager.Create(pi, gameData))
{ }
}