add some UI stuff

This commit is contained in:
Stanley Dimant
2022-06-16 18:55:19 +02:00
parent 4f72daa0eb
commit f643b413f2
8 changed files with 311 additions and 150 deletions

View File

@@ -1,6 +1,7 @@
using Dalamud.Configuration;
using Dalamud.Plugin;
using System;
using System.Collections.Generic;
namespace MareSynchronos
{
@@ -10,8 +11,9 @@ namespace MareSynchronos
public int Version { get; set; } = 0;
public string CacheFolder { get; set; } = string.Empty;
public string ClientSecret { get; internal set; } = string.Empty;
public Dictionary<string, string> ClientSecret { get; internal set; } = new();
public string ApiUri { get; internal set; } = string.Empty;
public bool UseCustomService { get; internal set; }
// the below exist just to make saving less cumbersome

View File

@@ -19,10 +19,15 @@ namespace MareSynchronos.Factories
public FileReplacement Create(string gamePath, bool resolve = true)
{
if (!ipcManager.CheckPenumbraAPI())
{
throw new System.Exception();
}
var fileReplacement = new FileReplacement(gamePath, ipcManager.PenumbraModDirectory()!);
if (!resolve) return fileReplacement;
if(clientState.LocalPlayer != null)
if (clientState.LocalPlayer != null)
{
playerName = clientState.LocalPlayer.Name.ToString();
}

View File

@@ -12,6 +12,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using MareSynchronos.Factories;
using MareSynchronos.Managers;
using MareSynchronos.Models;
using Penumbra.GameData.ByteString;
using Penumbra.Interop.Structs;
@@ -20,6 +21,8 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace MareSynchronos.Hooks
{
@@ -136,7 +139,8 @@ namespace MareSynchronos.Hooks
}
}
}
} catch (Exception ex)
}
catch (Exception ex)
{
PluginLog.Error(ex, ex.Message);
}

View File

@@ -112,7 +112,13 @@ namespace MareSynchronos.Managers
// wait one more second just in case
Thread.Sleep(1000);
apiController.SendCharacterData(drawHooks.BuildCharacterCache()).RunSynchronously();
var cache = CreateFullCharacterCache();
while (!cache.IsCompleted)
{
Task.Delay(50);
}
_ = apiController.SendCharacterData(cache.Result);
});
}
@@ -120,12 +126,12 @@ namespace MareSynchronos.Managers
public void PrintRequestedResources() => drawHooks.PrintRequestedResources();
public void DebugJson()
private async Task<CharacterCache> CreateFullCharacterCache()
{
var cache = drawHooks.BuildCharacterCache();
cache.SetGlamourerData(ipcManager.GlamourerGetCharacterCustomization()!);
cache.JobId = clientState.LocalPlayer!.ClassJob.Id;
Task.Run(async () =>
await Task.Run(async () =>
{
while (!cache.IsReady)
{
@@ -134,10 +140,20 @@ namespace MareSynchronos.Managers
var json = JsonConvert.SerializeObject(cache, Formatting.Indented);
cache.CacheHash = Crypto.GetHash(json);
json = JsonConvert.SerializeObject(cache, Formatting.Indented);
PluginLog.Debug(json);
});
return cache;
}
public void DebugJson()
{
var cache = CreateFullCharacterCache();
while (!cache.IsCompleted)
{
Task.Delay(50);
}
PluginLog.Debug(JsonConvert.SerializeObject(cache.Result, Formatting.Indented));
}
}
}

View File

