Code commit

This commit is contained in:
RisaDev
2024-01-06 01:21:41 +03:00
parent a7d7297c59
commit a486dd2c96
90 changed files with 11576 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
using OtterGui.Classes;
using OtterGui.Log;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace CustomizePlus.Core.Services;
public class BackupService
{
private readonly Logger _logger;
private readonly FilenameService _filenameService;
private readonly DirectoryInfo _configDirectory;
private readonly IReadOnlyList<FileInfo> _fileNames;
public BackupService(Logger logger, FilenameService filenameService)
{
_logger = logger;
_filenameService = filenameService;
_fileNames = PluginFiles(_filenameService);
_configDirectory = new DirectoryInfo(_filenameService.ConfigDirectory);
Backup.CreateAutomaticBackup(logger, _configDirectory, _fileNames);
}
/// <summary>
/// Create a permanent backup with a given name for migrations.
/// </summary>
public void CreateMigrationBackup(string name)
=> Backup.CreatePermanentBackup(_logger, _configDirectory, _fileNames, name);
/// <summary>
/// Create backup for all version 3 configuration files
/// </summary>
public void CreateV3Backup(string name = "v3_to_v4_migration")
{
var list = new List<FileInfo>(16) { new(_filenameService.ConfigFile) };
list.AddRange(Directory.EnumerateFiles(_filenameService.ConfigDirectory, "*.profile", SearchOption.TopDirectoryOnly).Select(x => new FileInfo(x)));
Backup.CreatePermanentBackup(_logger, _configDirectory, list, name);
}
/// <summary>
/// Collect all relevant files for plugin configuration.
/// </summary>
private static IReadOnlyList<FileInfo> PluginFiles(FilenameService fileNames)
{
var list = new List<FileInfo>(16)
{
new(fileNames.ConfigFile),
new(fileNames.ProfileFileSystem),
new(fileNames.TemplateFileSystem)
};
list.AddRange(fileNames.Profiles());
list.AddRange(fileNames.Templates());
return list;
}
}

View File

