diff --git a/MareSynchronos/Configuration.cs b/MareSynchronos/Configuration.cs index 2e00b03..7cd8790 100644 --- a/MareSynchronos/Configuration.cs +++ b/MareSynchronos/Configuration.cs @@ -9,7 +9,9 @@ namespace MareSynchronos { public int Version { get; set; } = 0; - public string PenumbraFolder { get; set; } = string.Empty; + public string CacheFolder { get; set; } = string.Empty; + public string ClientSecret { get; internal set; } = string.Empty; + public string ApiUri { get; internal set; } = string.Empty; // the below exist just to make saving less cumbersome diff --git a/MareSynchronos/Factories/FileReplacementFactory.cs b/MareSynchronos/Factories/FileReplacementFactory.cs index 7f56dab..d880898 100644 --- a/MareSynchronos/Factories/FileReplacementFactory.cs +++ b/MareSynchronos/Factories/FileReplacementFactory.cs @@ -31,7 +31,7 @@ namespace MareSynchronos.Factories { // try to resolve path with --filename instead? string[] tempGamePath = gamePath.Split('/'); - tempGamePath[tempGamePath.Length - 1] = "--" + tempGamePath[tempGamePath.Length - 1]; + tempGamePath[^1] = "--" + tempGamePath[^1]; string newTempGamePath = string.Join('/', tempGamePath); var resolvedPath = ipcManager.PenumbraResolvePath(newTempGamePath, playerName)!; if (resolvedPath != newTempGamePath) diff --git a/MareSynchronos/Hooks/DrawHooks.cs b/MareSynchronos/Hooks/DrawHooks.cs index fe9be07..00a7672 100644 --- a/MareSynchronos/Hooks/DrawHooks.cs +++ b/MareSynchronos/Hooks/DrawHooks.cs @@ -144,7 +144,7 @@ namespace MareSynchronos.Hooks return cache; } - public List PrintRequestedResources() + public void PrintRequestedResources() { var cache = BuildCharacterCache(); @@ -158,8 +158,6 @@ namespace MareSynchronos.Hooks { PluginLog.Verbose(resource.Value.ToString()); } - - return cache.FileReplacements; } public void StartHooks() diff --git a/MareSynchronos/Managers/CharacterManager.cs b/MareSynchronos/Managers/CharacterManager.cs new file mode 100644 index 0000000..cbb05a9 --- /dev/null +++ b/MareSynchronos/Managers/CharacterManager.cs @@ -0,0 +1,143 @@ +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Logging; +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using MareSynchronos.Hooks; +using MareSynchronos.Models; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace MareSynchronos.Managers +{ + public class CharacterManager : IDisposable + { + private readonly DrawHooks drawHooks; + private readonly ClientState clientState; + private readonly Framework framework; + private readonly ApiController apiController; + private readonly ObjectTable objectTable; + private readonly IpcManager ipcManager; + private Task? drawHookTask = null; + + public CharacterManager(DrawHooks drawhooks, ClientState clientState, Framework framework, ApiController apiController, ObjectTable objectTable, IpcManager ipcManager) + { + this.drawHooks = drawhooks; + this.clientState = clientState; + this.framework = framework; + this.apiController = apiController; + this.objectTable = objectTable; + this.ipcManager = ipcManager; + drawHooks.StartHooks(); + clientState.TerritoryChanged += ClientState_TerritoryChanged; + framework.Update += Framework_Update; + drawhooks.PlayerLoadEvent += Drawhooks_PlayerLoadEvent; + } + + Dictionary localPlayers = new(); + private DateTime lastCheck = DateTime.Now; + + private unsafe void Framework_Update(Framework framework) + { + try + { + if (clientState.LocalPlayer == null) return; + + if (DateTime.Now < lastCheck.AddSeconds(5)) return; + + List localPlayersList = new(); + List newPlayers = new(); + foreach (var obj in objectTable) + { + if (obj.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player) continue; + string playerName = obj.Name.ToString(); + if (playerName == clientState.LocalPlayer.Name.ToString()) continue; + var hashedName = Crypto.GetHash(playerName); + } + + foreach (var item in localPlayers.ToList()) + { + if (!localPlayersList.Contains(item.Key)) + { + localPlayers.Remove(item.Key); + } + } + + if (newPlayers.Any()) + PluginLog.Debug("New players: " + string.Join(",", newPlayers.Select(p => p + ":" + localPlayers[p]))); + lastCheck = DateTime.Now; + } + catch (Exception ex) + { + PluginLog.Error(ex, "error"); + } + } + + private void ClientState_TerritoryChanged(object? sender, ushort e) + { + localPlayers.Clear(); + } + + public void Dispose() + { + framework.Update -= Framework_Update; + drawHooks.PlayerLoadEvent -= Drawhooks_PlayerLoadEvent; + clientState.TerritoryChanged -= ClientState_TerritoryChanged; + drawHooks?.Dispose(); + } + + private unsafe void Drawhooks_PlayerLoadEvent(object? sender, EventArgs e) + { + if (sender == null) return; + if (drawHookTask != null && !drawHookTask.IsCompleted) return; + + var obj = (GameObject*)(IntPtr)sender; + drawHookTask = Task.Run(() => + { + PluginLog.Debug("Waiting for charater to be drawn"); + while ((obj->RenderFlags & 0b100000000000) == 0b100000000000) // 0b100000000000 is "still rendering" or something + { + Thread.Sleep(10); + } + PluginLog.Debug("Character finished drawing"); + + // wait one more second just in case + Thread.Sleep(1000); + + apiController.SendCharacterData(drawHooks.BuildCharacterCache()).RunSynchronously(); + }); + } + + public CharacterCache GetCharacterCache() => drawHooks.BuildCharacterCache(); + + public void PrintRequestedResources() => drawHooks.PrintRequestedResources(); + + public void DebugJson() + { + var cache = drawHooks.BuildCharacterCache(); + cache.SetGlamourerData(ipcManager.GlamourerGetCharacterCustomization()!); + cache.JobId = clientState.LocalPlayer!.ClassJob.Id; + Task.Run(async () => + { + while (!cache.IsReady) + { + await Task.Delay(50); + } + var json = JsonConvert.SerializeObject(cache, Formatting.Indented); + + cache.CacheHash = Crypto.GetHash(json); + + json = JsonConvert.SerializeObject(cache, Formatting.Indented); + PluginLog.Debug(json); + }); + } + } +} diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index 85613cb..50ac2de 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -26,6 +26,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -45,9 +46,6 @@ $(DalamudLibPath)FFXIVClientStructs.dll false - - ..\..\..\..\..\AppData\Roaming\XIVLauncher\installedPlugins\Glamourer\0.0.9.0\Glamourer.GameData.dll - $(DalamudLibPath)Newtonsoft.Json.dll false diff --git a/MareSynchronos/Models/CharacterCache.cs b/MareSynchronos/Models/CharacterCache.cs index b67f796..45331d4 100644 --- a/MareSynchronos/Models/CharacterCache.cs +++ b/MareSynchronos/Models/CharacterCache.cs @@ -88,7 +88,7 @@ namespace MareSynchronos.Models } public override string ToString() { - StringBuilder stringBuilder = new StringBuilder(); + StringBuilder stringBuilder = new(); foreach (var fileReplacement in FileReplacements.OrderBy(a => a.GamePath)) { stringBuilder.AppendLine(fileReplacement.ToString()); diff --git a/MareSynchronos/Models/FileReplacement.cs b/MareSynchronos/Models/FileReplacement.cs index dfc31f2..36686bc 100644 --- a/MareSynchronos/Models/FileReplacement.cs +++ b/MareSynchronos/Models/FileReplacement.cs @@ -119,7 +119,7 @@ namespace MareSynchronos.Models public override string ToString() { - StringBuilder builder = new StringBuilder(); + StringBuilder builder = new(); builder.AppendLine($"Modded: {HasFileReplacement} - {GamePath} => {ResolvedPath}"); foreach (var l1 in Associated) { diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 12a4f9c..56fe3ca 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -1,80 +1,56 @@ 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 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; using MareSynchronos.Managers; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using MareSynchronos.Utils; +using LZ4; +using MareSynchronos.WebAPI; namespace MareSynchronos { public sealed class Plugin : IDalamudPlugin { - public string Name => "Mare Synchronos"; - private const string commandName = "/mare"; - private readonly Framework framework; - private readonly ObjectTable objectTable; private readonly ClientState clientState; + private readonly Framework framework; private readonly GameGui gameGui; - - private DalamudPluginInterface pluginInterface { get; init; } - private CommandManager commandManager { get; init; } - private Configuration configuration { get; init; } - private PluginUI PluginUi { get; init; } - private DrawHooks drawHooks; - private IpcManager ipcManager; - + private readonly ObjectTable objectTable; + private readonly ApiController apiController; + private CharacterManager? characterManager; + private IpcManager? ipcManager; public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager, Framework framework, ObjectTable objectTable, ClientState clientState, GameGui gameGui) { - this.pluginInterface = pluginInterface; - this.commandManager = commandManager; + this.PluginInterface = pluginInterface; + this.CommandManager = commandManager; this.framework = framework; this.objectTable = objectTable; this.clientState = clientState; this.gameGui = gameGui; - configuration = this.pluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); - configuration.Initialize(this.pluginInterface); + Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); + Configuration.Initialize(this.PluginInterface); + + apiController = new ApiController(Configuration); // you might normally want to embed resources and load them from the manifest stream - this.PluginUi = new PluginUI(this.configuration); + this.PluginUi = new PluginUI(this.Configuration); - this.commandManager.AddHandler(commandName, new CommandInfo(OnCommand) - { - HelpMessage = "pass 'scan' to initialize or rescan files into the database" - }); - - FileCacheContext db = new FileCacheContext(); - db.Dispose(); + new FileCacheContext().Dispose(); // make sure db is initialized I guess clientState.Login += ClientState_Login; clientState.Logout += ClientState_Logout; @@ -84,105 +60,123 @@ namespace MareSynchronos ClientState_Login(null, null!); } - this.pluginInterface.UiBuilder.Draw += DrawUI; - this.pluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI; + this.PluginInterface.UiBuilder.Draw += DrawUI; + this.PluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI; } - private void IpcManager_IpcManagerInitialized(object? sender, EventArgs e) + public string Name => "Mare Synchronos"; + private CommandManager CommandManager { get; init; } + private Configuration Configuration { get; init; } + private DalamudPluginInterface PluginInterface { get; init; } + private PluginUI PluginUi { get; init; } + public void Dispose() { - PluginLog.Debug("IPC Manager initialized event"); - ipcManager.IpcManagerInitialized -= IpcManager_IpcManagerInitialized; - Task.Run(async () => + this.PluginUi?.Dispose(); + this.CommandManager.RemoveHandler(commandName); + clientState.Login -= ClientState_Login; + clientState.Logout -= ClientState_Logout; + ipcManager?.Dispose(); + characterManager?.Dispose(); + } + + private void ClientState_Login(object? sender, EventArgs e) + { + PluginLog.Debug("Client login"); + ipcManager = new IpcManager(PluginInterface); + ipcManager.IpcManagerInitialized += IpcManager_IpcManagerInitialized; + ipcManager.Initialize(); + + CommandManager.AddHandler(commandName, new CommandInfo(OnCommand) { - while (clientState.LocalPlayer == null) - { - await Task.Delay(500); - } - drawHooks.StartHooks(); - ipcManager.PenumbraRedraw(clientState.LocalPlayer!.Name.ToString()); + HelpMessage = "pass 'scan' to initialize or rescan files into the database" }); } private void ClientState_Logout(object? sender, EventArgs e) { PluginLog.Debug("Client logout"); - drawHooks.PlayerLoadEvent -= DrawHooks_PlayerLoadEvent; - ipcManager.Dispose(); - drawHooks.Dispose(); - ipcManager = null!; - drawHooks = null!; - } - - private void ClientState_Login(object? sender, EventArgs e) - { - PluginLog.Debug("Client login"); - ipcManager = new IpcManager(pluginInterface); - drawHooks = new DrawHooks(pluginInterface, clientState, objectTable, new FileReplacementFactory(ipcManager, clientState), gameGui); - ipcManager.IpcManagerInitialized += IpcManager_IpcManagerInitialized; - ipcManager.Initialize(); - drawHooks.PlayerLoadEvent += DrawHooks_PlayerLoadEvent; - } - - private Task drawHookTask; - - private unsafe void DrawHooks_PlayerLoadEvent(object? sender, EventArgs e) - { - if (sender == null) return; - if (drawHookTask != null && !drawHookTask.IsCompleted) return; - - var obj = (GameObject*)(IntPtr)sender; - drawHookTask = Task.Run(() => - { - PluginLog.Debug("Waiting for charater to be drawn"); - while ((obj->RenderFlags & 0b100000000000) == 0b100000000000) // 0b100000000000 is "still rendering" or something - { - Thread.Sleep(10); - } - PluginLog.Debug("Character finished drawing"); - - // we should recalculate cache here - // probably needs a different method - // at that point we will also have to send data to the api - _ = drawHooks.BuildCharacterCache(); - }); - } - - public void Dispose() - { - this.PluginUi?.Dispose(); - this.commandManager.RemoveHandler(commandName); - drawHooks.PlayerLoadEvent -= DrawHooks_PlayerLoadEvent; - clientState.Login -= ClientState_Login; - clientState.Logout -= ClientState_Logout; + characterManager?.Dispose(); ipcManager?.Dispose(); - drawHooks?.Dispose(); + ipcManager = null!; + CommandManager.RemoveHandler(commandName); + } + + private void CopyFile(FileReplacement replacement, string targetDirectory, Dictionary? resourceDict = null) + { + if (replacement.HasFileReplacement) + { + PluginLog.Debug("Copying file \"" + replacement.ResolvedPath + "\""); + var db1 = new FileCacheContext(); + var fileCache = db1.FileCaches.Single(f => f.Filepath.Contains(replacement.ResolvedPath.Replace('/', '\\'))); + db1.Dispose(); + try + { + var ext = new FileInfo(fileCache.Filepath).Extension; + var newFilePath = Path.Combine(targetDirectory, "files", fileCache.Hash.ToLower() + ext); + string lc4hcPath = Path.Combine(targetDirectory, "files", "lz4hc." + fileCache.Hash.ToLower() + ext); + if (!File.Exists(lc4hcPath)) + { + + Stopwatch st = Stopwatch.StartNew(); + File.WriteAllBytes(lc4hcPath, LZ4Codec.Encode(File.ReadAllBytes(fileCache.Filepath), 0, (int)new FileInfo(fileCache.Filepath).Length)); + st.Stop(); + PluginLog.Debug("Compressed " + new FileInfo(fileCache.Filepath).Length + " bytes to " + new FileInfo(lc4hcPath).Length + " bytes in " + st.Elapsed); + File.Copy(fileCache.Filepath, newFilePath); + 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 (Exception ex) + { + PluginLog.Error(ex, "error during copy"); + } + } + } + + private void DrawConfigUI() + { + this.PluginUi.SettingsVisible = true; + } + + private void DrawUI() + { + this.PluginUi.Draw(); + } + + private void IpcManager_IpcManagerInitialized(object? sender, EventArgs e) + { + PluginLog.Debug("IPC Manager initialized event"); + ipcManager!.IpcManagerInitialized -= IpcManager_IpcManagerInitialized; + Task.Run(async () => + { + while (clientState.LocalPlayer == null) + { + await Task.Delay(500); + } + + characterManager = new CharacterManager( + new DrawHooks(PluginInterface, clientState, objectTable, new FileReplacementFactory(ipcManager, clientState), gameGui), + clientState, framework, apiController, objectTable, ipcManager); + ipcManager.PenumbraRedraw(clientState.LocalPlayer!.Name.ToString()); + }); } private void OnCommand(string command, string args) { if (args == "print") { - var resources = drawHooks.PrintRequestedResources(); + characterManager?.PrintRequestedResources(); } if (args == "printjson") { - var cache = drawHooks.BuildCharacterCache(); - cache.SetGlamourerData(ipcManager.GlamourerGetCharacterCustomization()!); - cache.JobId = clientState.LocalPlayer!.ClassJob.Id; - Task.Run(async () => - { - while (!cache.IsReady) - { - await Task.Delay(50); - } - var json = JsonConvert.SerializeObject(cache, Formatting.Indented); - - cache.CacheHash = Crypto.GetHash(json); - - json = JsonConvert.SerializeObject(cache, Formatting.Indented); - PluginLog.Debug(json); - }); + characterManager?.DebugJson(); } if (args == "createtestmod") @@ -191,7 +185,7 @@ namespace MareSynchronos { var playerName = clientState.LocalPlayer!.Name.ToString(); var modName = $"Mare Synchronos Test Mod {playerName}"; - var modDirectory = ipcManager.PenumbraModDirectory()!; + var modDirectory = ipcManager!.PenumbraModDirectory()!; string modDirectoryPath = Path.Combine(modDirectory, modName); if (Directory.Exists(modDirectoryPath)) { @@ -200,24 +194,26 @@ namespace MareSynchronos Directory.CreateDirectory(modDirectoryPath); Directory.CreateDirectory(Path.Combine(modDirectoryPath, "files")); - Meta meta = new Meta() + Meta meta = new() { Name = modName, Author = playerName, Description = "Mare Synchronous Test Mod Export", }; - var resources = drawHooks.PrintRequestedResources(); + var resources = characterManager!.GetCharacterCache(); var metaJson = JsonConvert.SerializeObject(meta); File.WriteAllText(Path.Combine(modDirectoryPath, "meta.json"), metaJson); - DefaultMod defaultMod = new DefaultMod(); + DefaultMod defaultMod = new(); - using var db = new FileCacheContext(); - foreach (var resource in resources) + //using var db = new FileCacheContext(); + Stopwatch st = Stopwatch.StartNew(); + Parallel.ForEach(resources.AllReplacements, resource => { - CopyRecursive(resource, modDirectoryPath, db, defaultMod.Files); - } + CopyFile(resource, modDirectoryPath, defaultMod.Files); + }); + PluginLog.Debug("Compression took " + st.Elapsed); var defaultModJson = JsonConvert.SerializeObject(defaultMod); File.WriteAllText(Path.Combine(modDirectoryPath, "default_mod.json"), defaultModJson); @@ -226,56 +222,5 @@ namespace MareSynchronos }); } } - - 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 DrawUI() - { - this.PluginUi.Draw(); - } - - private void DrawConfigUI() - { - this.PluginUi.SettingsVisible = true; - } } } diff --git a/MareSynchronos/PluginUI.cs b/MareSynchronos/PluginUI.cs index ef05770..9655cdb 100644 --- a/MareSynchronos/PluginUI.cs +++ b/MareSynchronos/PluginUI.cs @@ -81,9 +81,9 @@ namespace MareSynchronos ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) { // can't ref a property, so use a local copy - string penumbraFolder = configuration.PenumbraFolder; + string penumbraFolder = configuration.CacheFolder; if(ImGui.InputText("Penumbra mod folder", ref penumbraFolder, 255)) { - this.configuration.PenumbraFolder = penumbraFolder; + this.configuration.CacheFolder = penumbraFolder; this.configuration.Save(); } } diff --git a/MareSynchronos/WebAPI/ApiController.cs b/MareSynchronos/WebAPI/ApiController.cs new file mode 100644 index 0000000..aae072d --- /dev/null +++ b/MareSynchronos/WebAPI/ApiController.cs @@ -0,0 +1,77 @@ +using Dalamud.Configuration; +using Dalamud.Logging; +using MareSynchronos.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace MareSynchronos.WebAPI +{ + public class ApiController + { + private readonly Configuration pluginConfiguration; + + private string SecretKey => pluginConfiguration.ClientSecret; + private string CacheFolder => pluginConfiguration.CacheFolder; + private string ApiUri => pluginConfiguration.ApiUri; + + public ApiController(Configuration pluginConfiguration) + { + this.pluginConfiguration = pluginConfiguration; + } + + public async Task Heartbeat() + { + PluginLog.Debug("Sending heartbeat to " + ApiUri); + } + + public async Task<(string, string)> Register() + { + PluginLog.Debug("Registering at service " + ApiUri); + return (string.Empty, string.Empty); + } + + public async Task UploadFile(string filePath) + { + PluginLog.Debug("Uploading file " + filePath + " to " + ApiUri); + } + + public async Task DownloadFile(string hash) + { + PluginLog.Debug("Downloading file from service " + ApiUri); + + return Array.Empty(); + } + + public async Task> SendCharacterData(CharacterCache character) + { + PluginLog.Debug("Sending Character data to service " + ApiUri); + + List list = new(); + return list; + } + + public async Task GetCharacterData(string uid) + { + PluginLog.Debug("Getting character data for " + uid + " from service " + ApiUri); + + CharacterCache characterCache = new(); + return characterCache; + } + + public async Task SendWhitelist() + { + PluginLog.Debug("Sending whitelist to service " + ApiUri); + } + + public async Task> GetWhitelist() + { + PluginLog.Debug("Getting whitelist from service " + ApiUri); + + List whitelist = new(); + return whitelist; + } + } +}