@@ -9,65 +9,55 @@ namespace MareSynchronos.Managers
{
private readonly DalamudPluginInterface pluginInterface;
private ICallGateSubscriber<object> penumbraInit;
private readonly ICallGateSubscriber<object> penumbraDispose;
private ICallGateSubscriber<string, string, string>? penumbraResolvePath;
private ICallGateSubscriber<string>? penumbraResolveModDir;
private ICallGateSubscriber<string>? glamourerGetCharacterCustomization;
private ICallGateSubscriber<string, string, object>? glamourerApplyCharacterCustomization;
private ICallGateSubscriber<int> penumbraApiVersion;
private ICallGateSubscriber<int> glamourerApiVersion;
private ICallGateSubscriber<string, int, object>? penumbraRedraw;
public bool Initialized { get; private set; } = false;
public event EventHandler? IpcManagerInitialized;
public IpcManager(DalamudPluginInterface pi)
{
pluginInterface = pi;
penumbraInit = pluginInterface.GetIpcSubscriber<object>("Penumbra.Initialized");
penumbraInit.Subscribe(Initialize);
penumbraDispose = pluginInterface.GetIpcSubscriber<object>("Penumbra.Disposed");
penumbraDispose.Subscribe(Uninitialize);
}
private bool CheckPenumbraAPI()
{
try
{
var penumbraApiVersion = pluginInterface.GetIpcSubscriber<int>("Penumbra.ApiVersion").InvokeFunc();
return penumbraApiVersion >= 4;
}
catch
{
return false;
}
}
private bool CheckGlamourerAPI()
{
try
{
var glamourerApiVersion = pluginInterface.GetIpcSubscriber<int>("Glamourer.ApiVersion").InvokeFunc();
return glamourerApiVersion >= 0;
}
catch
{
return false;
}
}
public void Initialize()
{
if (Initialized) return;
if (!CheckPenumbraAPI()) throw new Exception("Penumbra API is outdated or not available");
if (!CheckGlamourerAPI()) throw new Exception("Glamourer API is oudated or not available");
penumbraResolvePath = pluginInterface.GetIpcSubscriber<string, string, string>("Penumbra.ResolveCharacterPath");
penumbraResolveModDir = pluginInterface.GetIpcSubscriber<string>("Penumbra.GetModDirectory");
penumbraRedraw = pluginInterface.GetIpcSubscriber<string, int, object>("Penumbra.RedrawObjectByName");
glamourerGetCharacterCustomization = pluginInterface.GetIpcSubscriber<string>("Glamourer.GetCharacterCustomization");
glamourerApplyCharacterCustomization = pluginInterface.GetIpcSubscriber<string, string, object>("Glamourer.ApplyCharacterCustomization");
penumbraApiVersion = pluginInterface.GetIpcSubscriber<int>("Penumbra.ApiVersion");
glamourerApiVersion = pluginInterface.GetIpcSubscriber<int>("Glamourer.ApiVersion");
penumbraInit.Subscribe(() => penumbraRedraw!.InvokeAction("self", 0));
Initialized = true;
IpcManagerInitialized?.Invoke(this, new EventArgs());
PluginLog.Debug("[IPC Manager] initialized");
}
public bool CheckPenumbraAPI()
{
try
{
return penumbraApiVersion.InvokeFunc() >= 4;
}
catch
{
return false;
}
}
public bool CheckGlamourerAPI()
{
try
{
return glamourerApiVersion.InvokeFunc() >= 0;
}
catch
{
return false;
}
}
private void Uninitialize()
@@ -82,38 +72,37 @@ namespace MareSynchronos.Managers
public string? PenumbraResolvePath(string path, string characterName)
{
if (!Initialized) return null;
if (!CheckPenumbraAPI()) return null;
return penumbraResolvePath!.InvokeFunc(path, characterName);
}
public string? PenumbraModDirectory()
{
if (!Initialized) return null;
if (!CheckPenumbraAPI()) return null;
return penumbraResolveModDir!.InvokeFunc();
}
public string? GlamourerGetCharacterCustomization()
{
if (!Initialized) return null;
if (!CheckGlamourerAPI()) return null;
return glamourerGetCharacterCustomization!.InvokeFunc();
}
public void GlamourerApplyCharacterCustomization(string customization, string characterName)
{
if (!Initialized) return;
if (!CheckGlamourerAPI()) return;
glamourerApplyCharacterCustomization!.InvokeAction(customization, characterName);
}
public void PenumbraRedraw(string actorName)
{
if (!Initialized) return;
if (!CheckPenumbraAPI()) return;
penumbraRedraw!.InvokeAction(actorName, 0);
}
public void Dispose()
{
Uninitialize();
IpcManagerInitialized = null;
}
}
}

View File