@@ -0,0 +1,182 @@
using Dalamud.Game.Command;
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Plugin.Services;
using OtterGui.Classes;
using System;
using System.Linq;
using OtterGui.Log;
using Dalamud.Game.Text.SeStringHandling;
using CustomizePlus.Profiles;
using CustomizePlus.Game.Services;
using CustomizePlus.UI.Windows.MainWindow.Tabs.Templates;
using CustomizePlus.UI.Windows.MainWindow;
namespace CustomizePlus.Core.Services;
public class CommandService : IDisposable
{
private readonly ProfileManager _profileManager;
private readonly GameObjectService _gameObjectService;
private readonly ICommandManager _commandManager;
private readonly Logger _logger;
private readonly ChatService _chatService;
private readonly MainWindow _mainWindow;
private readonly BoneEditorPanel _boneEditorPanel;
private readonly MessageService _messageService;
private static readonly string[] Commands = new[] { "/customize", "/c+" };
public CommandService(
ProfileManager profileManager,
GameObjectService gameObjectService,
ICommandManager commandManager,
MainWindow mainWindow,
ChatService chatService,
BoneEditorPanel boneEditorPanel,
Logger logger,
MessageService messageService)
{
_profileManager = profileManager;
_gameObjectService = gameObjectService;
_commandManager = commandManager;
_logger = logger;
_chatService = chatService;
_mainWindow = mainWindow;
_boneEditorPanel = boneEditorPanel;
_messageService = messageService;
foreach (var command in Commands)
{
_commandManager.AddHandler(command, new CommandInfo(OnMainCommand) { HelpMessage = "Toggles main plugin window if no commands passed. Use \"/customize help\" for list of available commands." });
}
chatService.PrintInChat($"Started!"); //safe to assume at this point we have successfully initialized
}
public void Dispose()
{
foreach (var command in Commands)
{
_commandManager.RemoveHandler(command);
}
}
private void OnMainCommand(string command, string arguments)
{
if (_boneEditorPanel.IsEditorActive)
{
_messageService.NotificationMessage("Customize+ commands cannot be used when editor is active", NotificationType.Error);
return;
}
var argumentList = arguments.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
var argument = argumentList.Length == 2 ? argumentList[1] : string.Empty;
if (arguments.Length > 0)
{
switch (argumentList[0].ToLowerInvariant())
{
case "apply":
Apply(argument);
return;
case "toggle":
Apply(argument, true);
return;
default:
case "help":
PrintHelp(argumentList[0]);
return;
}
}
_mainWindow.Toggle();
}
private bool PrintHelp(string argument)
{
if (!string.Equals(argument, "help", StringComparison.OrdinalIgnoreCase) && argument != "?")
_chatService.PrintInChat(new SeStringBuilder().AddText("The given argument ").AddRed(argument, true)
.AddText(" is not valid. Valid arguments are:").BuiltString);
else
_chatService.PrintInChat(new SeStringBuilder().AddText("Valid arguments for /customize are:").BuiltString);
_chatService.PrintInChat(new SeStringBuilder().AddCommand("apply", "Applies a given profile for a given character. Use without arguments for help.")
.BuiltString);
_chatService.PrintInChat(new SeStringBuilder().AddCommand("toggle", "Toggles a given profile for a given character. Use without arguments for help.")
.BuiltString);
return true;
}
private void Apply(string argument, bool toggle = false)
{
var argumentList = argument.Split(',', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (argumentList.Length != 2)
{
_chatService.PrintInChat(new SeStringBuilder().AddText($"Usage: /customize {(toggle ? "toggle" : "apply")} ").AddBlue("Character Name", true)
.AddText(",")
.AddRed("Profile Name", true)
.BuiltString);
_chatService.PrintInChat(new SeStringBuilder().AddText(" 》 ")
.AddBlue("Character Name", true).AddText("can be either full character name or one of the following:").BuiltString);
_chatService.PrintInChat(new SeStringBuilder().AddText(" 》 To apply to yourself: ").AddBlue("<me>").AddText(", ").AddBlue("self").BuiltString);
_chatService.PrintInChat(new SeStringBuilder().AddText(" 》 To apply to target: ").AddBlue("<t>").AddText(", ").AddBlue("target").BuiltString);
return;
}
string charaName = "", profName = "";
try
{
(charaName, profName) = argumentList switch { var a => (a[0].Trim(), a[1].Trim()) };
charaName = charaName switch
{
"<me>" => _gameObjectService.GetCurrentPlayerName() ?? string.Empty,
"self" => _gameObjectService.GetCurrentPlayerName() ?? string.Empty,
"<t>" => _gameObjectService.GetCurrentPlayerTargetName() ?? string.Empty,
"target" => _gameObjectService.GetCurrentPlayerTargetName() ?? string.Empty,
_ => charaName,
};
if (!_profileManager.Profiles.Any())
{
_chatService.PrintInChat(
$"Can't {(toggle ? "toggle" : "apply")} profile \"{profName}\" for character \"{charaName}\" because no profiles exist", ChatService.ChatMessageColor.Error);
return;
}
if (_profileManager.Profiles.Count(x => x.Name == profName && x.CharacterName == charaName) > 1)
{
_logger.Warning(
$"Found more than one profile matching profile \"{profName}\" and character \"{charaName}\". Using first match.");
}
var outProf = _profileManager.Profiles.FirstOrDefault(x => x.Name == profName && x.CharacterName == charaName);
if (outProf == null)
{
_chatService.PrintInChat(
$"Can't {(toggle ? "toggle" : "apply")} profile \"{(string.IsNullOrWhiteSpace(profName) ? "empty (none provided)" : profName)}\" " +
$"for Character \"{(string.IsNullOrWhiteSpace(charaName) ? "empty (none provided)" : charaName)}\"\n" +
"Check if the profile and character names were provided correctly and said profile exists for chosen character", ChatService.ChatMessageColor.Error);
return;
}
if (!toggle)
_profileManager.SetEnabled(outProf, true);
else
_profileManager.SetEnabled(outProf, !outProf.Enabled);
_chatService.PrintInChat(
$"{outProf.Name} was successfully {(toggle ? "toggled" : "applied")} for {outProf.CharacterName}", ChatService.ChatMessageColor.Info);
}
catch (Exception e)
{
_chatService.PrintInChat($"Error while {(toggle ? "toggling" : "applying")} profile, details are available in dalamud log", ChatService.ChatMessageColor.Error);
_logger.Error($"Error {(toggle ? "toggling" : "applying")} profile by command: \n" +
$"Profile name \"{(string.IsNullOrWhiteSpace(profName) ? "empty (none provided)" : profName)}\"\n" +
$"Character name \"{(string.IsNullOrWhiteSpace(charaName) ? "empty (none provided)" : charaName)}\"\n" +
$"Error: {e}");
}
}
}

View File

@@ -0,0 +1,93 @@
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects;
using Dalamud.IoC;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection;
namespace CustomizePlus.Core.Services;
public class DalamudServices
{
[PluginService]
[RequiredVersion("1.0")]
public DalamudPluginInterface PluginInterface { get; private set; } = null!;
[PluginService]
[RequiredVersion("1.0")]
public ISigScanner SigScanner { get; private set; } = null!;
[PluginService]
public IFramework Framework { get; private set; } = null!;
[PluginService]
[RequiredVersion("1.0")]
public IObjectTable ObjectTable { get; private set; } = null!;
[PluginService]
[RequiredVersion("1.0")]
public ICommandManager CommandManager { get; private set; } = null!;
[PluginService]
[RequiredVersion("1.0")]
public IChatGui ChatGui { get; private set; } = null!;
[PluginService]
[RequiredVersion("1.0")]
public IClientState ClientState { get; private set; } = null!;
[PluginService]
[RequiredVersion("1.0")]
public IGameGui GameGui { get; private set; } = null!;
[PluginService]
[RequiredVersion("1.0")]
internal IGameInteropProvider Hooker { get; private set; } = null!;
[PluginService]
[RequiredVersion("1.0")]
public IKeyState KeyState { get; private set; } = null!;
//GameData
[PluginService]
[RequiredVersion("1.0")]
public IDataManager DataManager { get; private set; } = null!;
[PluginService]
[RequiredVersion("1.0")]
public IPluginLog PluginLog { get; private set; } = null!;
/*[PluginService]
[RequiredVersion("1.0")]
public ICondition Condition { get; private set; } = null!;*/
[PluginService]
[RequiredVersion("1.0")]
public ITargetManager TargetManager { get; private set; } = null!;
public DalamudServices(DalamudPluginInterface pluginInterface)
{
pluginInterface.Inject(this);
}
public void AddServices(IServiceCollection services)
{
services
.AddSingleton(PluginInterface)
.AddSingleton(SigScanner)
.AddSingleton(Framework)
.AddSingleton(ObjectTable)
.AddSingleton(CommandManager)
.AddSingleton(ChatGui)
.AddSingleton(ClientState)
.AddSingleton(GameGui)
.AddSingleton(Hooker)
.AddSingleton(KeyState)
.AddSingleton(this)
.AddSingleton(PluginInterface.UiBuilder)
.AddSingleton(DataManager)
.AddSingleton(PluginLog)
//.AddSingleton(Condition)
.AddSingleton(TargetManager);
}
}

View File

@@ -0,0 +1,77 @@
using CustomizePlus.UI.Windows;
using Dalamud.Plugin;
using OtterGui.Log;
using System;
using System.Linq;
using System.Timers;
namespace CustomizePlus.Core.Services;
/// <summary>
/// Detects is Fantasia+ is installed and shows a message if it is. The check is performed every 15 seconds.
/// </summary>
public class FantasiaPlusDetectService : IDisposable
{
private readonly DalamudPluginInterface _pluginInterface;
private readonly PopupSystem _popupSystem;
private readonly Logger _logger;
private Timer? _checkTimer = null;
/// <summary>
/// Note: if this is set to true then this is locked until the plugin or game is restarted
/// </summary>
public bool IsFantasiaPlusInstalled { get; private set; }
public FantasiaPlusDetectService(DalamudPluginInterface pluginInterface, PopupSystem popupSystem, Logger logger)
{
_pluginInterface = pluginInterface;
_popupSystem = popupSystem;
_logger = logger;
_popupSystem.RegisterPopup("fantasia_detected_warn", "Customize+ detected that you have Fantasia+ installed.\nPlease delete or turn it off and restart your game to use Customize+.");
if (CheckFantasiaPlusPresence())
{
_popupSystem.ShowPopup("fantasia_detected_warn");
_logger.Error("Fantasia+ detected during startup, plugin will be locked");
}
else
{
_checkTimer = new Timer(15 * 1000);
_checkTimer.Elapsed += CheckTimerOnElapsed;
_checkTimer.Start();
}
}
/// <summary>
/// Returns true if Fantasia+ is installed and loaded
/// </summary>
/// <returns></returns>
private bool CheckFantasiaPlusPresence()
{
if (IsFantasiaPlusInstalled)
return true;
IsFantasiaPlusInstalled = _pluginInterface.InstalledPlugins.Any(pluginInfo => pluginInfo is { InternalName: "FantasiaPlus", IsLoaded: true });
return IsFantasiaPlusInstalled;
}
private void CheckTimerOnElapsed(object? sender, ElapsedEventArgs e)
{
if (CheckFantasiaPlusPresence())
{
_popupSystem.ShowPopup("fantasia_detected_warn");
_checkTimer!.Stop();
_checkTimer?.Dispose();
_logger.Error("Fantasia+ detected by timer, plugin will be locked");
}
}
public void Dispose()
{
if (_checkTimer != null)
_checkTimer.Dispose();
}
}

View File

@@ -0,0 +1,57 @@
using Dalamud.Plugin;
using System;
using System.Collections.Generic;
using System.IO;
using CustomizePlus.Profiles.Data;
namespace CustomizePlus.Core.Services;
public class FilenameService
{
public readonly string ConfigDirectory;
public readonly string ConfigFile;
public readonly string ProfileDirectory;
public readonly string ProfileFileSystem;
public readonly string TemplateDirectory;
public readonly string TemplateFileSystem;
public FilenameService(DalamudPluginInterface pi)
{
ConfigDirectory = pi.ConfigDirectory.FullName;
ConfigFile = pi.ConfigFile.FullName;
ProfileDirectory = Path.Combine(ConfigDirectory, "profiles");
ProfileFileSystem = Path.Combine(ConfigDirectory, "profile_sort_order.json");
TemplateDirectory = Path.Combine(ConfigDirectory, "templates");
TemplateFileSystem = Path.Combine(ConfigDirectory, "template_sort_order.json");
}
public IEnumerable<FileInfo> Templates()
{
if (!Directory.Exists(TemplateDirectory))
yield break;
foreach (var file in Directory.EnumerateFiles(TemplateDirectory, "*.json", SearchOption.TopDirectoryOnly))
yield return new FileInfo(file);
}
public string TemplateFile(Guid id)
=> Path.Combine(TemplateDirectory, $"{id}.json");
public string TemplateFile(Templates.Data.Template template)
=> TemplateFile(template.UniqueId);
public IEnumerable<FileInfo> Profiles()
{
if (!Directory.Exists(ProfileDirectory))
yield break;
foreach (var file in Directory.EnumerateFiles(ProfileDirectory, "*.json", SearchOption.TopDirectoryOnly))
yield return new FileInfo(file);
}
public string ProfileFile(Guid id)
=> Path.Combine(ProfileDirectory, $"{id}.json");
public string ProfileFile(Profile profile)
=> ProfileFile(profile.UniqueId);
}

View File

@@ -0,0 +1,156 @@
using Dalamud.Game;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using System;
using System.Runtime.InteropServices;
using OtterGui.Log;
using CustomizePlus.Core.Data;
using CustomizePlus.Game.Services;
using CustomizePlus.Configuration.Data;
using CustomizePlus.Profiles;
using CustomizePlus.Armatures.Services;
using CustomizePlus.GameData.Data;
namespace CustomizePlus.Core.Services;
public class HookingService : IDisposable
{
private readonly PluginConfiguration _configuration;
private readonly ISigScanner _sigScanner;
private readonly IGameInteropProvider _hooker;
private readonly ProfileManager _profileManager;
private readonly ArmatureManager _armatureManager;
private readonly GameStateService _gameStateService;
private readonly FantasiaPlusDetectService _fantasiaPlusDetectService;
private readonly Logger _logger;
private Hook<RenderDelegate>? _renderManagerHook;
private Hook<GameObjectMovementDelegate>? _gameObjectMovementHook;
private delegate nint RenderDelegate(nint a1, nint a2, int a3, int a4);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
private delegate void GameObjectMovementDelegate(nint gameObject);
public HookingService(
PluginConfiguration configuration,
ISigScanner sigScanner,
IGameInteropProvider hooker,
ProfileManager profileManager,
ArmatureManager armatureManager,
GameStateService gameStateService,
FantasiaPlusDetectService fantasiaPlusDetectService,
Logger logger)
{
_configuration = configuration;
_sigScanner = sigScanner;
_hooker = hooker;
_profileManager = profileManager;
_armatureManager = armatureManager;
_gameStateService = gameStateService;
_fantasiaPlusDetectService = fantasiaPlusDetectService;
_logger = logger;
ReloadHooks();
}
public void ReloadHooks()
{
try
{
if (_configuration.PluginEnabled)
{
if (_renderManagerHook == null)
{
var renderAddress = _sigScanner.ScanText(Constants.RenderHookAddress);
_renderManagerHook = _hooker.HookFromAddress<RenderDelegate>(renderAddress, OnRender);
_logger.Debug("Render hook established");
}
if (_gameObjectMovementHook == null)
{
var movementAddress = _sigScanner.ScanText(Constants.MovementHookAddress);
_gameObjectMovementHook = _hooker.HookFromAddress<GameObjectMovementDelegate>(movementAddress, OnGameObjectMove);
_logger.Debug("Movement hook established");
}
_logger.Debug("Hooking render & movement functions");
_renderManagerHook.Enable();
_gameObjectMovementHook.Enable();
_logger.Debug("Hooking render manager");
_renderManagerHook.Enable();
//Send current player's profile update message to IPC
//IPCManager.OnProfileUpdate(null);
}
else
{
_logger.Debug("Unhooking...");
_renderManagerHook?.Disable();
_gameObjectMovementHook?.Disable();
_renderManagerHook?.Disable();
}
}
catch (Exception e)
{
_logger.Error($"Failed to hook Render::Manager::Render {e}");
throw;
}
}
private nint OnRender(nint a1, nint a2, int a3, int a4)
{
if (_renderManagerHook == null)
{
throw new Exception();
}
if (_fantasiaPlusDetectService.IsFantasiaPlusInstalled)
{
_logger.Error($"Fantasia+ detected, disabling all hooks");
_renderManagerHook?.Disable();
_gameObjectMovementHook.Disable();
return _renderManagerHook.Original(a1, a2, a3, a4);
}
try
{
_armatureManager.OnRender();
_profileManager.OnRender();
}
catch (Exception e)
{
_logger.Error($"Error in Customize+ render hook {e}");
_renderManagerHook?.Disable();
}
return _renderManagerHook.Original(a1, a2, a3, a4);
}
private unsafe void OnGameObjectMove(nint gameObjectPtr)
{
// Call the original function.
_gameObjectMovementHook.Original(gameObjectPtr);
// If GPose and a 3rd-party posing service are active simultneously, abort
if (_gameStateService.GameInPosingMode())
{
return;
}
var actor = (Actor)gameObjectPtr;
if (actor.Valid)
_armatureManager.OnGameObjectMove((Actor)gameObjectPtr);
}
public void Dispose()
{
_gameObjectMovementHook?.Disable();
_gameObjectMovementHook?.Dispose();
_renderManagerHook?.Disable();
_renderManagerHook?.Dispose();
}
}

View File

@@ -0,0 +1,17 @@
using OtterGui.Classes;
using OtterGui.Log;
namespace CustomizePlus.Core.Services;
/// <summary>
/// Any file type that we want to save via SaveService.
/// </summary>
public interface ISavable : ISavable<FilenameService>
{ }
public sealed class SaveService : SaveServiceBase<FilenameService>
{
public SaveService(Logger logger, FrameworkManager framework, FilenameService fileNames)
: base(logger, framework, fileNames)
{ }
}