using Dalamud.Game.Command; using Dalamud.IoC; using Dalamud.Logging; using Dalamud.Plugin; using MareSynchronos.FileCacheDB; using MareSynchronos.Factories; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using MareSynchronos.Hooks; using Penumbra.PlayerWatch; using Dalamud.Game; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState; using Dalamud.Data; using Lumina.Excel.GeneratedSheets; using Glamourer.Customization; using System.Text; using Penumbra.GameData.Enums; using System; using MareSynchronos.Models; using Dalamud.Game.Gui; using MareSynchronos.PenumbraMod; using System.Text.Json; using System.Text.Encodings.Web; using System.Text.Unicode; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System.Reflection; namespace SamplePlugin { public sealed class Plugin : IDalamudPlugin { public string Name => "Mare Synchronos"; private const string commandName = "/mare"; private readonly ClientState clientState; private DalamudPluginInterface PluginInterface { get; init; } private CommandManager CommandManager { get; init; } private Configuration Configuration { get; init; } private PluginUI PluginUi { get; init; } private FileCacheFactory FileCacheFactory { get; init; } private DrawHooks drawHooks; private CancellationTokenSource cts; private IPlayerWatcher playerWatch; public Plugin( [RequiredVersion("1.0")] DalamudPluginInterface pluginInterface, [RequiredVersion("1.0")] CommandManager commandManager, Framework framework, ObjectTable objectTable, ClientState clientState, DataManager dataManager, GameGui gameGui) { this.PluginInterface = pluginInterface; this.CommandManager = commandManager; this.clientState = clientState; this.Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); this.Configuration.Initialize(this.PluginInterface); // you might normally want to embed resources and load them from the manifest stream this.PluginUi = new PluginUI(this.Configuration); this.CommandManager.AddHandler(commandName, new CommandInfo(OnCommand) { HelpMessage = "pass 'scan' to initialize or rescan files into the database" }); FileCacheFactory = new FileCacheFactory(); this.PluginInterface.UiBuilder.Draw += DrawUI; this.PluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI; playerWatch = PlayerWatchFactory.Create(framework, clientState, objectTable); drawHooks = new DrawHooks(pluginInterface, clientState, objectTable, new FileReplacementFactory(pluginInterface, clientState), gameGui); } public void Dispose() { this.PluginUi.Dispose(); this.CommandManager.RemoveHandler(commandName); playerWatch.PlayerChanged -= PlayerWatch_PlayerChanged; playerWatch.RemovePlayerFromWatch("Ilya Zhelmo"); drawHooks.Dispose(); } private void OnCommand(string command, string args) { if (args == "stop") { cts?.Cancel(); return; } if(args == "playerdata") { PluginLog.Debug(PluginInterface.GetIpcSubscriber("Glamourer.GetCharacterCustomization").InvokeFunc()); } if(args == "applyglam") { PluginInterface.GetIpcSubscriber("Glamourer.ApplyCharacterCustomization") .InvokeAction("Ah3/DwQBAR4IBHOABIOceTIIApkDAgADQmQBZJqepQZlAAEAAAAAAAAAAACcEwEAyxcBbrAXAUnKFwJIuBcBBkYAAQBIAAEANQABADUAAQACAAQAAQAAAIA/Eg==", "Ilya Zhelmo"); } if (args == "scan") { cts = new CancellationTokenSource(); Task.Run(() => StartScan(), cts.Token); } if (args == "watch") { playerWatch.AddPlayerToWatch("Ilya Zhelmo"); playerWatch.PlayerChanged += PlayerWatch_PlayerChanged; } if (args == "stopwatch") { playerWatch.PlayerChanged -= PlayerWatch_PlayerChanged; playerWatch.RemovePlayerFromWatch("Ilya Zhelmo"); } if (args == "hook") { drawHooks.StartHooks(); } if (args == "print") { var resources = drawHooks.PrintRequestedResources(); } if (args == "printjson") { var cache = drawHooks.BuildCharacterCache(); var json = JsonConvert.SerializeObject(cache, Formatting.Indented); PluginLog.Debug(json); } if (args == "createtestmod") { Task.Run(() => { var playerName = clientState.LocalPlayer!.Name.ToString(); var modName = $"Mare Synchronos Test Mod {playerName}"; var modDirectory = PluginInterface.GetIpcSubscriber("Penumbra.GetModDirectory").InvokeFunc(); string modDirectoryPath = Path.Combine(modDirectory, modName); if (Directory.Exists(modDirectoryPath)) { Directory.Delete(modDirectoryPath, true); } Directory.CreateDirectory(modDirectoryPath); Directory.CreateDirectory(Path.Combine(modDirectoryPath, "files")); Meta meta = new Meta() { Name = modName, Author = playerName, Description = "Mare Synchronous Test Mod Export", }; var resources = drawHooks.PrintRequestedResources(); var metaJson = JsonConvert.SerializeObject(meta); File.WriteAllText(Path.Combine(modDirectoryPath, "meta.json"), metaJson); DefaultMod defaultMod = new DefaultMod(); using var db = new FileCacheContext(); foreach (var resource in resources) { CopyRecursive(resource, modDirectoryPath, db, defaultMod.Files); } var defaultModJson = JsonConvert.SerializeObject(defaultMod); File.WriteAllText(Path.Combine(modDirectoryPath, "default_mod.json"), defaultModJson); PluginLog.Debug("Mod created to " + modDirectoryPath); }); } } private void CopyRecursive(FileReplacement replacement, string targetDirectory, FileCacheContext db, Dictionary? resourceDict = null) { if (replacement.HasFileReplacement) { PluginLog.Debug("Copying file \"" + replacement.ResolvedPath + "\""); var fileCache = db.FileCaches.Single(f => f.Filepath.Contains(replacement.ResolvedPath.Replace('/', '\\'))); try { var ext = new FileInfo(fileCache.Filepath).Extension; File.Copy(fileCache.Filepath, Path.Combine(targetDirectory, "files", fileCache.Hash.ToLower() + ext)); if (resourceDict != null) { resourceDict[replacement.GamePath] = $"files\\{fileCache.Hash.ToLower() + ext}"; } else { File.AppendAllLines(Path.Combine(targetDirectory, "filelist.txt"), new[] { $"\"{replacement.GamePath}\": \"files\\\\{fileCache.Hash.ToLower() + ext}\"," }); } } catch { } } foreach (var associated in replacement.Associated) { CopyRecursive(associated, targetDirectory, db, resourceDict); } } private void PlayerWatch_PlayerChanged(Dalamud.Game.ClientState.Objects.Types.Character actor) { var equipment = playerWatch.UpdatePlayerWithoutEvent(actor); var customization = new CharacterCustomization(actor); //DebugCustomization(customization); //PluginLog.Debug(customization.Gender.ToString()); if (equipment != null) { PluginLog.Debug(equipment.ToString()); } } private void StartScan() { Stopwatch st = Stopwatch.StartNew(); string penumbraDir = PluginInterface.GetIpcSubscriber("Penumbra.GetModDirectory").InvokeFunc(); PluginLog.Debug("Getting files from " + penumbraDir); ConcurrentDictionary charaFiles = new ConcurrentDictionary( Directory.GetFiles(penumbraDir, "*.*", SearchOption.AllDirectories) .Select(s => s.ToLowerInvariant()) .Where(f => !f.EndsWith(".json")) .Where(f => f.Contains(@"\chara\")) .Select(p => new KeyValuePair(p, false))); int count = 0; using FileCacheContext db = new(); var fileCaches = db.FileCaches.ToList(); var fileCachesToUpdate = new ConcurrentBag(); var fileCachesToDelete = new ConcurrentBag(); var fileCachesToAdd = new ConcurrentBag(); // scan files from database Parallel.ForEach(fileCaches, new ParallelOptions() { CancellationToken = cts.Token, MaxDegreeOfParallelism = 10 }, cache => { count = Interlocked.Increment(ref count); PluginLog.Debug($"[{count}/{fileCaches.Count}] Checking: {cache.Filepath}"); if (!File.Exists(cache.Filepath)) { PluginLog.Debug("File was not found anymore: " + cache.Filepath); fileCachesToDelete.Add(cache); } else { charaFiles[cache.Filepath] = true; FileInfo fileInfo = new(cache.Filepath); if (fileInfo.LastWriteTimeUtc.Ticks != long.Parse(cache.LastModifiedDate)) { PluginLog.Debug("File was modified since last time: " + cache.Filepath + "; " + cache.LastModifiedDate + " / " + fileInfo.LastWriteTimeUtc.Ticks); FileCacheFactory.UpdateFileCache(cache); fileCachesToUpdate.Add(cache); } } }); // scan new files count = 0; Parallel.ForEach(charaFiles.Where(c => c.Value == false), new ParallelOptions() { CancellationToken = cts.Token, MaxDegreeOfParallelism = 10 }, file => { count = Interlocked.Increment(ref count); PluginLog.Debug($"[{count}/{charaFiles.Count()}] Hashing: {file.Key}"); fileCachesToAdd.Add(FileCacheFactory.Create(file.Key)); }); st.Stop(); if (cts.Token.IsCancellationRequested) return; PluginLog.Debug("Scanning complete, total elapsed time: " + st.Elapsed.ToString()); if (fileCachesToAdd.Any() || fileCachesToUpdate.Any() || fileCachesToDelete.Any()) { PluginLog.Debug("Writing files to database…"); db.FileCaches.AddRange(fileCachesToAdd); db.FileCaches.UpdateRange(fileCachesToUpdate); db.FileCaches.RemoveRange(fileCachesToDelete); db.SaveChanges(); PluginLog.Debug("Database has been written."); } cts = new CancellationTokenSource(); } private void DrawUI() { this.PluginUi.Draw(); } private void DrawConfigUI() { this.PluginUi.SettingsVisible = true; } } }