@@ -20,6 +20,7 @@ using Newtonsoft.Json;
using MareSynchronos.Managers;
using LZ4;
using MareSynchronos.WebAPI;
using Dalamud.Interface.Windowing;
namespace MareSynchronos
{
@@ -30,9 +31,10 @@ namespace MareSynchronos
private readonly Framework framework;
private readonly GameGui gameGui;
private readonly ObjectTable objectTable;
private readonly WindowSystem windowSystem;
private readonly ApiController apiController;
private CharacterManager? characterManager;
private IpcManager? ipcManager;
private IpcManager ipcManager;
public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager,
Framework framework, ObjectTable objectTable, ClientState clientState, GameGui gameGui)
{
@@ -45,10 +47,13 @@ namespace MareSynchronos
Configuration = this.PluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
Configuration.Initialize(this.PluginInterface);
windowSystem = new WindowSystem("MareSynchronos");
apiController = new ApiController(Configuration);
ipcManager = new IpcManager(PluginInterface);
// 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, windowSystem, apiController, ipcManager);
new FileCacheContext().Dispose(); // make sure db is initialized I guess
@@ -59,9 +64,6 @@ namespace MareSynchronos
{
ClientState_Login(null, null!);
}
this.PluginInterface.UiBuilder.Draw += DrawUI;
this.PluginInterface.UiBuilder.OpenConfigUi += DrawConfigUI;
}
public string Name => "Mare Synchronos";
@@ -77,14 +79,28 @@ namespace MareSynchronos
clientState.Logout -= ClientState_Logout;
ipcManager?.Dispose();
characterManager?.Dispose();
apiController?.Dispose();
}
private void ClientState_Login(object? sender, EventArgs e)
{
PluginLog.Debug("Client login");
ipcManager = new IpcManager(PluginInterface);
ipcManager.IpcManagerInitialized += IpcManager_IpcManagerInitialized;
ipcManager.Initialize();
Task.Run(async () =>
{
while (clientState.LocalPlayer == null)
{
await Task.Delay(50);
}
characterManager = new CharacterManager(
new DrawHooks(PluginInterface, clientState, objectTable, new FileReplacementFactory(ipcManager, clientState), gameGui),
clientState, framework, apiController, objectTable, ipcManager);
ipcManager.PenumbraRedraw(clientState.LocalPlayer!.Name.ToString());
});
PluginInterface.UiBuilder.Draw += Draw;
PluginInterface.UiBuilder.OpenConfigUi += OpenConfigUI;
CommandManager.AddHandler(commandName, new CommandInfo(OnCommand)
{
@@ -96,8 +112,8 @@ namespace MareSynchronos
{
PluginLog.Debug("Client logout");
characterManager?.Dispose();
ipcManager?.Dispose();
ipcManager = null!;
PluginInterface.UiBuilder.Draw -= Draw;
PluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUI;
CommandManager.RemoveHandler(commandName);
}
@@ -139,32 +155,14 @@ namespace MareSynchronos
}
}
private void DrawConfigUI()
private void Draw()
{
this.PluginUi.SettingsVisible = true;
windowSystem.Draw();
}
private void DrawUI()
private void OpenConfigUI()
{
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());
});
this.PluginUi.Toggle();
}
private void OnCommand(string command, string args)

View File

