Add PenumbraAPI & GlamourerAPI + Update API & Connector
This commit is contained in:
@@ -13,20 +13,18 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
|
||||
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly NoSnapService _noSnapService;
|
||||
private readonly Dictionary<string, HandledCharaDataEntry> _handledCharaData = new(StringComparer.Ordinal);
|
||||
|
||||
public IReadOnlyDictionary<string, HandledCharaDataEntry> HandledCharaData => _handledCharaData;
|
||||
|
||||
public CharaDataCharacterHandler(ILogger<CharaDataCharacterHandler> logger, MareMediator mediator,
|
||||
GameObjectHandlerFactory gameObjectHandlerFactory, DalamudUtilService dalamudUtilService,
|
||||
IpcManager ipcManager, NoSnapService noSnapService)
|
||||
IpcManager ipcManager)
|
||||
: base(logger, mediator)
|
||||
{
|
||||
_gameObjectHandlerFactory = gameObjectHandlerFactory;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_ipcManager = ipcManager;
|
||||
_noSnapService = noSnapService;
|
||||
mediator.Subscribe<GposeEndMessage>(this, msg =>
|
||||
{
|
||||
foreach (var chara in _handledCharaData)
|
||||
@@ -94,7 +92,6 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
|
||||
_handledCharaData.Remove(handled.Name);
|
||||
await _dalamudUtilService.RunOnFrameworkThread(async () =>
|
||||
{
|
||||
RemoveGposer(handled);
|
||||
await RevertChara(handled.Name, handled.CustomizePlus).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
return true;
|
||||
@@ -103,7 +100,6 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
|
||||
internal void AddHandledChara(HandledCharaDataEntry handledCharaDataEntry)
|
||||
{
|
||||
_handledCharaData.Add(handledCharaDataEntry.Name, handledCharaDataEntry);
|
||||
_ = _dalamudUtilService.RunOnFrameworkThread(() => AddGposer(handledCharaDataEntry));
|
||||
}
|
||||
|
||||
public void UpdateHandledData(Dictionary<string, CharaDataMetaInfoExtendedDto?> newData)
|
||||
@@ -134,23 +130,4 @@ public sealed class CharaDataCharacterHandler : DisposableMediatorSubscriberBase
|
||||
if (handler.Address == nint.Zero) return null;
|
||||
return handler;
|
||||
}
|
||||
|
||||
private int GetGposerObjectIndex(string name)
|
||||
{
|
||||
return _dalamudUtilService.GetGposeCharacterFromObjectTableByName(name, _dalamudUtilService.IsInGpose)?.ObjectIndex ?? -1;
|
||||
}
|
||||
|
||||
private void AddGposer(HandledCharaDataEntry handled)
|
||||
{
|
||||
int objectIndex = GetGposerObjectIndex(handled.Name);
|
||||
if (objectIndex > 0)
|
||||
_noSnapService.AddGposer(objectIndex);
|
||||
}
|
||||
|
||||
private void RemoveGposer(HandledCharaDataEntry handled)
|
||||
{
|
||||
int objectIndex = GetGposerObjectIndex(handled.Name);
|
||||
if (objectIndex > 0)
|
||||
_noSnapService.RemoveGposer(objectIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
{
|
||||
var chatMsg = message.ChatMsg;
|
||||
var prefix = new SeStringBuilder();
|
||||
prefix.AddText("[BnnuyChat] ");
|
||||
prefix.AddText("[SnowChat] ");
|
||||
_chatGui.Print(new XivChatEntry{
|
||||
MessageBytes = [..prefix.Build().Encode(), ..message.ChatMsg.PayloadContent],
|
||||
Name = chatMsg.SenderName,
|
||||
@@ -207,7 +207,7 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
_chatGui.PrintError($"[UmbraSyncSync] Syncshell number #{shellNumber} not found");
|
||||
_chatGui.PrintError($"[UmbraSync] Syncshell number #{shellNumber} not found");
|
||||
}
|
||||
|
||||
public void SendChatShell(int shellNumber, byte[] chatBytes)
|
||||
@@ -236,6 +236,6 @@ public class ChatService : DisposableMediatorSubscriberBase
|
||||
}
|
||||
}
|
||||
|
||||
_chatGui.PrintError($"[UmbraSyncSync] Syncshell number #{shellNumber} not found");
|
||||
_chatGui.PrintError($"[UmbraSync] Syncshell number #{shellNumber} not found");
|
||||
}
|
||||
}
|
||||
@@ -42,11 +42,11 @@ public sealed class CommandManagerService : IDisposable
|
||||
_mareConfigService = mareConfigService;
|
||||
_commandManager.AddHandler(_commandName, new CommandInfo(OnCommand)
|
||||
{
|
||||
HelpMessage = "Opens the UmbraSync UI"
|
||||
HelpMessage = "Opens the Umbra UI"
|
||||
});
|
||||
_commandManager.AddHandler(_commandName2, new CommandInfo(OnCommand)
|
||||
{
|
||||
HelpMessage = "Opens the UmbraSync UI"
|
||||
HelpMessage = "Opens the Umbra UI"
|
||||
});
|
||||
|
||||
// Lazy registration of all possible /ss# commands which tbf is what the game does for linkshells anyway
|
||||
@@ -86,7 +86,7 @@ public sealed class CommandManagerService : IDisposable
|
||||
{
|
||||
if (_apiController.ServerState == WebAPI.SignalR.Utils.ServerState.Disconnecting)
|
||||
{
|
||||
_mediator.Publish(new NotificationMessage("UmbraSync disconnecting", "Cannot use /toggle while UmbraSync is still disconnecting",
|
||||
_mediator.Publish(new NotificationMessage("Umbra disconnecting", "Cannot use /toggle while Umbra is still disconnecting",
|
||||
NotificationType.Error));
|
||||
}
|
||||
|
||||
|
||||
@@ -462,9 +462,9 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
_logger.LogInformation("Starting DalamudUtilService");
|
||||
#pragma warning disable S2696 // Instance members should not write to "static" fields
|
||||
UmbraSyncSync.Plugin.Self.RealOnFrameworkUpdate = this.FrameworkOnUpdate;
|
||||
Umbra.Plugin.Self.RealOnFrameworkUpdate = this.FrameworkOnUpdate;
|
||||
#pragma warning restore S2696
|
||||
_framework.Update += UmbraSyncSync.Plugin.Self.OnFrameworkUpdate;
|
||||
_framework.Update += Umbra.Plugin.Self.OnFrameworkUpdate;
|
||||
if (IsLoggedIn)
|
||||
{
|
||||
_classJobId = _clientState.LocalPlayer!.ClassJob.RowId;
|
||||
@@ -479,7 +479,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
|
||||
_logger.LogTrace("Stopping {type}", GetType());
|
||||
|
||||
Mediator.UnsubscribeAll(this);
|
||||
_framework.Update -= UmbraSyncSync.Plugin.Self.OnFrameworkUpdate;
|
||||
_framework.Update -= Umbra.Plugin.Self.OnFrameworkUpdate;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
using Dalamud.Plugin;
|
||||
using MareSynchronos.Interop.Ipc;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
public sealed class NoSnapService : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
private record NoSnapConfig
|
||||
{
|
||||
[JsonPropertyName("listOfPlugins")]
|
||||
public string[]? ListOfPlugins { get; set; }
|
||||
}
|
||||
|
||||
private readonly ILogger<NoSnapService> _logger;
|
||||
private readonly IDalamudPluginInterface _pluginInterface;
|
||||
private readonly Dictionary<string, bool> _listOfPlugins = new(StringComparer.Ordinal)
|
||||
{
|
||||
["Snapper"] = false,
|
||||
["Snappy"] = false,
|
||||
["Meddle.Plugin"] = false,
|
||||
};
|
||||
private static readonly HashSet<int> _gposers = new();
|
||||
private static readonly HashSet<string> _gposersNamed = new(StringComparer.Ordinal);
|
||||
private readonly IHostApplicationLifetime _hostApplicationLifetime;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly IpcManager _ipcManager;
|
||||
private readonly RemoteConfigurationService _remoteConfig;
|
||||
|
||||
public static bool AnyLoaded { get; private set; } = false;
|
||||
public static string ActivePlugins { get; private set; } = string.Empty;
|
||||
|
||||
public MareMediator Mediator { get; init; }
|
||||
|
||||
public NoSnapService(ILogger<NoSnapService> logger, IDalamudPluginInterface pluginInterface, MareMediator mediator,
|
||||
IHostApplicationLifetime hostApplicationLifetime, DalamudUtilService dalamudUtilService, IpcManager ipcManager,
|
||||
RemoteConfigurationService remoteConfig)
|
||||
{
|
||||
_logger = logger;
|
||||
_pluginInterface = pluginInterface;
|
||||
Mediator = mediator;
|
||||
_hostApplicationLifetime = hostApplicationLifetime;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
_ipcManager = ipcManager;
|
||||
_remoteConfig = remoteConfig;
|
||||
|
||||
Mediator.Subscribe<GposeEndMessage>(this, msg => ClearGposeList());
|
||||
Mediator.Subscribe<CutsceneEndMessage>(this, msg => ClearGposeList());
|
||||
}
|
||||
|
||||
public void AddGposer(int objectIndex)
|
||||
{
|
||||
if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogTrace("Immediately reverting object index {id}", objectIndex);
|
||||
RevertAndRedraw(objectIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Registering gposer object index {id}", objectIndex);
|
||||
lock (_gposers)
|
||||
_gposers.Add(objectIndex);
|
||||
}
|
||||
|
||||
public void RemoveGposer(int objectIndex)
|
||||
{
|
||||
_logger.LogTrace("Un-registering gposer object index {id}", objectIndex);
|
||||
lock (_gposers)
|
||||
_gposers.Remove(objectIndex);
|
||||
}
|
||||
|
||||
public void AddGposerNamed(string name)
|
||||
{
|
||||
if (AnyLoaded || _hostApplicationLifetime.ApplicationStopping.IsCancellationRequested)
|
||||
{
|
||||
_logger.LogTrace("Immediately reverting {name}", name);
|
||||
RevertAndRedraw(name);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogTrace("Registering gposer {name}", name);
|
||||
lock (_gposers)
|
||||
_gposersNamed.Add(name);
|
||||
}
|
||||
|
||||
private void ClearGposeList()
|
||||
{
|
||||
if (_gposers.Count > 0 || _gposersNamed.Count > 0)
|
||||
_logger.LogTrace("Clearing gposer list");
|
||||
lock (_gposers)
|
||||
_gposers.Clear();
|
||||
lock (_gposersNamed)
|
||||
_gposersNamed.Clear();
|
||||
}
|
||||
|
||||
private void RevertAndRedraw(int objIndex, Guid applicationId = default)
|
||||
{
|
||||
if (applicationId == default)
|
||||
applicationId = Guid.NewGuid();
|
||||
|
||||
try
|
||||
{
|
||||
_ipcManager.Glamourer.RevertNow(_logger, applicationId, objIndex);
|
||||
_ipcManager.Penumbra.RedrawNow(_logger, applicationId, objIndex);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private void RevertAndRedraw(string name, Guid applicationId = default)
|
||||
{
|
||||
if (applicationId == default)
|
||||
applicationId = Guid.NewGuid();
|
||||
|
||||
try
|
||||
{
|
||||
_ipcManager.Glamourer.RevertByNameNow(_logger, applicationId, name);
|
||||
var addr = _dalamudUtilService.GetPlayerCharacterFromCachedTableByName(name);
|
||||
if (addr != 0)
|
||||
{
|
||||
var obj = _dalamudUtilService.CreateGameObject(addr);
|
||||
if (obj != null)
|
||||
_ipcManager.Penumbra.RedrawNow(_logger, applicationId, obj.ObjectIndex);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private void RevertGposers()
|
||||
{
|
||||
List<int>? gposersList = null;
|
||||
List<string>? gposersList2 = null;
|
||||
|
||||
lock (_gposers)
|
||||
{
|
||||
if (_gposers.Count > 0)
|
||||
{
|
||||
gposersList = _gposers.ToList();
|
||||
_gposers.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
lock (_gposersNamed)
|
||||
{
|
||||
if (_gposersNamed.Count > 0)
|
||||
{
|
||||
gposersList2 = _gposersNamed.ToList();
|
||||
_gposersNamed.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (gposersList == null && gposersList2 == null)
|
||||
return;
|
||||
|
||||
_logger.LogInformation("Reverting gposers");
|
||||
|
||||
_dalamudUtilService.RunOnFrameworkThread(() =>
|
||||
{
|
||||
Guid applicationId = Guid.NewGuid();
|
||||
|
||||
foreach (var gposer in gposersList ?? [])
|
||||
RevertAndRedraw(gposer, applicationId);
|
||||
|
||||
foreach (var gposerName in gposersList2 ?? [])
|
||||
RevertAndRedraw(gposerName, applicationId);
|
||||
}).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var config = await _remoteConfig.GetConfigAsync<NoSnapConfig>("noSnap").ConfigureAwait(false) ?? new();
|
||||
|
||||
if (config.ListOfPlugins != null)
|
||||
{
|
||||
_listOfPlugins.Clear();
|
||||
foreach (var pluginName in config.ListOfPlugins)
|
||||
_listOfPlugins.TryAdd(pluginName, value: false);
|
||||
}
|
||||
|
||||
foreach (var pluginName in _listOfPlugins.Keys)
|
||||
{
|
||||
_listOfPlugins[pluginName] = PluginWatcherService.GetInitialPluginState(_pluginInterface, pluginName)?.IsLoaded ?? false;
|
||||
Mediator.SubscribeKeyed<PluginChangeMessage>(this, pluginName, (msg) =>
|
||||
{
|
||||
_listOfPlugins[pluginName] = msg.IsLoaded;
|
||||
_logger.LogDebug("{pluginName} isLoaded = {isLoaded}", pluginName, msg.IsLoaded);
|
||||
Update();
|
||||
});
|
||||
}
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
RevertGposers();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
bool anyLoadedNow = _listOfPlugins.Values.Any(p => p);
|
||||
|
||||
if (AnyLoaded != anyLoadedNow)
|
||||
{
|
||||
AnyLoaded = anyLoadedNow;
|
||||
Mediator.Publish(new RecalculatePerformanceMessage(null));
|
||||
|
||||
if (AnyLoaded)
|
||||
{
|
||||
RevertGposers();
|
||||
var pluginList = string.Join(", ", _listOfPlugins.Where(p => p.Value).Select(p => p.Key));
|
||||
Mediator.Publish(new NotificationMessage("Incompatible plugin loaded", $"Synced player appearances will not apply until incompatible plugins are disabled: {pluginList}.",
|
||||
NotificationType.Error));
|
||||
ActivePlugins = pluginList;
|
||||
}
|
||||
else
|
||||
{
|
||||
ActivePlugins = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,19 +41,19 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ
|
||||
|
||||
private void PrintErrorChat(string? message)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[UmbraSyncSync] Error: " + message);
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[UmbraSync] Error: " + message);
|
||||
_chatGui.PrintError(se.BuiltString);
|
||||
}
|
||||
|
||||
private void PrintInfoChat(string? message)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[UmbraSyncSync] Info: ").AddItalics(message ?? string.Empty);
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[UmbraSync] Info: ").AddItalics(message ?? string.Empty);
|
||||
_chatGui.Print(se.BuiltString);
|
||||
}
|
||||
|
||||
private void PrintWarnChat(string? message)
|
||||
{
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[UmbraSyncSync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
|
||||
SeStringBuilder se = new SeStringBuilder().AddText("[UmbraSync] ").AddUiForeground("Warning: " + (message ?? string.Empty), 31).AddUiForegroundOff();
|
||||
_chatGui.Print(se.BuiltString);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
using Chaos.NaCl;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
public sealed class RemoteConfigurationService
|
||||
{
|
||||
private readonly static Dictionary<string, string> ConfigPublicKeys = new(StringComparer.Ordinal)
|
||||
{
|
||||
{ "UMBR4KEY", "+MwCXedODmU+yD7vtdI+Ho2iLx+PV3U0H2XRLP/gReA=" }
|
||||
};
|
||||
|
||||
private readonly static string[] ConfigSources = [
|
||||
"https://umbra-sync.net/config/umbra.json"
|
||||
];
|
||||
|
||||
private readonly ILogger<RemoteConfigurationService> _logger;
|
||||
private readonly RemoteConfigCacheService _configService;
|
||||
private readonly Task _initTask;
|
||||
|
||||
public RemoteConfigurationService(ILogger<RemoteConfigurationService> logger, RemoteConfigCacheService configService)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_initTask = Task.Run(DownloadConfig);
|
||||
}
|
||||
|
||||
public async Task<JsonObject> GetConfigAsync(string sectionName)
|
||||
{
|
||||
await _initTask.ConfigureAwait(false);
|
||||
if (!_configService.Current.Configuration.TryGetPropertyValue(sectionName, out var section))
|
||||
section = null;
|
||||
return (section as JsonObject) ?? new();
|
||||
}
|
||||
|
||||
public async Task<T?> GetConfigAsync<T>(string sectionName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = await GetConfigAsync(sectionName).ConfigureAwait(false);
|
||||
return JsonSerializer.Deserialize<T>(json);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Invalid JSON in remote config: {sectionName}", sectionName);
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadConfig()
|
||||
{
|
||||
string? jsonResponse = null;
|
||||
|
||||
foreach (var remoteUrl in ConfigSources)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Fetching {url}", remoteUrl);
|
||||
|
||||
using var httpClient = new HttpClient(
|
||||
new HttpClientHandler
|
||||
{
|
||||
AllowAutoRedirect = true,
|
||||
MaxAutomaticRedirections = 5
|
||||
}
|
||||
);
|
||||
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(6);
|
||||
|
||||
var ver = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MareSynchronos", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, remoteUrl);
|
||||
|
||||
if (remoteUrl.Equals(_configService.Current.Origin, StringComparison.Ordinal))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_configService.Current.ETag))
|
||||
request.Headers.IfNoneMatch.Add(new EntityTagHeaderValue(_configService.Current.ETag));
|
||||
|
||||
if (_configService.Current.LastModified != null)
|
||||
request.Headers.IfModifiedSince = _configService.Current.LastModified;
|
||||
}
|
||||
|
||||
var response = await httpClient.SendAsync(request).ConfigureAwait(false);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.NotModified)
|
||||
{
|
||||
_logger.LogDebug("Using cached remote configuration from {url}", remoteUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var contentType = response.Content.Headers.ContentType?.MediaType;
|
||||
|
||||
if (contentType == null || !contentType.Equals("application/json", StringComparison.Ordinal))
|
||||
{
|
||||
_logger.LogWarning("HTTP request for remote config failed: wrong MIME type");
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Downloaded new configuration from {url}", remoteUrl);
|
||||
|
||||
_configService.Current.Origin = remoteUrl;
|
||||
_configService.Current.ETag = response.Headers.ETag?.ToString() ?? string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
if (response.Content.Headers.Contains("Last-Modified"))
|
||||
{
|
||||
var lastModified = response.Content.Headers.GetValues("Last-Modified").First();
|
||||
_configService.Current.LastModified = DateTimeOffset.Parse(lastModified, System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_configService.Current.LastModified = null;
|
||||
}
|
||||
|
||||
jsonResponse = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "HTTP request for remote config failed");
|
||||
|
||||
if (remoteUrl.Equals(_configService.Current.Origin, StringComparison.Ordinal))
|
||||
{
|
||||
_configService.Current.ETag = string.Empty;
|
||||
_configService.Current.LastModified = null;
|
||||
_configService.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonResponse == null)
|
||||
{
|
||||
_logger.LogWarning("Could not download remote config");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var jsonDoc = JsonNode.Parse(jsonResponse) as JsonObject;
|
||||
|
||||
if (jsonDoc == null)
|
||||
{
|
||||
_logger.LogWarning("Downloaded remote config is not a JSON object");
|
||||
return;
|
||||
}
|
||||
|
||||
LoadConfig(jsonDoc);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Invalid JSON in remote config response");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool VerifySignature(string message, ulong ts, string signature, string pubKey)
|
||||
{
|
||||
byte[] msg = [.. BitConverter.GetBytes(ts), .. Encoding.UTF8.GetBytes(message)];
|
||||
byte[] sig = Convert.FromBase64String(signature);
|
||||
byte[] pub = Convert.FromBase64String(pubKey);
|
||||
return Ed25519.Verify(sig, msg, pub);
|
||||
}
|
||||
|
||||
private void LoadConfig(JsonObject jsonDoc)
|
||||
{
|
||||
var ts = jsonDoc["ts"]!.GetValue<ulong>();
|
||||
|
||||
if (ts <= _configService.Current.Timestamp)
|
||||
{
|
||||
_logger.LogDebug("Remote configuration is not newer than cached config");
|
||||
return;
|
||||
}
|
||||
|
||||
var signatures = jsonDoc["sig"]!.AsObject();
|
||||
var configString = jsonDoc["config"]!.GetValue<string>();
|
||||
bool verified = signatures.Any(sig =>
|
||||
ConfigPublicKeys.TryGetValue(sig.Key, out var pubKey) &&
|
||||
VerifySignature(configString, ts, sig.Value!.GetValue<string>(), pubKey));
|
||||
|
||||
if (!verified)
|
||||
{
|
||||
_logger.LogWarning("Could not verify signature for downloaded remote config");
|
||||
return;
|
||||
}
|
||||
|
||||
_configService.Current.Configuration = JsonNode.Parse(configString)!.AsObject();
|
||||
_configService.Current.Timestamp = ts;
|
||||
_configService.Save();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
public record RepoChangeConfig
|
||||
{
|
||||
[JsonPropertyName("current_repo")]
|
||||
public string? CurrentRepo { get; set; }
|
||||
|
||||
[JsonPropertyName("valid_repos")]
|
||||
public string[]? ValidRepos { get; set; }
|
||||
}
|
||||
@@ -1,401 +0,0 @@
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MareSynchronos.Services;
|
||||
|
||||
/* Reflection code based almost entirely on ECommons DalamudReflector
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 NightmareXIV
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
*/
|
||||
|
||||
public sealed class RepoChangeService : IHostedService
|
||||
{
|
||||
#region Reflection Helpers
|
||||
private const BindingFlags AllFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static;
|
||||
private const BindingFlags StaticFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
|
||||
private const BindingFlags InstanceFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
|
||||
private static object GetFoP(object obj, string name)
|
||||
{
|
||||
Type? type = obj.GetType();
|
||||
while (type != null)
|
||||
{
|
||||
var fieldInfo = type.GetField(name, AllFlags);
|
||||
if (fieldInfo != null)
|
||||
{
|
||||
return fieldInfo.GetValue(obj)!;
|
||||
}
|
||||
var propertyInfo = type.GetProperty(name, AllFlags);
|
||||
if (propertyInfo != null)
|
||||
{
|
||||
return propertyInfo.GetValue(obj)!;
|
||||
}
|
||||
type = type.BaseType;
|
||||
}
|
||||
throw new Exception($"Reflection GetFoP failed (not found: {obj.GetType().Name}.{name})");
|
||||
}
|
||||
|
||||
private static T GetFoP<T>(object obj, string name)
|
||||
{
|
||||
return (T)GetFoP(obj, name);
|
||||
}
|
||||
|
||||
private static void SetFoP(object obj, string name, object value)
|
||||
{
|
||||
var type = obj.GetType();
|
||||
var field = type.GetField(name, AllFlags);
|
||||
if (field != null)
|
||||
{
|
||||
field.SetValue(obj, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
var prop = type.GetProperty(name, AllFlags)!;
|
||||
if (prop == null)
|
||||
throw new Exception($"Reflection SetFoP failed (not found: {type.Name}.{name})");
|
||||
prop.SetValue(obj, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static object? Call(object obj, string name, object[] @params, bool matchExactArgumentTypes = false)
|
||||
{
|
||||
MethodInfo? info;
|
||||
var type = obj.GetType();
|
||||
if (!matchExactArgumentTypes)
|
||||
{
|
||||
info = type.GetMethod(name, AllFlags);
|
||||
}
|
||||
else
|
||||
{
|
||||
info = type.GetMethod(name, AllFlags, @params.Select(x => x.GetType()).ToArray());
|
||||
}
|
||||
if (info == null)
|
||||
throw new Exception($"Reflection Call failed (not found: {type.Name}.{name})");
|
||||
return info.Invoke(obj, @params);
|
||||
}
|
||||
|
||||
private static T Call<T>(object obj, string name, object[] @params, bool matchExactArgumentTypes = false)
|
||||
{
|
||||
return (T)Call(obj, name, @params, matchExactArgumentTypes)!;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Dalamud Reflection
|
||||
public object GetService(string serviceFullName)
|
||||
{
|
||||
return _pluginInterface.GetType().Assembly.
|
||||
GetType("Dalamud.Service`1", true)!.MakeGenericType(_pluginInterface.GetType().Assembly.GetType(serviceFullName, true)!).
|
||||
GetMethod("Get")!.Invoke(null, BindingFlags.Default, null, Array.Empty<object>(), null)!;
|
||||
}
|
||||
|
||||
private object GetPluginManager()
|
||||
{
|
||||
return _pluginInterface.GetType().Assembly.
|
||||
GetType("Dalamud.Service`1", true)!.MakeGenericType(_pluginInterface.GetType().Assembly.GetType("Dalamud.Plugin.Internal.PluginManager", true)!).
|
||||
GetMethod("Get")!.Invoke(null, BindingFlags.Default, null, Array.Empty<object>(), null)!;
|
||||
}
|
||||
|
||||
private void ReloadPluginMasters()
|
||||
{
|
||||
var mgr = GetService("Dalamud.Plugin.Internal.PluginManager");
|
||||
var pluginReload = mgr.GetType().GetMethod("SetPluginReposFromConfigAsync", BindingFlags.Instance | BindingFlags.Public)!;
|
||||
pluginReload.Invoke(mgr, [true]);
|
||||
}
|
||||
|
||||
public void SaveDalamudConfig()
|
||||
{
|
||||
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
|
||||
var configSave = conf?.GetType().GetMethod("QueueSave", BindingFlags.Instance | BindingFlags.Public);
|
||||
configSave?.Invoke(conf, null);
|
||||
}
|
||||
|
||||
private IEnumerable<object> GetRepoByURL(string repoURL)
|
||||
{
|
||||
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
|
||||
var repolist = (System.Collections.IEnumerable)GetFoP(conf, "ThirdRepoList");
|
||||
foreach (var r in repolist)
|
||||
{
|
||||
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
|
||||
yield return r;
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasRepo(string repoURL)
|
||||
{
|
||||
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
|
||||
var repolist = (System.Collections.IEnumerable)GetFoP(conf, "ThirdRepoList");
|
||||
foreach (var r in repolist)
|
||||
{
|
||||
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void AddRepo(string repoURL, bool enabled)
|
||||
{
|
||||
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
|
||||
var repolist = (System.Collections.IEnumerable)GetFoP(conf, "ThirdRepoList");
|
||||
foreach (var r in repolist)
|
||||
{
|
||||
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
}
|
||||
var instance = Activator.CreateInstance(_pluginInterface.GetType().Assembly.GetType("Dalamud.Configuration.ThirdPartyRepoSettings")!)!;
|
||||
SetFoP(instance, "Url", repoURL);
|
||||
SetFoP(instance, "IsEnabled", enabled);
|
||||
GetFoP<System.Collections.IList>(conf, "ThirdRepoList").Add(instance!);
|
||||
}
|
||||
|
||||
private void RemoveRepo(string repoURL)
|
||||
{
|
||||
var toRemove = new List<object>();
|
||||
var conf = GetService("Dalamud.Configuration.Internal.DalamudConfiguration");
|
||||
var repolist = (System.Collections.IList)GetFoP(conf, "ThirdRepoList");
|
||||
foreach (var r in repolist)
|
||||
{
|
||||
if (((string)GetFoP(r, "Url")).Equals(repoURL, StringComparison.OrdinalIgnoreCase))
|
||||
toRemove.Add(r);
|
||||
}
|
||||
foreach (var r in toRemove)
|
||||
repolist.Remove(r);
|
||||
}
|
||||
|
||||
public List<(object LocalPlugin, string InstalledFromUrl)> GetLocalPluginsByName(string internalName)
|
||||
{
|
||||
List<(object LocalPlugin, string RepoURL)> result = [];
|
||||
|
||||
var pluginManager = GetPluginManager();
|
||||
var installedPlugins = (System.Collections.IList)pluginManager.GetType().GetProperty("InstalledPlugins")!.GetValue(pluginManager)!;
|
||||
|
||||
foreach (var plugin in installedPlugins)
|
||||
{
|
||||
if (((string)plugin.GetType().GetProperty("InternalName")!.GetValue(plugin)!).Equals(internalName, StringComparison.Ordinal))
|
||||
{
|
||||
var type = plugin.GetType();
|
||||
if (type.Name.Equals("LocalDevPlugin", StringComparison.Ordinal))
|
||||
continue;
|
||||
var manifest = GetFoP(plugin, "manifest");
|
||||
string installedFromUrl = (string)GetFoP(manifest, "InstalledFromUrl");
|
||||
result.Add((plugin, installedFromUrl));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
#endregion
|
||||
|
||||
private readonly ILogger<RepoChangeService> _logger;
|
||||
private readonly RemoteConfigurationService _remoteConfig;
|
||||
private readonly IDalamudPluginInterface _pluginInterface;
|
||||
private readonly IFramework _framework;
|
||||
|
||||
public RepoChangeService(ILogger<RepoChangeService> logger, RemoteConfigurationService remoteConfig, IDalamudPluginInterface pluginInterface, IFramework framework)
|
||||
{
|
||||
_logger = logger;
|
||||
_remoteConfig = remoteConfig;
|
||||
_pluginInterface = pluginInterface;
|
||||
_framework = framework;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug("Starting RepoChange Service");
|
||||
var repoChangeConfig = await _remoteConfig.GetConfigAsync<RepoChangeConfig>("repoChange").ConfigureAwait(false) ?? new();
|
||||
|
||||
var currentRepo = repoChangeConfig.CurrentRepo;
|
||||
var validRepos = (repoChangeConfig.ValidRepos ?? []).ToList();
|
||||
|
||||
if (!currentRepo.IsNullOrEmpty() && !validRepos.Contains(currentRepo, StringComparer.Ordinal))
|
||||
validRepos.Add(currentRepo);
|
||||
|
||||
if (validRepos.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("No valid repos configured, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
await _framework.RunOnTick(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var internalName = Assembly.GetExecutingAssembly().GetName().Name!;
|
||||
var localPlugins = GetLocalPluginsByName(internalName);
|
||||
|
||||
var suffix = string.Empty;
|
||||
|
||||
if (localPlugins.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("Skipping: No intalled plugin found");
|
||||
return;
|
||||
}
|
||||
|
||||
var hasValidCustomRepoUrl = false;
|
||||
|
||||
foreach (var vr in validRepos)
|
||||
{
|
||||
var vrCN = vr.Replace(".json", "_CN.json", StringComparison.Ordinal);
|
||||
var vrKR = vr.Replace(".json", "_KR.json", StringComparison.Ordinal);
|
||||
if (HasRepo(vr) || HasRepo(vrCN) || HasRepo(vrKR))
|
||||
{
|
||||
hasValidCustomRepoUrl = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
List<string> oldRepos = [];
|
||||
var pluginRepoUrl = localPlugins[0].InstalledFromUrl;
|
||||
|
||||
if (pluginRepoUrl.Contains("_CN.json", StringComparison.Ordinal))
|
||||
suffix = "_CN";
|
||||
else if (pluginRepoUrl.Contains("_KR.json", StringComparison.Ordinal))
|
||||
suffix = "_KR";
|
||||
|
||||
bool hasOldPluginRepoUrl = false;
|
||||
|
||||
foreach (var plugin in localPlugins)
|
||||
{
|
||||
foreach (var vr in validRepos)
|
||||
{
|
||||
var validRepo = vr.Replace(".json", $"{suffix}.json");
|
||||
if (!plugin.InstalledFromUrl.Equals(validRepo, StringComparison.Ordinal))
|
||||
{
|
||||
oldRepos.Add(plugin.InstalledFromUrl);
|
||||
hasOldPluginRepoUrl = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasValidCustomRepoUrl)
|
||||
{
|
||||
if (hasOldPluginRepoUrl)
|
||||
_logger.LogInformation("Result: Repo URL is up to date, but plugin install source is incorrect");
|
||||
else
|
||||
_logger.LogInformation("Result: Repo URL is up to date");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Result: Repo URL needs to be replaced");
|
||||
}
|
||||
|
||||
if (currentRepo.IsNullOrEmpty())
|
||||
{
|
||||
_logger.LogWarning("No current repo URL configured");
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre-test plugin repo url rewriting to ensure it succeeds before replacing the custom repo URL
|
||||
if (hasOldPluginRepoUrl)
|
||||
{
|
||||
foreach (var plugin in localPlugins)
|
||||
{
|
||||
var manifest = GetFoP(plugin.LocalPlugin, "manifest");
|
||||
if (manifest == null)
|
||||
throw new Exception("Plugin manifest is null");
|
||||
var manifestFile = GetFoP(plugin.LocalPlugin, "manifestFile");
|
||||
if (manifestFile == null)
|
||||
throw new Exception("Plugin manifestFile is null");
|
||||
var repo = GetFoP(manifest, "InstalledFromUrl");
|
||||
if (((string)repo).IsNullOrEmpty())
|
||||
throw new Exception("Plugin repo url is null or empty");
|
||||
SetFoP(manifest, "InstalledFromUrl", repo);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasValidCustomRepoUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var oldRepo in oldRepos)
|
||||
{
|
||||
_logger.LogInformation("* Removing old repo: {r}", oldRepo);
|
||||
RemoveRepo(oldRepo);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_logger.LogInformation("* Adding current repo: {r}", currentRepo);
|
||||
AddRepo(currentRepo, true);
|
||||
}
|
||||
}
|
||||
|
||||
// This time do it for real, and crash the game if we fail, to avoid saving a broken state
|
||||
if (hasOldPluginRepoUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("* Updating plugins");
|
||||
foreach (var plugin in localPlugins)
|
||||
{
|
||||
var manifest = GetFoP(plugin.LocalPlugin, "manifest");
|
||||
if (manifest == null)
|
||||
throw new Exception("Plugin manifest is null");
|
||||
var manifestFile = GetFoP(plugin.LocalPlugin, "manifestFile");
|
||||
if (manifestFile == null)
|
||||
throw new Exception("Plugin manifestFile is null");
|
||||
var repo = GetFoP(manifest, "InstalledFromUrl");
|
||||
if (((string)repo).IsNullOrEmpty())
|
||||
throw new Exception("Plugin repo url is null or empty");
|
||||
SetFoP(manifest, "InstalledFromUrl", currentRepo);
|
||||
Call(manifest, "Save", [manifestFile, "RepoChange"]);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Exception while changing plugin install repo");
|
||||
foreach (var oldRepo in oldRepos)
|
||||
{
|
||||
_logger.LogInformation("* Restoring old repo: {r}", oldRepo);
|
||||
AddRepo(oldRepo, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasValidCustomRepoUrl || hasOldPluginRepoUrl)
|
||||
{
|
||||
_logger.LogInformation("* Saving dalamud config");
|
||||
SaveDalamudConfig();
|
||||
_logger.LogInformation("* Reloading plugin masters");
|
||||
ReloadPluginMasters();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Exception in RepoChangeService");
|
||||
}
|
||||
}, default, 10, cancellationToken).ConfigureAwait(false);
|
||||
_logger.LogInformation("Started RepoChangeService");
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_ = cancellationToken;
|
||||
_logger.LogDebug("Stopping RepoChange Service");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -496,17 +496,17 @@ public class ServerConfigurationManager
|
||||
|
||||
private void EnsureMainExists()
|
||||
{
|
||||
bool lopExists = false;
|
||||
bool elfExists = false;
|
||||
for (int i = 0; i < _configService.Current.ServerStorage.Count; ++i)
|
||||
{
|
||||
var x = _configService.Current.ServerStorage[i];
|
||||
if (x.ServerUri.Equals(ApiController.UmbraSyncServiceUri, StringComparison.OrdinalIgnoreCase))
|
||||
lopExists = true;
|
||||
if (x.ServerUri.Equals(ApiController.UmbraServiceUri, StringComparison.OrdinalIgnoreCase))
|
||||
elfExists = true;
|
||||
}
|
||||
if (!lopExists)
|
||||
if (!elfExists)
|
||||
{
|
||||
_logger.LogDebug("Re-adding missing server {uri}", ApiController.UmbraSyncServiceUri);
|
||||
_configService.Current.ServerStorage.Insert(0, new ServerStorage() { ServerUri = ApiController.UmbraSyncServiceUri, ServerName = ApiController.UmbraSyncServer });
|
||||
_logger.LogDebug("Re-adding missing server {uri}", ApiController.UmbraServiceUri);
|
||||
_configService.Current.ServerStorage.Insert(0, new ServerStorage() { ServerUri = ApiController.UmbraServiceUri, ServerName = ApiController.UmbraServer });
|
||||
if (_configService.Current.CurrentServer >= 0)
|
||||
_configService.Current.CurrentServer++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user