Initial
This commit is contained in:
42
MareSynchronos/Interop/BlockedCharacterHandler.cs
Normal file
42
MareSynchronos/Interop/BlockedCharacterHandler.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Interop;
|
||||
|
||||
public unsafe class BlockedCharacterHandler
|
||||
{
|
||||
private sealed record CharaData(ulong AccId, ulong ContentId);
|
||||
private readonly Dictionary<CharaData, bool> _blockedCharacterCache = new();
|
||||
|
||||
private readonly ILogger<BlockedCharacterHandler> _logger;
|
||||
|
||||
public BlockedCharacterHandler(ILogger<BlockedCharacterHandler> logger, IGameInteropProvider gameInteropProvider)
|
||||
{
|
||||
gameInteropProvider.InitializeFromAttributes(this);
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private static CharaData GetIdsFromPlayerPointer(nint ptr)
|
||||
{
|
||||
if (ptr == nint.Zero) return new(0, 0);
|
||||
var castChar = ((BattleChara*)ptr);
|
||||
return new(castChar->Character.AccountId, castChar->Character.ContentId);
|
||||
}
|
||||
|
||||
public bool IsCharacterBlocked(nint ptr, out bool firstTime)
|
||||
{
|
||||
firstTime = false;
|
||||
var combined = GetIdsFromPlayerPointer(ptr);
|
||||
if (_blockedCharacterCache.TryGetValue(combined, out var isBlocked))
|
||||
return isBlocked;
|
||||
|
||||
firstTime = true;
|
||||
var blockStatus = InfoProxyBlacklist.Instance()->GetBlockResultType(combined.AccId, combined.ContentId);
|
||||
_logger.LogTrace("CharaPtr {ptr} is BlockStatus: {status}", ptr, blockStatus);
|
||||
if ((int)blockStatus == 0)
|
||||
return false;
|
||||
return _blockedCharacterCache[combined] = blockStatus != InfoProxyBlacklist.BlockResultType.NotBlocked;
|
||||
}
|
||||
}
|
55
MareSynchronos/Interop/DalamudLogger.cs
Normal file
55
MareSynchronos/Interop/DalamudLogger.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using Dalamud.Plugin.Services;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronos.Interop;
|
||||
|
||||
internal sealed class DalamudLogger : ILogger
|
||||
{
|
||||
private readonly MareConfigService _mareConfigService;
|
||||
private readonly string _name;
|
||||
private readonly IPluginLog _pluginLog;
|
||||
|
||||
public DalamudLogger(string name, MareConfigService mareConfigService, IPluginLog pluginLog)
|
||||
{
|
||||
_name = name;
|
||||
_mareConfigService = mareConfigService;
|
||||
_pluginLog = pluginLog;
|
||||
}
|
||||
|
||||
public IDisposable BeginScope<TState>(TState state) where TState : notnull => default!;
|
||||
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
return (int)_mareConfigService.Current.LogLevel <= (int)logLevel;
|
||||
}
|
||||
|
||||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel)) return;
|
||||
|
||||
if ((int)logLevel <= (int)LogLevel.Information)
|
||||
_pluginLog.Information($"[{_name}]{{{(int)logLevel}}} {state}");
|
||||
else
|
||||
{
|
||||
StringBuilder sb = new();
|
||||
sb.Append($"[{_name}]{{{(int)logLevel}}} {state}: {exception?.Message}");
|
||||
if (!string.IsNullOrWhiteSpace(exception?.StackTrace))
|
||||
sb.AppendLine(exception?.StackTrace);
|
||||
var innerException = exception?.InnerException;
|
||||
while (innerException != null)
|
||||
{
|
||||
sb.AppendLine($"InnerException {innerException}: {innerException.Message}");
|
||||
sb.AppendLine(innerException.StackTrace);
|
||||
innerException = innerException.InnerException;
|
||||
}
|
||||
if (logLevel == LogLevel.Warning)
|
||||
_pluginLog.Warning(sb.ToString());
|
||||
else if (logLevel == LogLevel.Error)
|
||||
_pluginLog.Error(sb.ToString());
|
||||
else
|
||||
_pluginLog.Fatal(sb.ToString());
|
||||
}
|
||||
}
|
||||
}
|
44
MareSynchronos/Interop/DalamudLoggingProvider.cs
Normal file
44
MareSynchronos/Interop/DalamudLoggingProvider.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Dalamud.Plugin.Services;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace MareSynchronos.Interop;
|
||||
|
||||
[ProviderAlias("Dalamud")]
|
||||
public sealed class DalamudLoggingProvider : ILoggerProvider
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, DalamudLogger> _loggers =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly MareConfigService _mareConfigService;
|
||||
private readonly IPluginLog _pluginLog;
|
||||
|
||||
public DalamudLoggingProvider(MareConfigService mareConfigService, IPluginLog pluginLog)
|
||||
{
|
||||
_mareConfigService = mareConfigService;
|
||||
_pluginLog = pluginLog;
|
||||
}
|
||||
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
string catName = categoryName.Split(".", StringSplitOptions.RemoveEmptyEntries).Last();
|
||||
if (catName.Length > 15)
|
||||
{
|
||||
catName = string.Join("", catName.Take(6)) + "..." + string.Join("", catName.TakeLast(6));
|
||||
}
|
||||
else
|
||||
{
|
||||
catName = string.Join("", Enumerable.Range(0, 15 - catName.Length).Select(_ => " ")) + catName;
|
||||
}
|
||||
|
||||
return _loggers.GetOrAdd(catName, name => new DalamudLogger(name, _mareConfigService, _pluginLog));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_loggers.Clear();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
20
MareSynchronos/Interop/DalamudLoggingProviderExtensions.cs
Normal file
20
MareSynchronos/Interop/DalamudLoggingProviderExtensions.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using Dalamud.Plugin.Services;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Interop;
|
||||
|
||||
public static class DalamudLoggingProviderExtensions
|
||||
{
|
||||
public static ILoggingBuilder AddDalamudLogging(this ILoggingBuilder builder, IPluginLog pluginLog)
|
||||
{
|
||||
builder.ClearProviders();
|
||||
|
||||
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, DalamudLoggingProvider>
|
||||
(b => new DalamudLoggingProvider(b.GetRequiredService<MareConfigService>(), pluginLog)));
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
333
MareSynchronos/Interop/GameChatHooks.cs
Normal file
333
MareSynchronos/Interop/GameChatHooks.cs
Normal file
@@ -0,0 +1,333 @@
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Hooking;
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Shell;
|
||||
using FFXIVClientStructs.FFXIV.Component.Shell;
|
||||
using MareSynchronos.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Interop;
|
||||
|
||||
public record ChatChannelOverride
|
||||
{
|
||||
public string ChannelName = string.Empty;
|
||||
public Action<byte[]>? ChatMessageHandler;
|
||||
}
|
||||
|
||||
public unsafe sealed class GameChatHooks : IDisposable
|
||||
{
|
||||
// Based on https://git.anna.lgbt/anna/ExtraChat/src/branch/main/client/ExtraChat/GameFunctions.cs
|
||||
|
||||
private readonly ILogger<GameChatHooks> _logger;
|
||||
private readonly Action<int, byte[]> _ssCommandHandler;
|
||||
|
||||
#region signatures
|
||||
#pragma warning disable CS0649
|
||||
// I do not know what kind of black magic this function performs
|
||||
// Client::UI::Misc::PronounModule::???
|
||||
[Signature("E8 ?? ?? ?? ?? 44 88 74 24 ?? 4C 8D 45")]
|
||||
private readonly delegate* unmanaged<PronounModule*, Utf8String*, byte, Utf8String*> _processStringStep2;
|
||||
|
||||
// Component::Shell::ShellCommandModule::ExecuteCommandInner
|
||||
private delegate void SendMessageDelegate(ShellCommandModule* module, Utf8String* message, UIModule* uiModule);
|
||||
[Signature(
|
||||
"E8 ?? ?? ?? ?? FE 87 ?? ?? ?? ?? C7 87",
|
||||
DetourName = nameof(SendMessageDetour)
|
||||
)]
|
||||
private Hook<SendMessageDelegate>? SendMessageHook { get; init; }
|
||||
|
||||
// Client::UI::Shell::RaptureShellModule::SetChatChannel
|
||||
private delegate void SetChatChannelDelegate(RaptureShellModule* module, uint channel);
|
||||
[Signature(
|
||||
"E8 ?? ?? ?? ?? 33 C0 EB ?? 85 D2",
|
||||
DetourName = nameof(SetChatChannelDetour)
|
||||
)]
|
||||
private Hook<SetChatChannelDelegate>? SetChatChannelHook { get; init; }
|
||||
|
||||
// Component::Shell::ShellCommandModule::ChangeChannelName
|
||||
private delegate byte* ChangeChannelNameDelegate(AgentChatLog* agent);
|
||||
[Signature(
|
||||
"E8 ?? ?? ?? ?? BA ?? ?? ?? ?? 48 8D 4D B0 48 8B F8 E8 ?? ?? ?? ?? 41 8B D6",
|
||||
DetourName = nameof(ChangeChannelNameDetour)
|
||||
)]
|
||||
private Hook<ChangeChannelNameDelegate>? ChangeChannelNameHook { get; init; }
|
||||
|
||||
// Client::UI::Agent::AgentChatLog::???
|
||||
private delegate byte ShouldDoNameLookupDelegate(AgentChatLog* agent);
|
||||
[Signature(
|
||||
"48 89 5C 24 ?? 57 48 83 EC ?? 48 8B D9 40 32 FF 48 8B 49 ?? ?? ?? ?? FF 50",
|
||||
DetourName = nameof(ShouldDoNameLookupDetour)
|
||||
)]
|
||||
private Hook<ShouldDoNameLookupDelegate>? ShouldDoNameLookupHook { get; init; }
|
||||
|
||||
// Temporary chat channel change (via hotkey)
|
||||
// Client::UI::Shell::RaptureShellModule::???
|
||||
private delegate ulong TempChatChannelDelegate(RaptureShellModule* module, uint x, uint y, ulong z);
|
||||
[Signature(
|
||||
"48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 83 B9 ?? ?? ?? ?? ?? 49 8B F9 41 8B F0",
|
||||
DetourName = nameof(TempChatChannelDetour)
|
||||
)]
|
||||
private Hook<TempChatChannelDelegate>? TempChatChannelHook { get; init; }
|
||||
|
||||
// Temporary tell target change (via hotkey)
|
||||
// Client::UI::Shell::RaptureShellModule::SetContextTellTargetInForay
|
||||
private delegate ulong TempTellTargetDelegate(RaptureShellModule* module, ulong a, ulong b, ulong c, ushort d, ulong e, ulong f, ushort g);
|
||||
[Signature(
|
||||
"48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 83 B9 ?? ?? ?? ?? ?? 41 0F B7 F9",
|
||||
DetourName = nameof(TempTellTargetDetour)
|
||||
)]
|
||||
private Hook<TempTellTargetDelegate>? TempTellTargetHook { get; init; }
|
||||
|
||||
// Called every frame while the chat bar is not focused
|
||||
private delegate void UnfocusTickDelegate(RaptureShellModule* module);
|
||||
[Signature(
|
||||
"40 53 48 83 EC ?? 83 B9 ?? ?? ?? ?? ?? 48 8B D9 0F 84 ?? ?? ?? ?? 48 8D 91",
|
||||
DetourName = nameof(UnfocusTickDetour)
|
||||
)]
|
||||
private Hook<UnfocusTickDelegate>? UnfocusTickHook { get; init; }
|
||||
#pragma warning restore CS0649
|
||||
#endregion
|
||||
|
||||
private ChatChannelOverride? _chatChannelOverride;
|
||||
private ChatChannelOverride? _chatChannelOverrideTempBuffer;
|
||||
private bool _shouldForceNameLookup = false;
|
||||
|
||||
private DateTime _nextMessageIsReply = DateTime.UnixEpoch;
|
||||
|
||||
public ChatChannelOverride? ChatChannelOverride
|
||||
{
|
||||
get => _chatChannelOverride;
|
||||
set {
|
||||
_chatChannelOverride = value;
|
||||
_shouldForceNameLookup = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void StashChatChannel()
|
||||
{
|
||||
if (_chatChannelOverride != null)
|
||||
{
|
||||
_logger.LogTrace("Stashing chat channel");
|
||||
_chatChannelOverrideTempBuffer = _chatChannelOverride;
|
||||
ChatChannelOverride = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnstashChatChannel()
|
||||
{
|
||||
if (_chatChannelOverrideTempBuffer != null)
|
||||
{
|
||||
_logger.LogTrace("Unstashing chat channel");
|
||||
ChatChannelOverride = _chatChannelOverrideTempBuffer;
|
||||
_chatChannelOverrideTempBuffer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public GameChatHooks(ILogger<GameChatHooks> logger, IGameInteropProvider gameInteropProvider, Action<int, byte[]> ssCommandHandler)
|
||||
{
|
||||
_logger = logger;
|
||||
_ssCommandHandler = ssCommandHandler;
|
||||
|
||||
logger.LogInformation("Initializing GameChatHooks");
|
||||
gameInteropProvider.InitializeFromAttributes(this);
|
||||
|
||||
SendMessageHook?.Enable();
|
||||
SetChatChannelHook?.Enable();
|
||||
ChangeChannelNameHook?.Enable();
|
||||
ShouldDoNameLookupHook?.Enable();
|
||||
TempChatChannelHook?.Enable();
|
||||
TempTellTargetHook?.Enable();
|
||||
UnfocusTickHook?.Enable();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SendMessageHook?.Dispose();
|
||||
SetChatChannelHook?.Dispose();
|
||||
ChangeChannelNameHook?.Dispose();
|
||||
ShouldDoNameLookupHook?.Dispose();
|
||||
TempChatChannelHook?.Dispose();
|
||||
TempTellTargetHook?.Dispose();
|
||||
UnfocusTickHook?.Dispose();
|
||||
}
|
||||
|
||||
private byte[] ProcessChatMessage(Utf8String* message)
|
||||
{
|
||||
var pronounModule = UIModule.Instance()->GetPronounModule();
|
||||
var chatString1 = pronounModule->ProcessString(message, true);
|
||||
var chatString2 = _processStringStep2(pronounModule, chatString1, 1);
|
||||
return MemoryHelper.ReadRaw((nint)chatString2->StringPtr.Value, chatString2->Length);
|
||||
}
|
||||
|
||||
private void SendMessageDetour(ShellCommandModule* thisPtr, Utf8String* message, UIModule* uiModule)
|
||||
{
|
||||
try
|
||||
{
|
||||
var messageLength = message->Length;
|
||||
var messageSpan = message->AsSpan();
|
||||
|
||||
bool isCommand = false;
|
||||
bool isReply = false;
|
||||
|
||||
var utcNow = DateTime.UtcNow;
|
||||
|
||||
// Check if chat input begins with a command (or auto-translated command)
|
||||
// Or if we think we're being called to send text via the /r command
|
||||
if (_nextMessageIsReply >= utcNow)
|
||||
{
|
||||
isCommand = true;
|
||||
}
|
||||
else if (messageLength == 0 || messageSpan[0] == (byte)'/' || !messageSpan.ContainsAnyExcept((byte)' '))
|
||||
{
|
||||
isCommand = true;
|
||||
if (messageSpan.StartsWith(System.Text.Encoding.ASCII.GetBytes("/r ")) || messageSpan.StartsWith(System.Text.Encoding.ASCII.GetBytes("/reply ")))
|
||||
isReply = true;
|
||||
}
|
||||
else if (messageSpan[0] == (byte)0x02) /* Payload.START_BYTE */
|
||||
{
|
||||
var payload = Payload.Decode(new BinaryReader(new UnmanagedMemoryStream(message->StringPtr, message->BufSize))) as AutoTranslatePayload;
|
||||
|
||||
// Auto-translate text begins with /
|
||||
if (payload != null && payload.Text.Length > 2 && payload.Text[2] == '/')
|
||||
{
|
||||
isCommand = true;
|
||||
if (payload.Text[2..].StartsWith("/r ", StringComparison.Ordinal) || payload.Text[2..].StartsWith("/reply ", StringComparison.Ordinal))
|
||||
isReply = true;
|
||||
}
|
||||
}
|
||||
|
||||
// When using /r the game will set a flag and then call this function a second time
|
||||
// The next call to this function will be raw text intended for the IM recipient
|
||||
// This flag's validity is time-limited as a fail-safe
|
||||
if (isReply)
|
||||
_nextMessageIsReply = utcNow + TimeSpan.FromMilliseconds(100);
|
||||
|
||||
// If it is a command, check if it begins with /ss first so we can handle the message directly
|
||||
// Letting Dalamud handle the commands causes all of the special payloads to be dropped
|
||||
if (isCommand && messageSpan.StartsWith(System.Text.Encoding.ASCII.GetBytes("/ss")))
|
||||
{
|
||||
for (int i = 1; i <= ChatService.CommandMaxNumber; ++i)
|
||||
{
|
||||
var cmdString = $"/ss{i} ";
|
||||
if (messageSpan.StartsWith(System.Text.Encoding.ASCII.GetBytes(cmdString)))
|
||||
{
|
||||
var ssChatBytes = ProcessChatMessage(message);
|
||||
ssChatBytes = ssChatBytes.Skip(cmdString.Length).ToArray();
|
||||
_ssCommandHandler?.Invoke(i, ssChatBytes);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If not a command, or no override is set, then call the original chat handler
|
||||
if (isCommand || _chatChannelOverride == null)
|
||||
{
|
||||
SendMessageHook!.OriginalDisposeSafe(thisPtr, message, uiModule);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, the text is to be sent to the emulated chat channel handler
|
||||
// The chat input string is rendered in to a payload for display first
|
||||
var chatBytes = ProcessChatMessage(message);
|
||||
|
||||
if (chatBytes.Length > 0)
|
||||
_chatChannelOverride.ChatMessageHandler?.Invoke(chatBytes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Exception thrown during SendMessageDetour");
|
||||
}
|
||||
}
|
||||
|
||||
private void SetChatChannelDetour(RaptureShellModule* module, uint channel)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_chatChannelOverride != null)
|
||||
{
|
||||
_chatChannelOverride = null;
|
||||
_shouldForceNameLookup = true;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Exception thrown during SetChatChannelDetour");
|
||||
}
|
||||
|
||||
SetChatChannelHook!.OriginalDisposeSafe(module, channel);
|
||||
}
|
||||
|
||||
private ulong TempChatChannelDetour(RaptureShellModule* module, uint x, uint y, ulong z)
|
||||
{
|
||||
var result = TempChatChannelHook!.OriginalDisposeSafe(module, x, y, z);
|
||||
|
||||
if (result != 0)
|
||||
StashChatChannel();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ulong TempTellTargetDetour(RaptureShellModule* module, ulong a, ulong b, ulong c, ushort d, ulong e, ulong f, ushort g)
|
||||
{
|
||||
var result = TempTellTargetHook!.OriginalDisposeSafe(module, a, b, c, d, e, f, g);
|
||||
|
||||
if (result != 0)
|
||||
StashChatChannel();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void UnfocusTickDetour(RaptureShellModule* module)
|
||||
{
|
||||
UnfocusTickHook!.OriginalDisposeSafe(module);
|
||||
UnstashChatChannel();
|
||||
}
|
||||
|
||||
private byte* ChangeChannelNameDetour(AgentChatLog* agent)
|
||||
{
|
||||
var originalResult = ChangeChannelNameHook!.OriginalDisposeSafe(agent);
|
||||
|
||||
try
|
||||
{
|
||||
// Replace the chat channel name on the UI if active
|
||||
if (_chatChannelOverride != null)
|
||||
{
|
||||
agent->ChannelLabel.SetString(_chatChannelOverride.ChannelName);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Exception thrown during ChangeChannelNameDetour");
|
||||
}
|
||||
|
||||
return originalResult;
|
||||
}
|
||||
|
||||
private byte ShouldDoNameLookupDetour(AgentChatLog* agent)
|
||||
{
|
||||
var originalResult = ShouldDoNameLookupHook!.OriginalDisposeSafe(agent);
|
||||
|
||||
try
|
||||
{
|
||||
// Force the chat channel name to update when required
|
||||
if (_shouldForceNameLookup)
|
||||
{
|
||||
_shouldForceNameLookup = false;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Exception thrown during ShouldDoNameLookupDetour");
|
||||
}
|
||||
|
||||
return originalResult;
|
||||
}
|
||||
}
|
259
MareSynchronos/Interop/GameModel/MdlFile.cs
Normal file
259
MareSynchronos/Interop/GameModel/MdlFile.cs
Normal file
@@ -0,0 +1,259 @@
|
||||
using Lumina.Data;
|
||||
using Lumina.Extensions;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using static Lumina.Data.Parsing.MdlStructs;
|
||||
|
||||
namespace MareSynchronos.Interop.GameModel;
|
||||
|
||||
#pragma warning disable S1104 // Fields should not have public accessibility
|
||||
|
||||
// This code is completely and shamelessly borrowed from Penumbra to load V5 and V6 model files.
|
||||
// Original Source: https://github.com/Ottermandias/Penumbra.GameData/blob/main/Files/MdlFile.cs
|
||||
public class MdlFile
|
||||
{
|
||||
public const int V5 = 0x01000005;
|
||||
public const int V6 = 0x01000006;
|
||||
public const uint NumVertices = 17;
|
||||
public const uint FileHeaderSize = 0x44;
|
||||
|
||||
// Raw data to write back.
|
||||
public uint Version = 0x01000005;
|
||||
public float Radius;
|
||||
public float ModelClipOutDistance;
|
||||
public float ShadowClipOutDistance;
|
||||
public byte BgChangeMaterialIndex;
|
||||
public byte BgCrestChangeMaterialIndex;
|
||||
public ushort CullingGridCount;
|
||||
public byte Flags3;
|
||||
public byte Unknown6;
|
||||
public ushort Unknown8;
|
||||
public ushort Unknown9;
|
||||
|
||||
// Offsets are stored relative to RuntimeSize instead of file start.
|
||||
public uint[] VertexOffset = [0, 0, 0];
|
||||
public uint[] IndexOffset = [0, 0, 0];
|
||||
|
||||
public uint[] VertexBufferSize = [0, 0, 0];
|
||||
public uint[] IndexBufferSize = [0, 0, 0];
|
||||
public byte LodCount;
|
||||
public bool EnableIndexBufferStreaming;
|
||||
public bool EnableEdgeGeometry;
|
||||
|
||||
public ModelFlags1 Flags1;
|
||||
public ModelFlags2 Flags2;
|
||||
|
||||
public VertexDeclarationStruct[] VertexDeclarations = [];
|
||||
public ElementIdStruct[] ElementIds = [];
|
||||
public MeshStruct[] Meshes = [];
|
||||
public BoundingBoxStruct[] BoneBoundingBoxes = [];
|
||||
public LodStruct[] Lods = [];
|
||||
public ExtraLodStruct[] ExtraLods = [];
|
||||
|
||||
public MdlFile(string filePath)
|
||||
{
|
||||
using var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||
using var r = new LuminaBinaryReader(stream);
|
||||
|
||||
var header = LoadModelFileHeader(r);
|
||||
LodCount = header.LodCount;
|
||||
VertexBufferSize = header.VertexBufferSize;
|
||||
IndexBufferSize = header.IndexBufferSize;
|
||||
VertexOffset = header.VertexOffset;
|
||||
IndexOffset = header.IndexOffset;
|
||||
|
||||
var dataOffset = FileHeaderSize + header.RuntimeSize + header.StackSize;
|
||||
for (var i = 0; i < LodCount; ++i)
|
||||
{
|
||||
VertexOffset[i] -= dataOffset;
|
||||
IndexOffset[i] -= dataOffset;
|
||||
}
|
||||
|
||||
VertexDeclarations = new VertexDeclarationStruct[header.VertexDeclarationCount];
|
||||
for (var i = 0; i < header.VertexDeclarationCount; ++i)
|
||||
VertexDeclarations[i] = VertexDeclarationStruct.Read(r);
|
||||
|
||||
_ = LoadStrings(r);
|
||||
|
||||
var modelHeader = LoadModelHeader(r);
|
||||
ElementIds = new ElementIdStruct[modelHeader.ElementIdCount];
|
||||
for (var i = 0; i < modelHeader.ElementIdCount; i++)
|
||||
ElementIds[i] = ElementIdStruct.Read(r);
|
||||
|
||||
Lods = new LodStruct[3];
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
var lod = r.ReadStructure<LodStruct>();
|
||||
if (i < LodCount)
|
||||
{
|
||||
lod.VertexDataOffset -= dataOffset;
|
||||
lod.IndexDataOffset -= dataOffset;
|
||||
}
|
||||
|
||||
Lods[i] = lod;
|
||||
}
|
||||
|
||||
ExtraLods = modelHeader.Flags2.HasFlag(ModelFlags2.ExtraLodEnabled)
|
||||
? r.ReadStructuresAsArray<ExtraLodStruct>(3)
|
||||
: [];
|
||||
|
||||
Meshes = new MeshStruct[modelHeader.MeshCount];
|
||||
for (var i = 0; i < modelHeader.MeshCount; i++)
|
||||
Meshes[i] = MeshStruct.Read(r);
|
||||
}
|
||||
|
||||
private ModelFileHeader LoadModelFileHeader(LuminaBinaryReader r)
|
||||
{
|
||||
var header = ModelFileHeader.Read(r);
|
||||
Version = header.Version;
|
||||
EnableIndexBufferStreaming = header.EnableIndexBufferStreaming;
|
||||
EnableEdgeGeometry = header.EnableEdgeGeometry;
|
||||
return header;
|
||||
}
|
||||
|
||||
private ModelHeader LoadModelHeader(BinaryReader r)
|
||||
{
|
||||
var modelHeader = r.ReadStructure<ModelHeader>();
|
||||
Radius = modelHeader.Radius;
|
||||
Flags1 = modelHeader.Flags1;
|
||||
Flags2 = modelHeader.Flags2;
|
||||
ModelClipOutDistance = modelHeader.ModelClipOutDistance;
|
||||
ShadowClipOutDistance = modelHeader.ShadowClipOutDistance;
|
||||
CullingGridCount = modelHeader.CullingGridCount;
|
||||
Flags3 = modelHeader.Flags3;
|
||||
Unknown6 = modelHeader.Unknown6;
|
||||
Unknown8 = modelHeader.Unknown8;
|
||||
Unknown9 = modelHeader.Unknown9;
|
||||
BgChangeMaterialIndex = modelHeader.BGChangeMaterialIndex;
|
||||
BgCrestChangeMaterialIndex = modelHeader.BGCrestChangeMaterialIndex;
|
||||
|
||||
return modelHeader;
|
||||
}
|
||||
|
||||
private static (uint[], string[]) LoadStrings(BinaryReader r)
|
||||
{
|
||||
var stringCount = r.ReadUInt16();
|
||||
r.ReadUInt16();
|
||||
var stringSize = (int)r.ReadUInt32();
|
||||
var stringData = r.ReadBytes(stringSize);
|
||||
var start = 0;
|
||||
var strings = new string[stringCount];
|
||||
var offsets = new uint[stringCount];
|
||||
for (var i = 0; i < stringCount; ++i)
|
||||
{
|
||||
var span = stringData.AsSpan(start);
|
||||
var idx = span.IndexOf((byte)'\0');
|
||||
strings[i] = Encoding.UTF8.GetString(span[..idx]);
|
||||
offsets[i] = (uint)start;
|
||||
start = start + idx + 1;
|
||||
}
|
||||
|
||||
return (offsets, strings);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public unsafe struct ModelHeader
|
||||
{
|
||||
// MeshHeader
|
||||
public float Radius;
|
||||
public ushort MeshCount;
|
||||
public ushort AttributeCount;
|
||||
public ushort SubmeshCount;
|
||||
public ushort MaterialCount;
|
||||
public ushort BoneCount;
|
||||
public ushort BoneTableCount;
|
||||
public ushort ShapeCount;
|
||||
public ushort ShapeMeshCount;
|
||||
public ushort ShapeValueCount;
|
||||
public byte LodCount;
|
||||
public ModelFlags1 Flags1;
|
||||
public ushort ElementIdCount;
|
||||
public byte TerrainShadowMeshCount;
|
||||
public ModelFlags2 Flags2;
|
||||
public float ModelClipOutDistance;
|
||||
public float ShadowClipOutDistance;
|
||||
public ushort CullingGridCount;
|
||||
public ushort TerrainShadowSubmeshCount;
|
||||
public byte Flags3;
|
||||
public byte BGChangeMaterialIndex;
|
||||
public byte BGCrestChangeMaterialIndex;
|
||||
public byte Unknown6;
|
||||
public ushort BoneTableArrayCountTotal;
|
||||
public ushort Unknown8;
|
||||
public ushort Unknown9;
|
||||
private fixed byte _padding[6];
|
||||
}
|
||||
|
||||
public struct ShapeStruct
|
||||
{
|
||||
public uint StringOffset;
|
||||
public ushort[] ShapeMeshStartIndex;
|
||||
public ushort[] ShapeMeshCount;
|
||||
|
||||
public static ShapeStruct Read(LuminaBinaryReader br)
|
||||
{
|
||||
ShapeStruct ret = new ShapeStruct();
|
||||
ret.StringOffset = br.ReadUInt32();
|
||||
ret.ShapeMeshStartIndex = br.ReadUInt16Array(3);
|
||||
ret.ShapeMeshCount = br.ReadUInt16Array(3);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum ModelFlags1 : byte
|
||||
{
|
||||
DustOcclusionEnabled = 0x80,
|
||||
SnowOcclusionEnabled = 0x40,
|
||||
RainOcclusionEnabled = 0x20,
|
||||
Unknown1 = 0x10,
|
||||
LightingReflectionEnabled = 0x08,
|
||||
WavingAnimationDisabled = 0x04,
|
||||
LightShadowDisabled = 0x02,
|
||||
ShadowDisabled = 0x01,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum ModelFlags2 : byte
|
||||
{
|
||||
Unknown2 = 0x80,
|
||||
BgUvScrollEnabled = 0x40,
|
||||
EnableForceNonResident = 0x20,
|
||||
ExtraLodEnabled = 0x10,
|
||||
ShadowMaskEnabled = 0x08,
|
||||
ForceLodRangeEnabled = 0x04,
|
||||
EdgeGeometryEnabled = 0x02,
|
||||
Unknown3 = 0x01
|
||||
}
|
||||
|
||||
public struct VertexDeclarationStruct
|
||||
{
|
||||
// There are always 17, but stop when stream = -1
|
||||
public VertexElement[] VertexElements;
|
||||
|
||||
public static VertexDeclarationStruct Read(LuminaBinaryReader br)
|
||||
{
|
||||
VertexDeclarationStruct ret = new VertexDeclarationStruct();
|
||||
|
||||
var elems = new List<VertexElement>();
|
||||
|
||||
// Read the vertex elements that we need
|
||||
var thisElem = br.ReadStructure<VertexElement>();
|
||||
do
|
||||
{
|
||||
elems.Add(thisElem);
|
||||
thisElem = br.ReadStructure<VertexElement>();
|
||||
} while (thisElem.Stream != 255);
|
||||
|
||||
// Skip the number of bytes that we don't need to read
|
||||
// We skip elems.Count * 9 because we had to read the invalid element
|
||||
int toSeek = 17 * 8 - (elems.Count + 1) * 8;
|
||||
br.Seek(br.BaseStream.Position + toSeek);
|
||||
|
||||
ret.VertexElements = elems.ToArray();
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning restore S1104 // Fields should not have public accessibility
|
7
MareSynchronos/Interop/Ipc/IIpcCaller.cs
Normal file
7
MareSynchronos/Interop/Ipc/IIpcCaller.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace MareSynchronos.Interop.Ipc;
|
||||
|
||||
public interface IIpcCaller : IDisposable
|
||||
{
|
||||
bool APIAvailable { get; }
|
||||
void CheckAPI();
|
||||
}
|
147
MareSynchronos/Interop/Ipc/IpcCallerBrio.cs
Normal file
147
MareSynchronos/Interop/Ipc/IpcCallerBrio.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using MareSynchronos.API.Dto.CharaData;
|
||||
using MareSynchronos.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace MareSynchronos.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerBrio : IIpcCaller
|
||||
{
|
||||
private readonly ILogger<IpcCallerBrio> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtilService;
|
||||
private readonly ICallGateSubscriber<(int, int)> _brioApiVersion;
|
||||
|
||||
private readonly ICallGateSubscriber<bool, bool, bool, Task<IGameObject>> _brioSpawnActorAsync;
|
||||
private readonly ICallGateSubscriber<IGameObject, bool> _brioDespawnActor;
|
||||
private readonly ICallGateSubscriber<IGameObject, Vector3?, Quaternion?, Vector3?, bool, bool> _brioSetModelTransform;
|
||||
private readonly ICallGateSubscriber<IGameObject, (Vector3?, Quaternion?, Vector3?)> _brioGetModelTransform;
|
||||
private readonly ICallGateSubscriber<IGameObject, string> _brioGetPoseAsJson;
|
||||
private readonly ICallGateSubscriber<IGameObject, string, bool, bool> _brioSetPoseFromJson;
|
||||
private readonly ICallGateSubscriber<IGameObject, bool> _brioFreezeActor;
|
||||
private readonly ICallGateSubscriber<bool> _brioFreezePhysics;
|
||||
|
||||
|
||||
public bool APIAvailable { get; private set; }
|
||||
|
||||
public IpcCallerBrio(ILogger<IpcCallerBrio> logger, IDalamudPluginInterface dalamudPluginInterface,
|
||||
DalamudUtilService dalamudUtilService)
|
||||
{
|
||||
_logger = logger;
|
||||
_dalamudUtilService = dalamudUtilService;
|
||||
|
||||
_brioApiVersion = dalamudPluginInterface.GetIpcSubscriber<(int, int)>("Brio.ApiVersion");
|
||||
_brioSpawnActorAsync = dalamudPluginInterface.GetIpcSubscriber<bool, bool, bool, Task<IGameObject>>("Brio.Actor.SpawnExAsync");
|
||||
_brioDespawnActor = dalamudPluginInterface.GetIpcSubscriber<IGameObject, bool>("Brio.Actor.Despawn");
|
||||
_brioSetModelTransform = dalamudPluginInterface.GetIpcSubscriber<IGameObject, Vector3?, Quaternion?, Vector3?, bool, bool>("Brio.Actor.SetModelTransform");
|
||||
_brioGetModelTransform = dalamudPluginInterface.GetIpcSubscriber<IGameObject, (Vector3?, Quaternion?, Vector3?)>("Brio.Actor.GetModelTransform");
|
||||
_brioGetPoseAsJson = dalamudPluginInterface.GetIpcSubscriber<IGameObject, string>("Brio.Actor.Pose.GetPoseAsJson");
|
||||
_brioSetPoseFromJson = dalamudPluginInterface.GetIpcSubscriber<IGameObject, string, bool, bool>("Brio.Actor.Pose.LoadFromJson");
|
||||
_brioFreezeActor = dalamudPluginInterface.GetIpcSubscriber<IGameObject, bool>("Brio.Actor.Freeze");
|
||||
_brioFreezePhysics = dalamudPluginInterface.GetIpcSubscriber<bool>("Brio.FreezePhysics");
|
||||
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
try
|
||||
{
|
||||
var version = _brioApiVersion.InvokeFunc();
|
||||
APIAvailable = (version.Item1 == 2 && version.Item2 >= 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IGameObject?> SpawnActorAsync()
|
||||
{
|
||||
if (!APIAvailable) return null;
|
||||
_logger.LogDebug("Spawning Brio Actor");
|
||||
return await _brioSpawnActorAsync.InvokeFunc(false, false, true).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> DespawnActorAsync(nint address)
|
||||
{
|
||||
if (!APIAvailable) return false;
|
||||
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
|
||||
if (gameObject == null) return false;
|
||||
_logger.LogDebug("Despawning Brio Actor {actor}", gameObject.Name.TextValue);
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioDespawnActor.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> ApplyTransformAsync(nint address, WorldData data)
|
||||
{
|
||||
if (!APIAvailable) return false;
|
||||
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
|
||||
if (gameObject == null) return false;
|
||||
_logger.LogDebug("Applying Transform to Actor {actor}", gameObject.Name.TextValue);
|
||||
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetModelTransform.InvokeFunc(gameObject,
|
||||
new Vector3(data.PositionX, data.PositionY, data.PositionZ),
|
||||
new Quaternion(data.RotationX, data.RotationY, data.RotationZ, data.RotationW),
|
||||
new Vector3(data.ScaleX, data.ScaleY, data.ScaleZ), false)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<WorldData> GetTransformAsync(nint address)
|
||||
{
|
||||
if (!APIAvailable) return default;
|
||||
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
|
||||
if (gameObject == null) return default;
|
||||
var data = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetModelTransform.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||
if (data.Item1 == null || data.Item2 == null || data.Item3 == null) return default;
|
||||
//_logger.LogDebug("Getting Transform from Actor {actor}", gameObject.Name.TextValue);
|
||||
|
||||
return new WorldData()
|
||||
{
|
||||
PositionX = data.Item1.Value.X,
|
||||
PositionY = data.Item1.Value.Y,
|
||||
PositionZ = data.Item1.Value.Z,
|
||||
RotationX = data.Item2.Value.X,
|
||||
RotationY = data.Item2.Value.Y,
|
||||
RotationZ = data.Item2.Value.Z,
|
||||
RotationW = data.Item2.Value.W,
|
||||
ScaleX = data.Item3.Value.X,
|
||||
ScaleY = data.Item3.Value.Y,
|
||||
ScaleZ = data.Item3.Value.Z
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<string?> GetPoseAsync(nint address)
|
||||
{
|
||||
if (!APIAvailable) return null;
|
||||
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
|
||||
if (gameObject == null) return null;
|
||||
_logger.LogDebug("Getting Pose from Actor {actor}", gameObject.Name.TextValue);
|
||||
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetPoseAsJson.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<bool> SetPoseAsync(nint address, string pose)
|
||||
{
|
||||
if (!APIAvailable) return false;
|
||||
var gameObject = await _dalamudUtilService.CreateGameObjectAsync(address).ConfigureAwait(false);
|
||||
if (gameObject == null) return false;
|
||||
_logger.LogDebug("Setting Pose to Actor {actor}", gameObject.Name.TextValue);
|
||||
|
||||
var applicablePose = JsonNode.Parse(pose)!;
|
||||
var currentPose = await _dalamudUtilService.RunOnFrameworkThread(() => _brioGetPoseAsJson.InvokeFunc(gameObject)).ConfigureAwait(false);
|
||||
applicablePose["ModelDifference"] = JsonNode.Parse(JsonNode.Parse(currentPose)!["ModelDifference"]!.ToJsonString());
|
||||
|
||||
await _dalamudUtilService.RunOnFrameworkThread(() =>
|
||||
{
|
||||
_brioFreezeActor.InvokeFunc(gameObject);
|
||||
_brioFreezePhysics.InvokeFunc();
|
||||
}).ConfigureAwait(false);
|
||||
return await _dalamudUtilService.RunOnFrameworkThread(() => _brioSetPoseFromJson.InvokeFunc(gameObject, applicablePose.ToJsonString(), false)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
139
MareSynchronos/Interop/Ipc/IpcCallerCustomize.cs
Normal file
139
MareSynchronos/Interop/Ipc/IpcCallerCustomize.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using Dalamud.Utility;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronos.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerCustomize : IIpcCaller
|
||||
{
|
||||
private readonly ICallGateSubscriber<(int, int)> _customizePlusApiVersion;
|
||||
private readonly ICallGateSubscriber<ushort, (int, Guid?)> _customizePlusGetActiveProfile;
|
||||
private readonly ICallGateSubscriber<Guid, (int, string?)> _customizePlusGetProfileById;
|
||||
private readonly ICallGateSubscriber<ushort, Guid, object> _customizePlusOnScaleUpdate;
|
||||
private readonly ICallGateSubscriber<ushort, int> _customizePlusRevertCharacter;
|
||||
private readonly ICallGateSubscriber<ushort, string, (int, Guid?)> _customizePlusSetBodyScaleToCharacter;
|
||||
private readonly ICallGateSubscriber<Guid, int> _customizePlusDeleteByUniqueId;
|
||||
private readonly ILogger<IpcCallerCustomize> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly MareMediator _mareMediator;
|
||||
|
||||
public IpcCallerCustomize(ILogger<IpcCallerCustomize> logger, IDalamudPluginInterface dalamudPluginInterface,
|
||||
DalamudUtilService dalamudUtil, MareMediator mareMediator)
|
||||
{
|
||||
_customizePlusApiVersion = dalamudPluginInterface.GetIpcSubscriber<(int, int)>("CustomizePlus.General.GetApiVersion");
|
||||
_customizePlusGetActiveProfile = dalamudPluginInterface.GetIpcSubscriber<ushort, (int, Guid?)>("CustomizePlus.Profile.GetActiveProfileIdOnCharacter");
|
||||
_customizePlusGetProfileById = dalamudPluginInterface.GetIpcSubscriber<Guid, (int, string?)>("CustomizePlus.Profile.GetByUniqueId");
|
||||
_customizePlusRevertCharacter = dalamudPluginInterface.GetIpcSubscriber<ushort, int>("CustomizePlus.Profile.DeleteTemporaryProfileOnCharacter");
|
||||
_customizePlusSetBodyScaleToCharacter = dalamudPluginInterface.GetIpcSubscriber<ushort, string, (int, Guid?)>("CustomizePlus.Profile.SetTemporaryProfileOnCharacter");
|
||||
_customizePlusOnScaleUpdate = dalamudPluginInterface.GetIpcSubscriber<ushort, Guid, object>("CustomizePlus.Profile.OnUpdate");
|
||||
_customizePlusDeleteByUniqueId = dalamudPluginInterface.GetIpcSubscriber<Guid, int>("CustomizePlus.Profile.DeleteTemporaryProfileByUniqueId");
|
||||
|
||||
_customizePlusOnScaleUpdate.Subscribe(OnCustomizePlusScaleChange);
|
||||
_logger = logger;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_mareMediator = mareMediator;
|
||||
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
public async Task RevertAsync(nint character)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||
if (gameObj is ICharacter c)
|
||||
{
|
||||
_logger.LogTrace("CustomizePlus reverting for {chara}", c.Address.ToString("X"));
|
||||
_customizePlusRevertCharacter!.InvokeFunc(c.ObjectIndex);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Guid?> SetBodyScaleAsync(nint character, string scale)
|
||||
{
|
||||
if (!APIAvailable) return null;
|
||||
return await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||
if (gameObj is ICharacter c)
|
||||
{
|
||||
string decodedScale = Encoding.UTF8.GetString(Convert.FromBase64String(scale));
|
||||
_logger.LogTrace("CustomizePlus applying for {chara}", c.Address.ToString("X"));
|
||||
if (scale.IsNullOrEmpty())
|
||||
{
|
||||
_customizePlusRevertCharacter!.InvokeFunc(c.ObjectIndex);
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = _customizePlusSetBodyScaleToCharacter!.InvokeFunc(c.ObjectIndex, decodedScale);
|
||||
return result.Item2;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task RevertByIdAsync(Guid? profileId)
|
||||
{
|
||||
if (!APIAvailable || profileId == null) return;
|
||||
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
_ = _customizePlusDeleteByUniqueId.InvokeFunc(profileId.Value);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<string?> GetScaleAsync(nint character)
|
||||
{
|
||||
if (!APIAvailable) return null;
|
||||
var scale = await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||
if (gameObj is ICharacter c)
|
||||
{
|
||||
var res = _customizePlusGetActiveProfile.InvokeFunc(c.ObjectIndex);
|
||||
_logger.LogTrace("CustomizePlus GetActiveProfile returned {err}", res.Item1);
|
||||
if (res.Item1 != 0 || res.Item2 == null) return string.Empty;
|
||||
return _customizePlusGetProfileById.InvokeFunc(res.Item2.Value).Item2;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}).ConfigureAwait(false);
|
||||
if (string.IsNullOrEmpty(scale)) return string.Empty;
|
||||
return Convert.ToBase64String(Encoding.UTF8.GetBytes(scale));
|
||||
}
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
try
|
||||
{
|
||||
var version = _customizePlusApiVersion.InvokeFunc();
|
||||
APIAvailable = (version.Item1 == 6 && version.Item2 >= 0);
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCustomizePlusScaleChange(ushort c, Guid g)
|
||||
{
|
||||
var obj = _dalamudUtil.GetCharacterFromObjectTableByIndex(c);
|
||||
_mareMediator.Publish(new CustomizePlusMessage(obj?.Address ?? null));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_customizePlusOnScaleUpdate.Unsubscribe(OnCustomizePlusScaleChange);
|
||||
}
|
||||
}
|
253
MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs
Normal file
253
MareSynchronos/Interop/Ipc/IpcCallerGlamourer.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin;
|
||||
using Glamourer.Api.Helpers;
|
||||
using Glamourer.Api.IpcSubscribers;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.PlayerData.Handlers;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerGlamourer : DisposableMediatorSubscriberBase, IIpcCaller
|
||||
{
|
||||
private readonly ILogger<IpcCallerGlamourer> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly MareMediator _mareMediator;
|
||||
private readonly RedrawManager _redrawManager;
|
||||
|
||||
private readonly ApiVersion _glamourerApiVersions;
|
||||
private readonly ApplyState? _glamourerApplyAll;
|
||||
private readonly GetStateBase64? _glamourerGetAllCustomization;
|
||||
private readonly RevertState _glamourerRevert;
|
||||
private readonly RevertStateName _glamourerRevertByName;
|
||||
private readonly UnlockState _glamourerUnlock;
|
||||
private readonly UnlockStateName _glamourerUnlockByName;
|
||||
private readonly EventSubscriber<nint>? _glamourerStateChanged;
|
||||
|
||||
private bool _pluginLoaded;
|
||||
private Version _pluginVersion;
|
||||
|
||||
private bool _shownGlamourerUnavailable = false;
|
||||
private readonly uint LockCode = 0x626E7579;
|
||||
|
||||
public IpcCallerGlamourer(ILogger<IpcCallerGlamourer> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, MareMediator mareMediator,
|
||||
RedrawManager redrawManager) : base(logger, mareMediator)
|
||||
{
|
||||
_glamourerApiVersions = new ApiVersion(pi);
|
||||
_glamourerGetAllCustomization = new GetStateBase64(pi);
|
||||
_glamourerApplyAll = new ApplyState(pi);
|
||||
_glamourerRevert = new RevertState(pi);
|
||||
_glamourerRevertByName = new RevertStateName(pi);
|
||||
_glamourerUnlock = new UnlockState(pi);
|
||||
_glamourerUnlockByName = new UnlockStateName(pi);
|
||||
|
||||
_logger = logger;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_mareMediator = mareMediator;
|
||||
_redrawManager = redrawManager;
|
||||
|
||||
var plugin = PluginWatcherService.GetInitialPluginState(pi, "Glamourer");
|
||||
|
||||
_pluginLoaded = plugin?.IsLoaded ?? false;
|
||||
_pluginVersion = plugin?.Version ?? new(0, 0, 0, 0);
|
||||
|
||||
Mediator.SubscribeKeyed<PluginChangeMessage>(this, "Glamourer", (msg) =>
|
||||
{
|
||||
_pluginLoaded = msg.IsLoaded;
|
||||
_pluginVersion = msg.Version;
|
||||
CheckAPI();
|
||||
});
|
||||
|
||||
CheckAPI();
|
||||
|
||||
_glamourerStateChanged = StateChanged.Subscriber(pi, GlamourerChanged);
|
||||
_glamourerStateChanged.Enable();
|
||||
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, s => _shownGlamourerUnavailable = false);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
_redrawManager.Cancel();
|
||||
_glamourerStateChanged?.Dispose();
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; }
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
bool apiAvailable = false;
|
||||
try
|
||||
{
|
||||
bool versionValid = _pluginLoaded && _pluginVersion >= new Version(1, 0, 6, 1);
|
||||
try
|
||||
{
|
||||
var version = _glamourerApiVersions.Invoke();
|
||||
if (version is { Major: 1, Minor: >= 1 } && versionValid)
|
||||
{
|
||||
apiAvailable = true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
_shownGlamourerUnavailable = _shownGlamourerUnavailable && !apiAvailable;
|
||||
|
||||
APIAvailable = apiAvailable;
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = apiAvailable;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!apiAvailable && !_shownGlamourerUnavailable)
|
||||
{
|
||||
_shownGlamourerUnavailable = true;
|
||||
_mareMediator.Publish(new NotificationMessage("Glamourer inactive", "Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Elezen. If you just updated Glamourer, ignore this message.",
|
||||
NotificationType.Error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ApplyAllAsync(ILogger logger, GameObjectHandler handler, string? customization, Guid applicationId, CancellationToken token, bool allowImmediate = false)
|
||||
{
|
||||
if (!APIAvailable || string.IsNullOrEmpty(customization) || _dalamudUtil.IsZoning) return;
|
||||
|
||||
// Call immediately if possible
|
||||
if (allowImmediate && _dalamudUtil.IsOnFrameworkThread && !await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false))
|
||||
{
|
||||
var gameObj = await _dalamudUtil.CreateGameObjectAsync(handler.Address).ConfigureAwait(false);
|
||||
if (gameObj is ICharacter chara)
|
||||
{
|
||||
logger.LogDebug("[{appid}] Calling on IPC: GlamourerApplyAll", applicationId);
|
||||
_glamourerApplyAll!.Invoke(customization, chara.ObjectIndex, LockCode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogDebug("[{appid}] Calling on IPC: GlamourerApplyAll", applicationId);
|
||||
_glamourerApplyAll!.Invoke(customization, chara.ObjectIndex, LockCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "[{appid}] Failed to apply Glamourer data", applicationId);
|
||||
}
|
||||
}, token).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_redrawManager.RedrawSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetCharacterCustomizationAsync(IntPtr character)
|
||||
{
|
||||
if (!APIAvailable) return string.Empty;
|
||||
try
|
||||
{
|
||||
return await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||
if (gameObj is ICharacter c)
|
||||
{
|
||||
return _glamourerGetAllCustomization!.Invoke(c.ObjectIndex).Item2 ?? string.Empty;
|
||||
}
|
||||
return string.Empty;
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RevertAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
||||
{
|
||||
if ((!APIAvailable) || _dalamudUtil.IsZoning) return;
|
||||
try
|
||||
{
|
||||
await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
logger.LogDebug("[{appid}] Calling On IPC: GlamourerUnlock", applicationId);
|
||||
_glamourerUnlock.Invoke(chara.ObjectIndex, LockCode);
|
||||
logger.LogDebug("[{appid}] Calling On IPC: GlamourerRevert", applicationId);
|
||||
_glamourerRevert.Invoke(chara.ObjectIndex, LockCode);
|
||||
logger.LogDebug("[{appid}] Calling On IPC: PenumbraRedraw", applicationId);
|
||||
_mareMediator.Publish(new PenumbraRedrawCharacterMessage(chara));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "[{appid}] Error during GlamourerRevert", applicationId);
|
||||
}
|
||||
}, token).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_redrawManager.RedrawSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void RevertNow(ILogger logger, Guid applicationId, int objectIndex)
|
||||
{
|
||||
if ((!APIAvailable) || _dalamudUtil.IsZoning) return;
|
||||
logger.LogTrace("[{applicationId}] Immediately reverting object index {objId}", applicationId, objectIndex);
|
||||
_glamourerRevert.Invoke(objectIndex, LockCode);
|
||||
}
|
||||
|
||||
public void RevertByNameNow(ILogger logger, Guid applicationId, string name)
|
||||
{
|
||||
if ((!APIAvailable) || _dalamudUtil.IsZoning) return;
|
||||
logger.LogTrace("[{applicationId}] Immediately reverting {name}", applicationId, name);
|
||||
_glamourerRevertByName.Invoke(name, LockCode);
|
||||
}
|
||||
|
||||
public async Task RevertByNameAsync(ILogger logger, string name, Guid applicationId)
|
||||
{
|
||||
if ((!APIAvailable) || _dalamudUtil.IsZoning) return;
|
||||
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
RevertByName(logger, name, applicationId);
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void RevertByName(ILogger logger, string name, Guid applicationId)
|
||||
{
|
||||
if ((!APIAvailable) || _dalamudUtil.IsZoning) return;
|
||||
|
||||
try
|
||||
{
|
||||
logger.LogDebug("[{appid}] Calling On IPC: GlamourerRevertByName", applicationId);
|
||||
_glamourerRevertByName.Invoke(name, LockCode);
|
||||
logger.LogDebug("[{appid}] Calling On IPC: GlamourerUnlockName", applicationId);
|
||||
_glamourerUnlockByName.Invoke(name, LockCode);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error during Glamourer RevertByName");
|
||||
}
|
||||
}
|
||||
|
||||
private void GlamourerChanged(nint address)
|
||||
{
|
||||
_mareMediator.Publish(new GlamourerChangedMessage(address));
|
||||
}
|
||||
}
|
93
MareSynchronos/Interop/Ipc/IpcCallerHeels.cs
Normal file
93
MareSynchronos/Interop/Ipc/IpcCallerHeels.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerHeels : IIpcCaller
|
||||
{
|
||||
private readonly ILogger<IpcCallerHeels> _logger;
|
||||
private readonly MareMediator _mareMediator;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly ICallGateSubscriber<(int, int)> _heelsGetApiVersion;
|
||||
private readonly ICallGateSubscriber<string> _heelsGetOffset;
|
||||
private readonly ICallGateSubscriber<string, object?> _heelsOffsetUpdate;
|
||||
private readonly ICallGateSubscriber<int, string, object?> _heelsRegisterPlayer;
|
||||
private readonly ICallGateSubscriber<int, object?> _heelsUnregisterPlayer;
|
||||
|
||||
public IpcCallerHeels(ILogger<IpcCallerHeels> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil, MareMediator mareMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_mareMediator = mareMediator;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_heelsGetApiVersion = pi.GetIpcSubscriber<(int, int)>("SimpleHeels.ApiVersion");
|
||||
_heelsGetOffset = pi.GetIpcSubscriber<string>("SimpleHeels.GetLocalPlayer");
|
||||
_heelsRegisterPlayer = pi.GetIpcSubscriber<int, string, object?>("SimpleHeels.RegisterPlayer");
|
||||
_heelsUnregisterPlayer = pi.GetIpcSubscriber<int, object?>("SimpleHeels.UnregisterPlayer");
|
||||
_heelsOffsetUpdate = pi.GetIpcSubscriber<string, object?>("SimpleHeels.LocalChanged");
|
||||
|
||||
_heelsOffsetUpdate.Subscribe(HeelsOffsetChange);
|
||||
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
private void HeelsOffsetChange(string offset)
|
||||
{
|
||||
_mareMediator.Publish(new HeelsOffsetMessage());
|
||||
}
|
||||
|
||||
public async Task<string> GetOffsetAsync()
|
||||
{
|
||||
if (!APIAvailable) return string.Empty;
|
||||
return await _dalamudUtil.RunOnFrameworkThread(_heelsGetOffset.InvokeFunc).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task RestoreOffsetForPlayerAsync(IntPtr character)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||
if (gameObj != null)
|
||||
{
|
||||
_logger.LogTrace("Restoring Heels data to {chara}", character.ToString("X"));
|
||||
_heelsUnregisterPlayer.InvokeAction(gameObj.ObjectIndex);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SetOffsetForPlayerAsync(IntPtr character, string data)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||
if (gameObj != null)
|
||||
{
|
||||
_logger.LogTrace("Applying Heels data to {chara}", character.ToString("X"));
|
||||
_heelsRegisterPlayer.InvokeAction(gameObj.ObjectIndex, data);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
try
|
||||
{
|
||||
APIAvailable = _heelsGetApiVersion.InvokeFunc() is { Item1: 2, Item2: >= 0 };
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange);
|
||||
}
|
||||
}
|
135
MareSynchronos/Interop/Ipc/IpcCallerHonorific.cs
Normal file
135
MareSynchronos/Interop/Ipc/IpcCallerHonorific.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronos.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerHonorific : IIpcCaller
|
||||
{
|
||||
private readonly ICallGateSubscriber<(uint major, uint minor)> _honorificApiVersion;
|
||||
private readonly ICallGateSubscriber<int, object> _honorificClearCharacterTitle;
|
||||
private readonly ICallGateSubscriber<object> _honorificDisposing;
|
||||
private readonly ICallGateSubscriber<string> _honorificGetLocalCharacterTitle;
|
||||
private readonly ICallGateSubscriber<string, object> _honorificLocalCharacterTitleChanged;
|
||||
private readonly ICallGateSubscriber<object> _honorificReady;
|
||||
private readonly ICallGateSubscriber<int, string, object> _honorificSetCharacterTitle;
|
||||
private readonly ILogger<IpcCallerHonorific> _logger;
|
||||
private readonly MareMediator _mareMediator;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
|
||||
public IpcCallerHonorific(ILogger<IpcCallerHonorific> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||
MareMediator mareMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_mareMediator = mareMediator;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_honorificApiVersion = pi.GetIpcSubscriber<(uint, uint)>("Honorific.ApiVersion");
|
||||
_honorificGetLocalCharacterTitle = pi.GetIpcSubscriber<string>("Honorific.GetLocalCharacterTitle");
|
||||
_honorificClearCharacterTitle = pi.GetIpcSubscriber<int, object>("Honorific.ClearCharacterTitle");
|
||||
_honorificSetCharacterTitle = pi.GetIpcSubscriber<int, string, object>("Honorific.SetCharacterTitle");
|
||||
_honorificLocalCharacterTitleChanged = pi.GetIpcSubscriber<string, object>("Honorific.LocalCharacterTitleChanged");
|
||||
_honorificDisposing = pi.GetIpcSubscriber<object>("Honorific.Disposing");
|
||||
_honorificReady = pi.GetIpcSubscriber<object>("Honorific.Ready");
|
||||
|
||||
_honorificLocalCharacterTitleChanged.Subscribe(OnHonorificLocalCharacterTitleChanged);
|
||||
_honorificDisposing.Subscribe(OnHonorificDisposing);
|
||||
_honorificReady.Subscribe(OnHonorificReady);
|
||||
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
try
|
||||
{
|
||||
APIAvailable = _honorificApiVersion.InvokeFunc() is { Item1: 3, Item2: >= 0 };
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_honorificLocalCharacterTitleChanged.Unsubscribe(OnHonorificLocalCharacterTitleChanged);
|
||||
_honorificDisposing.Unsubscribe(OnHonorificDisposing);
|
||||
_honorificReady.Unsubscribe(OnHonorificReady);
|
||||
}
|
||||
|
||||
public async Task ClearTitleAsync(nint character)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||
if (gameObj is IPlayerCharacter c)
|
||||
{
|
||||
_logger.LogTrace("Honorific removing for {addr}", c.Address.ToString("X"));
|
||||
_honorificClearCharacterTitle!.InvokeAction(c.ObjectIndex);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<string> GetTitle()
|
||||
{
|
||||
if (!APIAvailable) return string.Empty;
|
||||
return await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
string title = _honorificGetLocalCharacterTitle.InvokeFunc();
|
||||
return string.IsNullOrEmpty(title) ? string.Empty : Convert.ToBase64String(Encoding.UTF8.GetBytes(title));
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SetTitleAsync(IntPtr character, string honorificDataB64)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
_logger.LogTrace("Applying Honorific data to {chara}", character.ToString("X"));
|
||||
try
|
||||
{
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||
if (gameObj is IPlayerCharacter pc)
|
||||
{
|
||||
string honorificData = string.IsNullOrEmpty(honorificDataB64) ? string.Empty : Encoding.UTF8.GetString(Convert.FromBase64String(honorificDataB64));
|
||||
if (string.IsNullOrEmpty(honorificData))
|
||||
{
|
||||
_honorificClearCharacterTitle!.InvokeAction(pc.ObjectIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
_honorificSetCharacterTitle!.InvokeAction(pc.ObjectIndex, honorificData);
|
||||
}
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Could not apply Honorific data");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHonorificDisposing()
|
||||
{
|
||||
_mareMediator.Publish(new HonorificMessage(string.Empty));
|
||||
}
|
||||
|
||||
private void OnHonorificLocalCharacterTitleChanged(string titleJson)
|
||||
{
|
||||
string titleData = string.IsNullOrEmpty(titleJson) ? string.Empty : Convert.ToBase64String(Encoding.UTF8.GetBytes(titleJson));
|
||||
_mareMediator.Publish(new HonorificMessage(titleData));
|
||||
}
|
||||
|
||||
private void OnHonorificReady()
|
||||
{
|
||||
CheckAPI();
|
||||
_mareMediator.Publish(new HonorificReadyMessage());
|
||||
}
|
||||
}
|
44
MareSynchronos/Interop/Ipc/IpcCallerMare.cs
Normal file
44
MareSynchronos/Interop/Ipc/IpcCallerMare.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerMare : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private readonly ICallGateSubscriber<List<nint>> _mareHandledGameAddresses;
|
||||
private readonly List<nint> _emptyList = [];
|
||||
|
||||
private bool _pluginLoaded;
|
||||
|
||||
public IpcCallerMare(ILogger<IpcCallerMare> logger, IDalamudPluginInterface pi, MareMediator mediator) : base(logger, mediator)
|
||||
{
|
||||
_mareHandledGameAddresses = pi.GetIpcSubscriber<List<nint>>("MareSynchronos.GetHandledAddresses");
|
||||
|
||||
_pluginLoaded = PluginWatcherService.GetInitialPluginState(pi, "MareSynchronos")?.IsLoaded ?? false;
|
||||
|
||||
Mediator.SubscribeKeyed<PluginChangeMessage>(this, "MareSynchronos", (msg) =>
|
||||
{
|
||||
_pluginLoaded = msg.IsLoaded;
|
||||
});
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
// Must be called on framework thread
|
||||
public IReadOnlyList<nint> GetHandledGameAddresses()
|
||||
{
|
||||
if (!_pluginLoaded) return _emptyList;
|
||||
|
||||
try
|
||||
{
|
||||
return _mareHandledGameAddresses.InvokeFunc();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return _emptyList;
|
||||
}
|
||||
}
|
||||
}
|
104
MareSynchronos/Interop/Ipc/IpcCallerMoodles.cs
Normal file
104
MareSynchronos/Interop/Ipc/IpcCallerMoodles.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerMoodles : IIpcCaller
|
||||
{
|
||||
private readonly ICallGateSubscriber<int> _moodlesApiVersion;
|
||||
private readonly ICallGateSubscriber<IPlayerCharacter, object> _moodlesOnChange;
|
||||
private readonly ICallGateSubscriber<nint, string> _moodlesGetStatus;
|
||||
private readonly ICallGateSubscriber<nint, string, object> _moodlesSetStatus;
|
||||
private readonly ICallGateSubscriber<nint, object> _moodlesRevertStatus;
|
||||
private readonly ILogger<IpcCallerMoodles> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly MareMediator _mareMediator;
|
||||
|
||||
public IpcCallerMoodles(ILogger<IpcCallerMoodles> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||
MareMediator mareMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_mareMediator = mareMediator;
|
||||
|
||||
_moodlesApiVersion = pi.GetIpcSubscriber<int>("Moodles.Version");
|
||||
_moodlesOnChange = pi.GetIpcSubscriber<IPlayerCharacter, object>("Moodles.StatusManagerModified");
|
||||
_moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtr");
|
||||
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtr");
|
||||
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtr");
|
||||
|
||||
_moodlesOnChange.Subscribe(OnMoodlesChange);
|
||||
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
private void OnMoodlesChange(IPlayerCharacter character)
|
||||
{
|
||||
_mareMediator.Publish(new MoodlesMessage(character.Address));
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
try
|
||||
{
|
||||
APIAvailable = _moodlesApiVersion.InvokeFunc() == 1;
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_moodlesOnChange.Unsubscribe(OnMoodlesChange);
|
||||
}
|
||||
|
||||
public async Task<string?> GetStatusAsync(nint address)
|
||||
{
|
||||
if (!APIAvailable) return null;
|
||||
|
||||
try
|
||||
{
|
||||
return await _dalamudUtil.RunOnFrameworkThread(() => _moodlesGetStatus.InvokeFunc(address)).ConfigureAwait(false);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Could not Get Moodles Status");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetStatusAsync(nint pointer, string status)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
try
|
||||
{
|
||||
await _dalamudUtil.RunOnFrameworkThread(() => _moodlesSetStatus.InvokeAction(pointer, status)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Could not Set Moodles Status");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RevertStatusAsync(nint pointer)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
try
|
||||
{
|
||||
await _dalamudUtil.RunOnFrameworkThread(() => _moodlesRevertStatus.InvokeAction(pointer)).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Could not Set Moodles Status");
|
||||
}
|
||||
}
|
||||
}
|
360
MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs
Normal file
360
MareSynchronos/Interop/Ipc/IpcCallerPenumbra.cs
Normal file
@@ -0,0 +1,360 @@
|
||||
using Dalamud.Plugin;
|
||||
using MareSynchronos.MareConfiguration.Models;
|
||||
using MareSynchronos.PlayerData.Handlers;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace MareSynchronos.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerPenumbra : DisposableMediatorSubscriberBase, IIpcCaller
|
||||
{
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly MareMediator _mareMediator;
|
||||
private readonly RedrawManager _redrawManager;
|
||||
private bool _shownPenumbraUnavailable = false;
|
||||
private string? _penumbraModDirectory;
|
||||
public string? ModDirectory
|
||||
{
|
||||
get => _penumbraModDirectory;
|
||||
private set
|
||||
{
|
||||
if (!string.Equals(_penumbraModDirectory, value, StringComparison.Ordinal))
|
||||
{
|
||||
_penumbraModDirectory = value;
|
||||
_mareMediator.Publish(new PenumbraDirectoryChangedMessage(_penumbraModDirectory));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly ConcurrentDictionary<IntPtr, bool> _penumbraRedrawRequests = new();
|
||||
|
||||
private readonly EventSubscriber _penumbraDispose;
|
||||
private readonly EventSubscriber<nint, string, string> _penumbraGameObjectResourcePathResolved;
|
||||
private readonly EventSubscriber _penumbraInit;
|
||||
private readonly EventSubscriber<ModSettingChange, Guid, string, bool> _penumbraModSettingChanged;
|
||||
private readonly EventSubscriber<nint, int> _penumbraObjectIsRedrawn;
|
||||
|
||||
private readonly AddTemporaryMod _penumbraAddTemporaryMod;
|
||||
private readonly AssignTemporaryCollection _penumbraAssignTemporaryCollection;
|
||||
private readonly ConvertTextureFile _penumbraConvertTextureFile;
|
||||
private readonly CreateTemporaryCollection _penumbraCreateNamedTemporaryCollection;
|
||||
private readonly GetEnabledState _penumbraEnabled;
|
||||
private readonly GetPlayerMetaManipulations _penumbraGetMetaManipulations;
|
||||
private readonly RedrawObject _penumbraRedraw;
|
||||
private readonly DeleteTemporaryCollection _penumbraRemoveTemporaryCollection;
|
||||
private readonly RemoveTemporaryMod _penumbraRemoveTemporaryMod;
|
||||
private readonly GetModDirectory _penumbraResolveModDir;
|
||||
private readonly ResolvePlayerPathsAsync _penumbraResolvePaths;
|
||||
private readonly GetGameObjectResourcePaths _penumbraResourcePaths;
|
||||
|
||||
private bool _pluginLoaded;
|
||||
private Version _pluginVersion;
|
||||
|
||||
public IpcCallerPenumbra(ILogger<IpcCallerPenumbra> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||
MareMediator mareMediator, RedrawManager redrawManager) : base(logger, mareMediator)
|
||||
{
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_mareMediator = mareMediator;
|
||||
_redrawManager = redrawManager;
|
||||
_penumbraInit = Initialized.Subscriber(pi, PenumbraInit);
|
||||
_penumbraDispose = Disposed.Subscriber(pi, PenumbraDispose);
|
||||
_penumbraResolveModDir = new GetModDirectory(pi);
|
||||
_penumbraRedraw = new RedrawObject(pi);
|
||||
_penumbraObjectIsRedrawn = GameObjectRedrawn.Subscriber(pi, RedrawEvent);
|
||||
_penumbraGetMetaManipulations = new GetPlayerMetaManipulations(pi);
|
||||
_penumbraRemoveTemporaryMod = new RemoveTemporaryMod(pi);
|
||||
_penumbraAddTemporaryMod = new AddTemporaryMod(pi);
|
||||
_penumbraCreateNamedTemporaryCollection = new CreateTemporaryCollection(pi);
|
||||
_penumbraRemoveTemporaryCollection = new DeleteTemporaryCollection(pi);
|
||||
_penumbraAssignTemporaryCollection = new AssignTemporaryCollection(pi);
|
||||
_penumbraResolvePaths = new ResolvePlayerPathsAsync(pi);
|
||||
_penumbraEnabled = new GetEnabledState(pi);
|
||||
_penumbraModSettingChanged = ModSettingChanged.Subscriber(pi, (change, arg1, arg, b) =>
|
||||
{
|
||||
if (change == ModSettingChange.EnableState)
|
||||
_mareMediator.Publish(new PenumbraModSettingChangedMessage());
|
||||
});
|
||||
_penumbraConvertTextureFile = new ConvertTextureFile(pi);
|
||||
_penumbraResourcePaths = new GetGameObjectResourcePaths(pi);
|
||||
|
||||
_penumbraGameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pi, ResourceLoaded);
|
||||
|
||||
var plugin = PluginWatcherService.GetInitialPluginState(pi, "Penumbra");
|
||||
|
||||
_pluginLoaded = plugin?.IsLoaded ?? false;
|
||||
_pluginVersion = plugin?.Version ?? new(0, 0, 0, 0);
|
||||
|
||||
Mediator.SubscribeKeyed<PluginChangeMessage>(this, "Penumbra", (msg) =>
|
||||
{
|
||||
_pluginLoaded = msg.IsLoaded;
|
||||
_pluginVersion = msg.Version;
|
||||
CheckAPI();
|
||||
});
|
||||
|
||||
CheckAPI();
|
||||
CheckModDirectory();
|
||||
|
||||
Mediator.Subscribe<PenumbraRedrawCharacterMessage>(this, (msg) =>
|
||||
{
|
||||
_penumbraRedraw.Invoke(msg.Character.ObjectIndex, RedrawType.AfterGPose);
|
||||
});
|
||||
|
||||
Mediator.Subscribe<DalamudLoginMessage>(this, (msg) => _shownPenumbraUnavailable = false);
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
bool penumbraAvailable = false;
|
||||
try
|
||||
{
|
||||
penumbraAvailable = _pluginLoaded && _pluginVersion >= new Version(1, 0, 1, 0);
|
||||
try
|
||||
{
|
||||
penumbraAvailable &= _penumbraEnabled.Invoke();
|
||||
}
|
||||
catch
|
||||
{
|
||||
penumbraAvailable = false;
|
||||
}
|
||||
_shownPenumbraUnavailable = _shownPenumbraUnavailable && !penumbraAvailable;
|
||||
APIAvailable = penumbraAvailable;
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = penumbraAvailable;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!penumbraAvailable && !_shownPenumbraUnavailable)
|
||||
{
|
||||
_shownPenumbraUnavailable = true;
|
||||
_mareMediator.Publish(new NotificationMessage("Penumbra inactive",
|
||||
"Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Elezen. If you just updated Penumbra, ignore this message.",
|
||||
NotificationType.Error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void CheckModDirectory()
|
||||
{
|
||||
if (!APIAvailable)
|
||||
{
|
||||
ModDirectory = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
ModDirectory = _penumbraResolveModDir!.Invoke().ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
_redrawManager.Cancel();
|
||||
|
||||
_penumbraModSettingChanged.Dispose();
|
||||
_penumbraGameObjectResourcePathResolved.Dispose();
|
||||
_penumbraDispose.Dispose();
|
||||
_penumbraInit.Dispose();
|
||||
_penumbraObjectIsRedrawn.Dispose();
|
||||
}
|
||||
|
||||
public async Task AssignTemporaryCollectionAsync(ILogger logger, Guid collName, int idx)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var retAssign = _penumbraAssignTemporaryCollection.Invoke(collName, idx, forceAssignment: true);
|
||||
logger.LogTrace("Assigning Temp Collection {collName} to index {idx}, Success: {ret}", collName, idx, retAssign);
|
||||
return collName;
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task ConvertTextureFiles(ILogger logger, Dictionary<string, string[]> textures, IProgress<(string, int)> progress, CancellationToken token)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
|
||||
_mareMediator.Publish(new HaltScanMessage(nameof(ConvertTextureFiles)));
|
||||
int currentTexture = 0;
|
||||
foreach (var texture in textures)
|
||||
{
|
||||
if (token.IsCancellationRequested) break;
|
||||
|
||||
progress.Report((texture.Key, ++currentTexture));
|
||||
|
||||
logger.LogInformation("Converting Texture {path} to {type}", texture.Key, TextureType.Bc7Tex);
|
||||
var convertTask = _penumbraConvertTextureFile.Invoke(texture.Key, texture.Key, TextureType.Bc7Tex, mipMaps: true);
|
||||
await convertTask.ConfigureAwait(false);
|
||||
if (convertTask.IsCompletedSuccessfully && texture.Value.Any())
|
||||
{
|
||||
foreach (var duplicatedTexture in texture.Value)
|
||||
{
|
||||
logger.LogInformation("Migrating duplicate {dup}", duplicatedTexture);
|
||||
try
|
||||
{
|
||||
File.Copy(texture.Key, duplicatedTexture, overwrite: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to copy duplicate {dup}", duplicatedTexture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_mareMediator.Publish(new ResumeScanMessage(nameof(ConvertTextureFiles)));
|
||||
|
||||
await _dalamudUtil.RunOnFrameworkThread(async () =>
|
||||
{
|
||||
var gameObject = await _dalamudUtil.CreateGameObjectAsync(await _dalamudUtil.GetPlayerPointerAsync().ConfigureAwait(false)).ConfigureAwait(false);
|
||||
_penumbraRedraw.Invoke(gameObject!.ObjectIndex, setting: RedrawType.Redraw);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Guid> CreateTemporaryCollectionAsync(ILogger logger, string uid)
|
||||
{
|
||||
if (!APIAvailable) return Guid.Empty;
|
||||
|
||||
return await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var collName = "ElfSync_" + uid;
|
||||
var collId = _penumbraCreateNamedTemporaryCollection.Invoke(collName);
|
||||
logger.LogTrace("Creating Temp Collection {collName}, GUID: {collId}", collName, collId);
|
||||
return collId;
|
||||
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, HashSet<string>>?> GetCharacterData(ILogger logger, GameObjectHandler handler)
|
||||
{
|
||||
if (!APIAvailable) return null;
|
||||
|
||||
return await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("Calling On IPC: Penumbra.GetGameObjectResourcePaths");
|
||||
var idx = handler.GetGameObject()?.ObjectIndex;
|
||||
if (idx == null) return null;
|
||||
return _penumbraResourcePaths.Invoke(idx.Value)[0];
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string GetMetaManipulations()
|
||||
{
|
||||
if (!APIAvailable) return string.Empty;
|
||||
return _penumbraGetMetaManipulations.Invoke();
|
||||
}
|
||||
|
||||
public async Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
|
||||
{
|
||||
if (!APIAvailable || _dalamudUtil.IsZoning) return;
|
||||
try
|
||||
{
|
||||
await _redrawManager.RedrawSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
await _redrawManager.PenumbraRedrawInternalAsync(logger, handler, applicationId, (chara) =>
|
||||
{
|
||||
logger.LogDebug("[{appid}] Calling on IPC: PenumbraRedraw", applicationId);
|
||||
_penumbraRedraw!.Invoke(chara.ObjectIndex, setting: RedrawType.Redraw);
|
||||
|
||||
}, token).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_redrawManager.RedrawSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void RedrawNow(ILogger logger, Guid applicationId, int objectIndex)
|
||||
{
|
||||
if (!APIAvailable || _dalamudUtil.IsZoning) return;
|
||||
logger.LogTrace("[{applicationId}] Immediately redrawing object index {objId}", applicationId, objectIndex);
|
||||
_penumbraRedraw.Invoke(objectIndex);
|
||||
}
|
||||
|
||||
public async Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collId)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("[{applicationId}] Removing temp collection for {collId}", applicationId, collId);
|
||||
var ret2 = _penumbraRemoveTemporaryCollection.Invoke(collId);
|
||||
logger.LogTrace("[{applicationId}] RemoveTemporaryCollection: {ret2}", applicationId, ret2);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse)
|
||||
{
|
||||
return await _penumbraResolvePaths.Invoke(forward, reverse).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collId, string manipulationData)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
logger.LogTrace("[{applicationId}] Manip: {data}", applicationId, manipulationData);
|
||||
var retAdd = _penumbraAddTemporaryMod.Invoke("MareChara_Meta", collId, [], manipulationData, 0);
|
||||
logger.LogTrace("[{applicationId}] Setting temp meta mod for {collId}, Success: {ret}", applicationId, collId, retAdd);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collId, Dictionary<string, string> modPaths)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
foreach (var mod in modPaths)
|
||||
{
|
||||
logger.LogTrace("[{applicationId}] Change: {from} => {to}", applicationId, mod.Key, mod.Value);
|
||||
}
|
||||
var retRemove = _penumbraRemoveTemporaryMod.Invoke("MareChara_Files", collId, 0);
|
||||
logger.LogTrace("[{applicationId}] Removing temp files mod for {collId}, Success: {ret}", applicationId, collId, retRemove);
|
||||
var retAdd = _penumbraAddTemporaryMod.Invoke("MareChara_Files", collId, modPaths, string.Empty, 0);
|
||||
logger.LogTrace("[{applicationId}] Setting temp files mod for {collId}, Success: {ret}", applicationId, collId, retAdd);
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void RedrawEvent(IntPtr objectAddress, int objectTableIndex)
|
||||
{
|
||||
bool wasRequested = false;
|
||||
if (_penumbraRedrawRequests.TryGetValue(objectAddress, out var redrawRequest) && redrawRequest)
|
||||
{
|
||||
_penumbraRedrawRequests[objectAddress] = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_mareMediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex, wasRequested));
|
||||
}
|
||||
}
|
||||
|
||||
private void ResourceLoaded(IntPtr ptr, string arg1, string arg2)
|
||||
{
|
||||
if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, ignoreCase: true, System.Globalization.CultureInfo.InvariantCulture) != 0)
|
||||
{
|
||||
_mareMediator.Publish(new PenumbraResourceLoadMessage(ptr, arg1, arg2));
|
||||
}
|
||||
}
|
||||
|
||||
private void PenumbraDispose()
|
||||
{
|
||||
_redrawManager.Cancel();
|
||||
_mareMediator.Publish(new PenumbraDisposedMessage());
|
||||
}
|
||||
|
||||
private void PenumbraInit()
|
||||
{
|
||||
APIAvailable = true;
|
||||
ModDirectory = _penumbraResolveModDir.Invoke();
|
||||
_mareMediator.Publish(new PenumbraInitializedMessage());
|
||||
_penumbraRedraw!.Invoke(0, setting: RedrawType.Redraw);
|
||||
}
|
||||
}
|
158
MareSynchronos/Interop/Ipc/IpcCallerPetNames.cs
Normal file
158
MareSynchronos/Interop/Ipc/IpcCallerPetNames.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Interop.Ipc;
|
||||
|
||||
public sealed class IpcCallerPetNames : IIpcCaller
|
||||
{
|
||||
private readonly ILogger<IpcCallerPetNames> _logger;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly MareMediator _mareMediator;
|
||||
|
||||
private readonly ICallGateSubscriber<object> _petnamesReady;
|
||||
private readonly ICallGateSubscriber<object> _petnamesDisposing;
|
||||
private readonly ICallGateSubscriber<(uint, uint)> _apiVersion;
|
||||
private readonly ICallGateSubscriber<bool> _enabled;
|
||||
|
||||
private readonly ICallGateSubscriber<string, object> _playerDataChanged;
|
||||
private readonly ICallGateSubscriber<string> _getPlayerData;
|
||||
private readonly ICallGateSubscriber<string, object> _setPlayerData;
|
||||
private readonly ICallGateSubscriber<ushort, object> _clearPlayerData;
|
||||
|
||||
public IpcCallerPetNames(ILogger<IpcCallerPetNames> logger, IDalamudPluginInterface pi, DalamudUtilService dalamudUtil,
|
||||
MareMediator mareMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_mareMediator = mareMediator;
|
||||
|
||||
_petnamesReady = pi.GetIpcSubscriber<object>("PetRenamer.Ready");
|
||||
_petnamesDisposing = pi.GetIpcSubscriber<object>("PetRenamer.Disposing");
|
||||
_apiVersion = pi.GetIpcSubscriber<(uint, uint)>("PetRenamer.ApiVersion");
|
||||
_enabled = pi.GetIpcSubscriber<bool>("PetRenamer.Enabled");
|
||||
|
||||
_playerDataChanged = pi.GetIpcSubscriber<string, object>("PetRenamer.PlayerDataChanged");
|
||||
_getPlayerData = pi.GetIpcSubscriber<string>("PetRenamer.GetPlayerData");
|
||||
_setPlayerData = pi.GetIpcSubscriber<string, object>("PetRenamer.SetPlayerData");
|
||||
_clearPlayerData = pi.GetIpcSubscriber<ushort, object>("PetRenamer.ClearPlayerData");
|
||||
|
||||
_petnamesReady.Subscribe(OnPetNicknamesReady);
|
||||
_petnamesDisposing.Subscribe(OnPetNicknamesDispose);
|
||||
_playerDataChanged.Subscribe(OnLocalPetNicknamesDataChange);
|
||||
|
||||
CheckAPI();
|
||||
}
|
||||
|
||||
public bool APIAvailable { get; private set; } = false;
|
||||
|
||||
public void CheckAPI()
|
||||
{
|
||||
try
|
||||
{
|
||||
APIAvailable = _enabled?.InvokeFunc() ?? false;
|
||||
if (APIAvailable)
|
||||
{
|
||||
APIAvailable = _apiVersion?.InvokeFunc() is { Item1: 3, Item2: >= 1 };
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
APIAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPetNicknamesReady()
|
||||
{
|
||||
CheckAPI();
|
||||
_mareMediator.Publish(new PetNamesReadyMessage());
|
||||
}
|
||||
|
||||
private void OnPetNicknamesDispose()
|
||||
{
|
||||
_mareMediator.Publish(new PetNamesMessage(string.Empty));
|
||||
}
|
||||
|
||||
public string GetLocalNames()
|
||||
{
|
||||
if (!APIAvailable) return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
string localNameData = _getPlayerData.InvokeFunc();
|
||||
return string.IsNullOrEmpty(localNameData) ? string.Empty : localNameData;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Could not obtain Pet Nicknames data");
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public async Task SetPlayerData(nint character, string playerData)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
|
||||
_logger.LogTrace("Applying Pet Nicknames data to {chara}", character.ToString("X"));
|
||||
|
||||
try
|
||||
{
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(playerData))
|
||||
{
|
||||
var gameObj = _dalamudUtil.CreateGameObject(character);
|
||||
if (gameObj is IPlayerCharacter pc)
|
||||
{
|
||||
_clearPlayerData.InvokeAction(pc.ObjectIndex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_setPlayerData.InvokeAction(playerData);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Could not apply Pet Nicknames data");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ClearPlayerData(nint characterPointer)
|
||||
{
|
||||
if (!APIAvailable) return;
|
||||
try
|
||||
{
|
||||
await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var gameObj = _dalamudUtil.CreateGameObject(characterPointer);
|
||||
if (gameObj is IPlayerCharacter pc)
|
||||
{
|
||||
_logger.LogTrace("Pet Nicknames removing for {addr}", pc.Address.ToString("X"));
|
||||
_clearPlayerData.InvokeAction(pc.ObjectIndex);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogWarning(e, "Could not clear Pet Nicknames data");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLocalPetNicknamesDataChange(string data)
|
||||
{
|
||||
_mareMediator.Publish(new PetNamesMessage(data));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_petnamesReady.Unsubscribe(OnPetNicknamesReady);
|
||||
_petnamesDisposing.Unsubscribe(OnPetNicknamesDispose);
|
||||
_playerDataChanged.Unsubscribe(OnLocalPetNicknamesDataChange);
|
||||
}
|
||||
}
|
68
MareSynchronos/Interop/Ipc/IpcManager.cs
Normal file
68
MareSynchronos/Interop/Ipc/IpcManager.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Interop.Ipc;
|
||||
|
||||
public sealed partial class IpcManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
public IpcManager(ILogger<IpcManager> logger, MareMediator mediator,
|
||||
IpcCallerPenumbra penumbraIpc, IpcCallerGlamourer glamourerIpc, IpcCallerCustomize customizeIpc, IpcCallerHeels heelsIpc,
|
||||
IpcCallerHonorific honorificIpc, IpcCallerMoodles moodlesIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerBrio ipcCallerBrio) : base(logger, mediator)
|
||||
{
|
||||
CustomizePlus = customizeIpc;
|
||||
Heels = heelsIpc;
|
||||
Glamourer = glamourerIpc;
|
||||
Penumbra = penumbraIpc;
|
||||
Honorific = honorificIpc;
|
||||
Moodles = moodlesIpc;
|
||||
PetNames = ipcCallerPetNames;
|
||||
Brio = ipcCallerBrio;
|
||||
|
||||
if (Initialized)
|
||||
{
|
||||
Mediator.Publish(new PenumbraInitializedMessage());
|
||||
}
|
||||
|
||||
Mediator.Subscribe<DelayedFrameworkUpdateMessage>(this, (_) => PeriodicApiStateCheck());
|
||||
|
||||
try
|
||||
{
|
||||
PeriodicApiStateCheck();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Failed to check for some IPC, plugin not installed?");
|
||||
}
|
||||
}
|
||||
|
||||
public bool Initialized => Penumbra.APIAvailable && Glamourer.APIAvailable;
|
||||
|
||||
public IpcCallerCustomize CustomizePlus { get; init; }
|
||||
public IpcCallerHonorific Honorific { get; init; }
|
||||
public IpcCallerHeels Heels { get; init; }
|
||||
public IpcCallerGlamourer Glamourer { get; }
|
||||
public IpcCallerPenumbra Penumbra { get; }
|
||||
public IpcCallerMoodles Moodles { get; }
|
||||
public IpcCallerPetNames PetNames { get; }
|
||||
|
||||
public IpcCallerBrio Brio { get; }
|
||||
|
||||
private int _stateCheckCounter = -1;
|
||||
|
||||
private void PeriodicApiStateCheck()
|
||||
{
|
||||
// Stagger API checks
|
||||
if (++_stateCheckCounter > 8)
|
||||
_stateCheckCounter = 0;
|
||||
int i = _stateCheckCounter;
|
||||
if (i == 0) Penumbra.CheckAPI();
|
||||
if (i == 1) Penumbra.CheckModDirectory();
|
||||
if (i == 2) Glamourer.CheckAPI();
|
||||
if (i == 3) Heels.CheckAPI();
|
||||
if (i == 4) CustomizePlus.CheckAPI();
|
||||
if (i == 5) Honorific.CheckAPI();
|
||||
if (i == 6) Moodles.CheckAPI();
|
||||
if (i == 7) PetNames.CheckAPI();
|
||||
if (i == 8) Brio.CheckAPI();
|
||||
}
|
||||
}
|
196
MareSynchronos/Interop/Ipc/IpcProvider.cs
Normal file
196
MareSynchronos/Interop/Ipc/IpcProvider.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using MareSynchronos.MareConfiguration;
|
||||
using MareSynchronos.PlayerData.Handlers;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Utils;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MareSynchronos.Interop.Ipc;
|
||||
|
||||
public class IpcProvider : IHostedService, IMediatorSubscriber
|
||||
{
|
||||
private readonly ILogger<IpcProvider> _logger;
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
private readonly MareConfigService _mareConfig;
|
||||
private readonly CharaDataManager _charaDataManager;
|
||||
private ICallGateProvider<string, IGameObject, bool>? _loadFileProvider;
|
||||
private ICallGateProvider<string, IGameObject, Task<bool>>? _loadFileAsyncProvider;
|
||||
private ICallGateProvider<List<nint>>? _handledGameAddresses;
|
||||
private readonly List<GameObjectHandler> _activeGameObjectHandlers = [];
|
||||
|
||||
private ICallGateProvider<string, IGameObject, bool>? _loadFileProviderMare;
|
||||
private ICallGateProvider<string, IGameObject, Task<bool>>? _loadFileAsyncProviderMare;
|
||||
private ICallGateProvider<List<nint>>? _handledGameAddressesMare;
|
||||
|
||||
private bool _marePluginEnabled = false;
|
||||
private bool _impersonating = false;
|
||||
private DateTime _unregisterTime = DateTime.UtcNow;
|
||||
private CancellationTokenSource _registerDelayCts = new();
|
||||
|
||||
public bool MarePluginEnabled => _marePluginEnabled;
|
||||
public bool ImpersonationActive => _impersonating;
|
||||
|
||||
public MareMediator Mediator { get; init; }
|
||||
|
||||
public IpcProvider(ILogger<IpcProvider> logger, IDalamudPluginInterface pi, MareConfigService mareConfig,
|
||||
CharaDataManager charaDataManager, MareMediator mareMediator)
|
||||
{
|
||||
_logger = logger;
|
||||
_pi = pi;
|
||||
_mareConfig = mareConfig;
|
||||
_charaDataManager = charaDataManager;
|
||||
Mediator = mareMediator;
|
||||
|
||||
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
|
||||
{
|
||||
if (msg.OwnedObject) return;
|
||||
_activeGameObjectHandlers.Add(msg.GameObjectHandler);
|
||||
});
|
||||
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, (msg) =>
|
||||
{
|
||||
if (msg.OwnedObject) return;
|
||||
_activeGameObjectHandlers.Remove(msg.GameObjectHandler);
|
||||
});
|
||||
|
||||
_marePluginEnabled = PluginWatcherService.GetInitialPluginState(pi, "MareSynchronos")?.IsLoaded ?? false;
|
||||
Mediator.SubscribeKeyed<PluginChangeMessage>(this, "MareSynchronos", p => {
|
||||
_marePluginEnabled = p.IsLoaded;
|
||||
HandleMareImpersonation(automatic: true);
|
||||
});
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug("Starting IpcProvider Service");
|
||||
_loadFileProvider = _pi.GetIpcProvider<string, IGameObject, bool>("ElfSync.LoadMcdf");
|
||||
_loadFileProvider.RegisterFunc(LoadMcdf);
|
||||
_loadFileAsyncProvider = _pi.GetIpcProvider<string, IGameObject, Task<bool>>("ElezenSync.LoadMcdfAsync");
|
||||
_loadFileAsyncProvider.RegisterFunc(LoadMcdfAsync);
|
||||
_handledGameAddresses = _pi.GetIpcProvider<List<nint>>("ElezenSync.GetHandledAddresses");
|
||||
_handledGameAddresses.RegisterFunc(GetHandledAddresses);
|
||||
|
||||
_loadFileProviderMare = _pi.GetIpcProvider<string, IGameObject, bool>("MareSynchronos.LoadMcdf");
|
||||
_loadFileAsyncProviderMare = _pi.GetIpcProvider<string, IGameObject, Task<bool>>("MareSynchronos.LoadMcdfAsync");
|
||||
_handledGameAddressesMare = _pi.GetIpcProvider<List<nint>>("MareSynchronos.GetHandledAddresses");
|
||||
HandleMareImpersonation(automatic: true);
|
||||
|
||||
_logger.LogInformation("Started IpcProviderService");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void HandleMareImpersonation(bool automatic = false)
|
||||
{
|
||||
if (_marePluginEnabled)
|
||||
{
|
||||
if (_impersonating)
|
||||
{
|
||||
_loadFileProviderMare?.UnregisterFunc();
|
||||
_loadFileAsyncProviderMare?.UnregisterFunc();
|
||||
_handledGameAddressesMare?.UnregisterFunc();
|
||||
_impersonating = false;
|
||||
_unregisterTime = DateTime.UtcNow;
|
||||
_logger.LogDebug("Unregistered MareSynchronos API");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_mareConfig.Current.MareAPI)
|
||||
{
|
||||
var cancelToken = _registerDelayCts.Token;
|
||||
Task.Run(async () =>
|
||||
{
|
||||
// Wait before registering to reduce the chance of a race condition
|
||||
if (automatic)
|
||||
await Task.Delay(5000);
|
||||
|
||||
if (cancelToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
if (_marePluginEnabled)
|
||||
{
|
||||
_logger.LogDebug("Not registering MareSynchronos API: Mare plugin is loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
_loadFileProviderMare?.RegisterFunc(LoadMcdf);
|
||||
_loadFileAsyncProviderMare?.RegisterFunc(LoadMcdfAsync);
|
||||
_handledGameAddressesMare?.RegisterFunc(GetHandledAddresses);
|
||||
_impersonating = true;
|
||||
_logger.LogDebug("Registered MareSynchronos API");
|
||||
}, cancelToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
_registerDelayCts = _registerDelayCts.CancelRecreate();
|
||||
if (_impersonating)
|
||||
{
|
||||
_loadFileProviderMare?.UnregisterFunc();
|
||||
_loadFileAsyncProviderMare?.UnregisterFunc();
|
||||
_handledGameAddressesMare?.UnregisterFunc();
|
||||
_impersonating = false;
|
||||
_unregisterTime = DateTime.UtcNow;
|
||||
_logger.LogDebug("Unregistered MareSynchronos API");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogDebug("Stopping IpcProvider Service");
|
||||
_loadFileProvider?.UnregisterFunc();
|
||||
_loadFileAsyncProvider?.UnregisterFunc();
|
||||
_handledGameAddresses?.UnregisterFunc();
|
||||
|
||||
_registerDelayCts.Cancel();
|
||||
if (_impersonating)
|
||||
{
|
||||
_loadFileProviderMare?.UnregisterFunc();
|
||||
_loadFileAsyncProviderMare?.UnregisterFunc();
|
||||
_handledGameAddressesMare?.UnregisterFunc();
|
||||
}
|
||||
|
||||
Mediator.UnsubscribeAll(this);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task<bool> LoadMcdfAsync(string path, IGameObject target)
|
||||
{
|
||||
await ApplyFileAsync(path, target).ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool LoadMcdf(string path, IGameObject target)
|
||||
{
|
||||
_ = Task.Run(async () => await ApplyFileAsync(path, target).ConfigureAwait(false)).ConfigureAwait(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task ApplyFileAsync(string path, IGameObject target)
|
||||
{
|
||||
_charaDataManager.LoadMcdf(path);
|
||||
await (_charaDataManager.LoadedMcdfHeader ?? Task.CompletedTask).ConfigureAwait(false);
|
||||
_charaDataManager.McdfApplyToTarget(target.Name.TextValue);
|
||||
}
|
||||
|
||||
private List<nint> GetHandledAddresses()
|
||||
{
|
||||
if (!_impersonating)
|
||||
{
|
||||
if ((DateTime.UtcNow - _unregisterTime).TotalSeconds >= 1.0)
|
||||
{
|
||||
_logger.LogWarning("GetHandledAddresses called when it should not be registered");
|
||||
_handledGameAddressesMare?.UnregisterFunc();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
return _activeGameObjectHandlers.Where(g => g.Address != nint.Zero).Select(g => g.Address).Distinct().ToList();
|
||||
}
|
||||
}
|
54
MareSynchronos/Interop/Ipc/RedrawManager.cs
Normal file
54
MareSynchronos/Interop/Ipc/RedrawManager.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using MareSynchronos.PlayerData.Handlers;
|
||||
using MareSynchronos.Services;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using MareSynchronos.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace MareSynchronos.Interop.Ipc;
|
||||
|
||||
public class RedrawManager
|
||||
{
|
||||
private readonly MareMediator _mareMediator;
|
||||
private readonly DalamudUtilService _dalamudUtil;
|
||||
private readonly ConcurrentDictionary<nint, bool> _penumbraRedrawRequests = [];
|
||||
private CancellationTokenSource _disposalCts = new();
|
||||
|
||||
public SemaphoreSlim RedrawSemaphore { get; init; } = new(2, 2);
|
||||
|
||||
public RedrawManager(MareMediator mareMediator, DalamudUtilService dalamudUtil)
|
||||
{
|
||||
_mareMediator = mareMediator;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
}
|
||||
|
||||
public async Task PenumbraRedrawInternalAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, Action<ICharacter> action, CancellationToken token)
|
||||
{
|
||||
_mareMediator.Publish(new PenumbraStartRedrawMessage(handler.Address));
|
||||
|
||||
_penumbraRedrawRequests[handler.Address] = true;
|
||||
|
||||
try
|
||||
{
|
||||
using CancellationTokenSource cancelToken = new CancellationTokenSource();
|
||||
using CancellationTokenSource combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancelToken.Token, token, _disposalCts.Token);
|
||||
var combinedToken = combinedCts.Token;
|
||||
cancelToken.CancelAfter(TimeSpan.FromSeconds(15));
|
||||
await handler.ActOnFrameworkAfterEnsureNoDrawAsync(action, combinedToken).ConfigureAwait(false);
|
||||
|
||||
if (!_disposalCts.Token.IsCancellationRequested)
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(logger, handler, applicationId, 30000, combinedToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_penumbraRedrawRequests[handler.Address] = false;
|
||||
_mareMediator.Publish(new PenumbraEndRedrawMessage(handler.Address));
|
||||
}
|
||||
}
|
||||
|
||||
internal void Cancel()
|
||||
{
|
||||
_disposalCts = _disposalCts.CancelRecreate();
|
||||
}
|
||||
}
|
203
MareSynchronos/Interop/VfxSpawnManager.cs
Normal file
203
MareSynchronos/Interop/VfxSpawnManager.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
using Dalamud.Memory;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using MareSynchronos.Services.Mediator;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace MareSynchronos.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Code for spawning mostly taken from https://git.anna.lgbt/anna/OrangeGuidanceTomestone/src/branch/main/client/Vfx.cs
|
||||
/// </summary>
|
||||
public unsafe class VfxSpawnManager : DisposableMediatorSubscriberBase
|
||||
{
|
||||
private static readonly byte[] _pool = "Client.System.Scheduler.Instance.VfxObject\0"u8.ToArray();
|
||||
|
||||
#region signatures
|
||||
#pragma warning disable CS0649
|
||||
[Signature("E8 ?? ?? ?? ?? F3 0F 10 35 ?? ?? ?? ?? 48 89 43 08")]
|
||||
private readonly delegate* unmanaged<byte*, byte*, VfxStruct*> _staticVfxCreate;
|
||||
|
||||
[Signature("E8 ?? ?? ?? ?? ?? ?? ?? 8B 4A ?? 85 C9")]
|
||||
private readonly delegate* unmanaged<VfxStruct*, float, int, ulong> _staticVfxRun;
|
||||
|
||||
[Signature("40 53 48 83 EC 20 48 8B D9 48 8B 89 ?? ?? ?? ?? 48 85 C9 74 28 33 D2 E8 ?? ?? ?? ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9")]
|
||||
private readonly delegate* unmanaged<VfxStruct*, nint> _staticVfxRemove;
|
||||
#pragma warning restore CS0649
|
||||
#endregion
|
||||
|
||||
public VfxSpawnManager(ILogger<VfxSpawnManager> logger, IGameInteropProvider gameInteropProvider, MareMediator mareMediator)
|
||||
: base(logger, mareMediator)
|
||||
{
|
||||
gameInteropProvider.InitializeFromAttributes(this);
|
||||
mareMediator.Subscribe<GposeStartMessage>(this, (msg) =>
|
||||
{
|
||||
ChangeSpawnVisibility(0f);
|
||||
});
|
||||
mareMediator.Subscribe<GposeEndMessage>(this, (msg) =>
|
||||
{
|
||||
RestoreSpawnVisiblity();
|
||||
});
|
||||
mareMediator.Subscribe<CutsceneStartMessage>(this, (msg) =>
|
||||
{
|
||||
ChangeSpawnVisibility(0f);
|
||||
});
|
||||
mareMediator.Subscribe<CutsceneEndMessage>(this, (msg) =>
|
||||
{
|
||||
RestoreSpawnVisiblity();
|
||||
});
|
||||
}
|
||||
|
||||
private unsafe void RestoreSpawnVisiblity()
|
||||
{
|
||||
foreach (var vfx in _spawnedObjects)
|
||||
{
|
||||
((VfxStruct*)vfx.Value.Address)->Alpha = vfx.Value.Visibility;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ChangeSpawnVisibility(float visibility)
|
||||
{
|
||||
foreach (var vfx in _spawnedObjects)
|
||||
{
|
||||
((VfxStruct*)vfx.Value.Address)->Alpha = visibility;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Dictionary<Guid, (nint Address, float Visibility)> _spawnedObjects = [];
|
||||
|
||||
private VfxStruct* SpawnStatic(string path, Vector3 pos, Quaternion rotation, float r, float g, float b, float a, Vector3 scale)
|
||||
{
|
||||
VfxStruct* vfx;
|
||||
fixed (byte* terminatedPath = Encoding.UTF8.GetBytes(path).NullTerminate())
|
||||
{
|
||||
fixed (byte* pool = _pool)
|
||||
{
|
||||
vfx = _staticVfxCreate(terminatedPath, pool);
|
||||
}
|
||||
}
|
||||
|
||||
if (vfx == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
vfx->Position = new Vector3(pos.X, pos.Y + 1, pos.Z);
|
||||
vfx->Rotation = new Quaternion(rotation.X, rotation.Y, rotation.Z, rotation.W);
|
||||
|
||||
vfx->SomeFlags &= 0xF7;
|
||||
vfx->Flags |= 2;
|
||||
vfx->Red = r;
|
||||
vfx->Green = g;
|
||||
vfx->Blue = b;
|
||||
vfx->Scale = scale;
|
||||
|
||||
vfx->Alpha = a;
|
||||
|
||||
_staticVfxRun(vfx, 0.0f, -1);
|
||||
|
||||
return vfx;
|
||||
}
|
||||
|
||||
public Guid? SpawnObject(Vector3 position, Quaternion rotation, Vector3 scale, float r = 1f, float g = 1f, float b = 1f, float a = 0.5f)
|
||||
{
|
||||
Logger.LogDebug("Trying to Spawn orb VFX at {pos}, {rot}", position, rotation);
|
||||
var vfx = SpawnStatic("bgcommon/world/common/vfx_for_event/eff/b0150_eext_y.avfx", position, rotation, r, g, b, a, scale);
|
||||
if (vfx == null || (nint)vfx == nint.Zero)
|
||||
{
|
||||
Logger.LogDebug("Failed to Spawn VFX at {pos}, {rot}", position, rotation);
|
||||
return null;
|
||||
}
|
||||
Guid guid = Guid.NewGuid();
|
||||
Logger.LogDebug("Spawned VFX at {pos}, {rot}: 0x{ptr:X}", position, rotation, (nint)vfx);
|
||||
|
||||
_spawnedObjects[guid] = ((nint)vfx, a);
|
||||
|
||||
return guid;
|
||||
}
|
||||
|
||||
public unsafe void MoveObject(Guid id, Vector3 newPosition)
|
||||
{
|
||||
if (_spawnedObjects.TryGetValue(id, out var vfxValue))
|
||||
{
|
||||
if (vfxValue.Address == nint.Zero) return;
|
||||
var vfx = (VfxStruct*)vfxValue.Address;
|
||||
vfx->Position = newPosition with { Y = newPosition.Y + 1 };
|
||||
vfx->Flags |= 2;
|
||||
}
|
||||
}
|
||||
|
||||
public void DespawnObject(Guid? id)
|
||||
{
|
||||
if (id == null) return;
|
||||
if (_spawnedObjects.Remove(id.Value, out var value))
|
||||
{
|
||||
Logger.LogDebug("Despawning {obj:X}", value.Address);
|
||||
_staticVfxRemove((VfxStruct*)value.Address);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveAllVfx()
|
||||
{
|
||||
foreach (var obj in _spawnedObjects.Values)
|
||||
{
|
||||
Logger.LogDebug("Despawning {obj:X}", obj);
|
||||
_staticVfxRemove((VfxStruct*)obj.Address);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (disposing)
|
||||
{
|
||||
RemoveAllVfx();
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
internal struct VfxStruct
|
||||
{
|
||||
[FieldOffset(0x38)]
|
||||
public byte Flags;
|
||||
|
||||
[FieldOffset(0x50)]
|
||||
public Vector3 Position;
|
||||
|
||||
[FieldOffset(0x60)]
|
||||
public Quaternion Rotation;
|
||||
|
||||
[FieldOffset(0x70)]
|
||||
public Vector3 Scale;
|
||||
|
||||
[FieldOffset(0x128)]
|
||||
public int ActorCaster;
|
||||
|
||||
[FieldOffset(0x130)]
|
||||
public int ActorTarget;
|
||||
|
||||
[FieldOffset(0x1B8)]
|
||||
public int StaticCaster;
|
||||
|
||||
[FieldOffset(0x1C0)]
|
||||
public int StaticTarget;
|
||||
|
||||
[FieldOffset(0x248)]
|
||||
public byte SomeFlags;
|
||||
|
||||
[FieldOffset(0x260)]
|
||||
public float Red;
|
||||
|
||||
[FieldOffset(0x264)]
|
||||
public float Green;
|
||||
|
||||
[FieldOffset(0x268)]
|
||||
public float Blue;
|
||||
|
||||
[FieldOffset(0x26C)]
|
||||
public float Alpha;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user