@@ -1,4 +1,10 @@
using ImGuiNET;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Windowing;
using Dalamud.Logging;
using ImGuiNET;
using MareSynchronos.Managers;
using MareSynchronos.WebAPI;
using System;
using System.Numerics;
@@ -6,88 +12,182 @@ namespace MareSynchronos
{
// It is good to have this be disposable in general, in case you ever need it
// to do any cleanup
class PluginUI : IDisposable
class PluginUI : Window, IDisposable
{
private Configuration configuration;
// this extra bool exists for ImGui, since you can't ref a property
private bool visible = false;
public bool Visible
{
get { return this.visible; }
set { this.visible = value; }
}
private bool settingsVisible = false;
public bool SettingsVisible
{
get { return this.settingsVisible; }
set { this.settingsVisible = value; }
}
private readonly WindowSystem windowSystem;
private readonly ApiController apiController;
private readonly IpcManager ipcManager;
private string? uid;
private const string mainServer = "Lunae Crescere Incipientis (Central Server EU)";
// passing in the image here just for simplicity
public PluginUI(Configuration configuration)
public PluginUI(Configuration configuration, WindowSystem windowSystem, ApiController apiController, IpcManager ipcManager) : base("Mare Synchronos Settings", ImGuiWindowFlags.None)
{
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new(700, 400),
MaximumSize = new(700, 2000)
};
this.configuration = configuration;
this.windowSystem = windowSystem;
this.apiController = apiController;
this.ipcManager = ipcManager;
windowSystem.AddWindow(this);
}
public void Dispose()
{
windowSystem.RemoveWindow(this);
}
public void Draw()
public override void Draw()
{
// This is our only draw handler attached to UIBuilder, so it needs to be
// able to draw any windows we might have open.
// Each method checks its own visibility/state to ensure it only draws when
// it actually makes sense.
// There are other ways to do this, but it is generally best to keep the number of
// draw delegates as low as possible.
DrawMainWindow();
DrawSettingsWindow();
}
public void DrawMainWindow()
{
if (!Visible)
if (!IsOpen)
{
return;
}
ImGui.SetNextWindowSize(new Vector2(375, 330), ImGuiCond.FirstUseEver);
ImGui.SetNextWindowSizeConstraints(new Vector2(375, 330), new Vector2(float.MaxValue, float.MaxValue));
if (ImGui.Begin("My Amazing Window", ref this.visible, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
if (string.IsNullOrEmpty(apiController.SecretKey))
{
if (ImGui.Button("Show Settings"))
DrawIntroContent();
}
else
{
if (!OtherPluginStateOk()) return;
DrawSettingsContent();
}
}
private void DrawSettingsContent()
{
PrintServerState();
ImGui.Separator();
ImGui.Text("Your UID");
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.ParsedGreen, apiController.UID);
ImGui.SameLine();
if (ImGui.Button("Copy UID"))
{
ImGui.SetClipboardText(apiController.UID);
}
ImGui.Text("Share this UID to other Mare users so they can add you to their whitelist.");
}
private int serverSelectionIndex = 0;
private async void DrawIntroContent()
{
ImGui.SetWindowFontScale(1.3f);
ImGui.Text("Welcome to Mare Synchronos!");
ImGui.SetWindowFontScale(1.0f);
ImGui.Separator();
ImGui.TextWrapped("Mare Synchronos is a plugin that will replicate your full current character state including all Penumbra mods to other whitelisted Mare Synchronos users. " +
"Note that you will have to have Penumbra as well as Glamourer installed to use this plugin.");
if (!OtherPluginStateOk()) return;
ImGui.SetWindowFontScale(1.5f);
string readThis = "READ THIS CAREFULLY BEFORE REGISTERING";
var textSize = ImGui.CalcTextSize(readThis);
ImGui.SetCursorPosX(ImGui.GetWindowSize().X / 2 - textSize.X / 2);
ImGui.TextColored(ImGuiColors.DalamudRed, readThis);
ImGui.SetWindowFontScale(1.0f);
ImGui.TextWrapped("All of the mod files currently active on your character as well as your current character state will be uploaded to the service you registered yourself at automatically. " +
"The plugin will exclusively upload the necessary mod files and not the whole mod.");
ImGui.TextWrapped("If you are on a data capped internet connection, higher fees due to data usage depending on the amount of downloaded and uploaded mod files might occur. " +
"Mod files will be compressed on up- and download to save on bandwidth usage. Due to varying up- and download speeds, changes in characters might not be visible immediately. " +
"Files present on the service that already represent your active mod files will not be uploaded again. To register at a service you will need to hold ctrl.");
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
ImGui.TextWrapped("The mod files you are uploading are confidential and will not be distributed to parties other than the ones who are requesting the exact same mod files. " +
"Please think about who you are going to whitelist since it is unavoidable that they will receive and locally cache the necessary mod files that you have currently in use. " +
"Locally cached mod files will have arbitrary file names to discourage attempts at replicating the original mod in question.");
ImGui.PopStyleColor();
ImGui.TextWrapped("Mod files that are saved on the service will remain on the service as long as there are requests for the files from clients. After a period of not being used, the mod files will be automatically deleted. " +
"You will also be able to wipe all the files you have personally uploaded on request.");
ImGui.TextColored(ImGuiColors.DalamudRed, "This service is provided as-is.");
ImGui.Separator();
string[] comboEntries = new[] { mainServer, "Custom Service" };
if (ImGui.BeginCombo("Service", comboEntries[serverSelectionIndex]))
{
for (int n = 0; n < comboEntries.Length; n++)
{
SettingsVisible = true;
bool isSelected = serverSelectionIndex == n;
if (ImGui.Selectable(comboEntries[n], isSelected))
{
serverSelectionIndex = n;
}
if (isSelected)
{
ImGui.SetItemDefaultFocus();
}
bool useCustomService = (serverSelectionIndex != 0);
if (apiController.UseCustomService != useCustomService)
{
PluginLog.Debug("Configuration " + apiController.UseCustomService + " changing to " + useCustomService);
apiController.UseCustomService = useCustomService;
configuration.Save();
}
}
ImGui.Spacing();
ImGui.EndCombo();
}
if (apiController.UseCustomService)
{
string serviceAddress = configuration.ApiUri;
ImGui.InputText("Service address", ref serviceAddress, 255);
configuration.ApiUri = serviceAddress;
configuration.Save();
}
PrintServerState();
if (apiController.IsConnected)
{
if (ImGui.Button("Register"))
{
if (ImGui.GetIO().KeyCtrl)
{
await apiController.Register();
}
}
}
ImGui.End();
}
public void DrawSettingsWindow()
private bool OtherPluginStateOk()
{
if (!SettingsVisible)
var penumbraExists = ipcManager.CheckPenumbraAPI();
var glamourerExists = ipcManager.CheckGlamourerAPI();
var penumbraColor = penumbraExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed;
var glamourerColor = glamourerExists ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed;
ImGui.Text("Penumbra:");
ImGui.SameLine();
ImGui.TextColored(penumbraColor, penumbraExists ? "Available" : "Unavailable");
ImGui.Text("Glamourer:");
ImGui.SameLine();
ImGui.TextColored(glamourerColor, glamourerExists ? "Available" : "Unavailable");
if (!penumbraExists || !glamourerExists)
{
return;
ImGui.TextColored(ImGuiColors.DalamudRed, "You need to install both Penumbra and Glamourer and keep them up to date to use Mare Synchronos.");
return false;
}
ImGui.SetNextWindowSize(new Vector2(500, 75), ImGuiCond.Always);
if (ImGui.Begin("QUALITY UI DEVELOPMENT", ref this.settingsVisible,
ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
{
// can't ref a property, so use a local copy
string penumbraFolder = configuration.CacheFolder;
if(ImGui.InputText("Penumbra mod folder", ref penumbraFolder, 255)) {
this.configuration.CacheFolder = penumbraFolder;
this.configuration.Save();
}
}
ImGui.End();
return true;
}
private void PrintServerState()
{
ImGui.Text("Service status of " + (string.IsNullOrEmpty(configuration.ApiUri) ? mainServer : configuration.ApiUri));
ImGui.SameLine();
var color = apiController.IsConnected ? ImGuiColors.ParsedGreen : ImGuiColors.DalamudRed;
ImGui.TextColored(color, apiController.IsConnected ? "Available" : "Unavailable");
}
}
}

View File

@@ -5,32 +5,74 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace MareSynchronos.WebAPI
{
public class ApiController
public class ApiController : IDisposable
{
private readonly Configuration pluginConfiguration;
private string SecretKey => pluginConfiguration.ClientSecret;
private const string mainService = "https://localhost:6591";
public string UID { get; private set; } = string.Empty;
public string SecretKey => pluginConfiguration.ClientSecret.ContainsKey(ApiUri) ? pluginConfiguration.ClientSecret[ApiUri] : string.Empty;
private string CacheFolder => pluginConfiguration.CacheFolder;
private string ApiUri => pluginConfiguration.ApiUri;
public bool UseCustomService
{
get => pluginConfiguration.UseCustomService;
set
{
pluginConfiguration.UseCustomService = value;
_ = Heartbeat();
pluginConfiguration.Save();
}
}
private string ApiUri => UseCustomService ? pluginConfiguration.ApiUri : mainService;
public bool IsConnected { get; set; }
Task heartbeatTask;
CancellationTokenSource cts;
public ApiController(Configuration pluginConfiguration)
{
this.pluginConfiguration = pluginConfiguration;
cts = new CancellationTokenSource();
heartbeatTask = Task.Run(async () =>
{
PluginLog.Debug("Starting heartbeat to " + ApiUri);
while (true && !cts.IsCancellationRequested)
{
await Heartbeat();
await Task.Delay(TimeSpan.FromSeconds(15), cts.Token);
}
PluginLog.Debug("Stopping heartbeat");
}, cts.Token);
}
public async Task Heartbeat()
{
PluginLog.Debug("Sending heartbeat to " + ApiUri);
try
{
PluginLog.Debug("Sending heartbeat to " + ApiUri);
if (ApiUri != mainService) throw new Exception();
IsConnected = true;
}
catch
{
IsConnected = false;
}
}
public async Task<(string, string)> Register()
public async Task Register()
{
PluginLog.Debug("Registering at service " + ApiUri);
return (string.Empty, string.Empty);
var response = ("RandomSecretKey", "RandomUID");
pluginConfiguration.ClientSecret[ApiUri] = response.Item1;
UID = response.Item2;
PluginLog.Debug(pluginConfiguration.ClientSecret[ApiUri]);
// pluginConfiguration.Save();
}
public async Task UploadFile(string filePath)
@@ -73,5 +115,10 @@ namespace MareSynchronos.WebAPI
List<string> whitelist = new();
return whitelist;
}
public void Dispose()
{
cts?.Cancel();
}
}
}