514 lines
16 KiB
C#
514 lines
16 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using System.IO.MemoryMappedFiles;
|
|
using System.Numerics;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
|
using Dalamud.Plugin.Services;
|
|
|
|
namespace QuestionableCompanion.Services;
|
|
|
|
public class CrossProcessIPC : IDisposable
|
|
{
|
|
private readonly IPluginLog log;
|
|
|
|
private readonly IFramework framework;
|
|
|
|
private readonly Configuration configuration;
|
|
|
|
private MemoryMappedFile? mmf;
|
|
|
|
private Thread? listenerThread;
|
|
|
|
private bool isRunning;
|
|
|
|
private const string MMF_NAME = "QSTCompanion_IPC";
|
|
|
|
private const int MMF_SIZE = 4096;
|
|
|
|
private const int POLLING_INTERVAL_MS = 10;
|
|
|
|
public event Action<string, ushort>? OnHelperAvailable;
|
|
|
|
public event Action<string, ushort>? OnHelperRequested;
|
|
|
|
public event Action? OnHelperDismissed;
|
|
|
|
public event Action<string>? OnChatMessageReceived;
|
|
|
|
public event Action<string>? OnCommandReceived;
|
|
|
|
public event Action<string, ushort>? OnHelperInParty;
|
|
|
|
public event Action<string, ushort>? OnHelperInDuty;
|
|
|
|
public event Action<string, ushort>? OnHelperReady;
|
|
|
|
public event Action? OnRequestHelperAnnouncements;
|
|
|
|
public event Action<string, ushort, uint, Vector3, Vector3, bool>? OnChauffeurSummonRequest;
|
|
|
|
public event Action<string>? OnChauffeurReadyForPickup;
|
|
|
|
public event Action<string, ushort>? OnChauffeurArrived;
|
|
|
|
public event Action<string, ushort, uint, string>? OnChauffeurZoneUpdate;
|
|
|
|
public event Action<string, ushort>? OnChauffeurMountReady;
|
|
|
|
public event Action? OnChauffeurPassengerMounted;
|
|
|
|
public event Action<string, ushort, string>? OnHelperStatusUpdate;
|
|
|
|
public event Action<string, ushort, uint, Vector3>? OnQuesterPositionUpdate;
|
|
|
|
public CrossProcessIPC(IPluginLog log, IFramework framework, Configuration configuration)
|
|
{
|
|
this.log = log;
|
|
this.framework = framework;
|
|
this.configuration = configuration;
|
|
InitializeIPC();
|
|
}
|
|
|
|
private void InitializeIPC()
|
|
{
|
|
try
|
|
{
|
|
mmf = MemoryMappedFile.CreateOrOpen("QSTCompanion_IPC", 4096L, MemoryMappedFileAccess.ReadWrite);
|
|
isRunning = true;
|
|
listenerThread = new Thread(ListenerLoop)
|
|
{
|
|
IsBackground = true,
|
|
Name = "QSTCompanion IPC Listener"
|
|
};
|
|
listenerThread.Start();
|
|
log.Information("[CrossProcessIPC] Initialized with Memory-Mapped File");
|
|
if (configuration.IsHighLevelHelper)
|
|
{
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
AnnounceHelper();
|
|
});
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[CrossProcessIPC] Failed to initialize: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private void ListenerLoop()
|
|
{
|
|
string lastMessage = "";
|
|
while (isRunning)
|
|
{
|
|
try
|
|
{
|
|
if (mmf == null)
|
|
{
|
|
break;
|
|
}
|
|
using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor(0L, 4096L, MemoryMappedFileAccess.Read))
|
|
{
|
|
byte[] buffer = new byte[4096];
|
|
accessor.ReadArray(0L, buffer, 0, 4096);
|
|
string message = Encoding.UTF8.GetString(buffer).TrimEnd('\0');
|
|
if (!string.IsNullOrEmpty(message) && message != lastMessage)
|
|
{
|
|
lastMessage = message;
|
|
ProcessMessage(message);
|
|
}
|
|
}
|
|
Thread.Sleep(10);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[CrossProcessIPC] Listener error: " + ex.Message);
|
|
Thread.Sleep(1000);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ProcessMessage(string message)
|
|
{
|
|
try
|
|
{
|
|
string[] parts = message.Split('|');
|
|
if (parts.Length < 2)
|
|
{
|
|
return;
|
|
}
|
|
string command = parts[0];
|
|
_003C_003Ec__DisplayClass63_0 CS_0024_003C_003E8__locals0;
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
string text = command;
|
|
if (text != null)
|
|
{
|
|
switch (text.Length)
|
|
{
|
|
case 16:
|
|
switch (text[0])
|
|
{
|
|
case 'H':
|
|
if (text == "HELPER_AVAILABLE" && parts.Length >= 3)
|
|
{
|
|
string text3 = parts[1];
|
|
if (ushort.TryParse(parts[2], out var result8))
|
|
{
|
|
log.Information($"[CrossProcessIPC] Helper available: {text3}@{result8}");
|
|
this.OnHelperAvailable?.Invoke(text3, result8);
|
|
}
|
|
}
|
|
break;
|
|
case 'C':
|
|
if (text == "CHAUFFEUR_SUMMON" && parts.Length >= 11)
|
|
{
|
|
ushort questerWorld = ushort.Parse(parts[2]);
|
|
uint zoneId = uint.Parse(parts[3]);
|
|
Vector3 targetPos = new Vector3(float.Parse(parts[4]), float.Parse(parts[5]), float.Parse(parts[6]));
|
|
Vector3 questerPos = new Vector3(float.Parse(parts[7]), float.Parse(parts[8]), float.Parse(parts[9]));
|
|
bool isAttuneAetheryte = bool.Parse(parts[10]);
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
this.OnChauffeurSummonRequest?.Invoke((string)(object)CS_0024_003C_003E8__locals0, questerWorld, zoneId, targetPos, questerPos, isAttuneAetheryte);
|
|
});
|
|
}
|
|
break;
|
|
case 'Q':
|
|
if (text == "QUESTER_POSITION" && parts.Length >= 7)
|
|
{
|
|
string arg3 = parts[1];
|
|
if (ushort.TryParse(parts[2], out var result3) && uint.TryParse(parts[3], out var result4) && float.TryParse(parts[4], NumberStyles.Float, CultureInfo.InvariantCulture, out var result5) && float.TryParse(parts[5], NumberStyles.Float, CultureInfo.InvariantCulture, out var result6) && float.TryParse(parts[6], NumberStyles.Float, CultureInfo.InvariantCulture, out var result7))
|
|
{
|
|
Vector3 arg4 = new Vector3(result5, result6, result7);
|
|
this.OnQuesterPositionUpdate?.Invoke(arg3, result3, result4, arg4);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case 14:
|
|
switch (text[7])
|
|
{
|
|
case 'R':
|
|
if (text == "HELPER_REQUEST" && parts.Length >= 3)
|
|
{
|
|
string text12 = parts[1];
|
|
if (ushort.TryParse(parts[2], out var result14))
|
|
{
|
|
log.Information($"[CrossProcessIPC] Helper request: {text12}@{result14}");
|
|
this.OnHelperRequested?.Invoke(text12, result14);
|
|
}
|
|
}
|
|
break;
|
|
case 'D':
|
|
if (text == "HELPER_DISMISS")
|
|
{
|
|
log.Information("[CrossProcessIPC] Helper dismiss");
|
|
this.OnHelperDismissed?.Invoke();
|
|
}
|
|
break;
|
|
case 'I':
|
|
if (text == "HELPER_IN_DUTY" && parts.Length >= 3)
|
|
{
|
|
string text11 = parts[1];
|
|
if (ushort.TryParse(parts[2], out var result13))
|
|
{
|
|
log.Information($"[CrossProcessIPC] Helper in duty: {text11}@{result13}");
|
|
this.OnHelperInDuty?.Invoke(text11, result13);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case 15:
|
|
switch (text[0])
|
|
{
|
|
case 'H':
|
|
if (text == "HELPER_IN_PARTY" && parts.Length >= 3)
|
|
{
|
|
string text9 = parts[1];
|
|
if (ushort.TryParse(parts[2], out var result12))
|
|
{
|
|
log.Information($"[CrossProcessIPC] Helper in party: {text9}@{result12}");
|
|
this.OnHelperInParty?.Invoke(text9, result12);
|
|
}
|
|
}
|
|
break;
|
|
case 'C':
|
|
if (text == "CHAUFFEUR_READY" && parts.Length >= 2)
|
|
{
|
|
string text8 = parts[1];
|
|
log.Information("[CrossProcessIPC] Chauffeur ready: " + text8);
|
|
this.OnChauffeurReadyForPickup?.Invoke(text8);
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case 21:
|
|
switch (text[10])
|
|
{
|
|
case 'Z':
|
|
if (text == "CHAUFFEUR_ZONE_UPDATE" && parts.Length >= 5)
|
|
{
|
|
string text5 = parts[1];
|
|
if (ushort.TryParse(parts[2], out var result10) && uint.TryParse(parts[3], out var result11))
|
|
{
|
|
string text6 = parts[4];
|
|
log.Information($"[CrossProcessIPC] Zone update: {text5}@{result10} -> {text6} ({result11})");
|
|
this.OnChauffeurZoneUpdate?.Invoke(text5, result10, result11, text6);
|
|
}
|
|
}
|
|
break;
|
|
case 'M':
|
|
if (text == "CHAUFFEUR_MOUNT_READY" && parts.Length >= 3)
|
|
{
|
|
string text4 = parts[1];
|
|
if (ushort.TryParse(parts[2], out var result9))
|
|
{
|
|
log.Information($"[CrossProcessIPC] Chauffeur mount ready for: {text4}@{result9}");
|
|
IPluginLog pluginLog = log;
|
|
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(53, 1);
|
|
defaultInterpolatedStringHandler.AppendLiteral("[CrossProcessIPC] OnChauffeurMountReady subscribers: ");
|
|
Action<string, ushort>? action = this.OnChauffeurMountReady;
|
|
defaultInterpolatedStringHandler.AppendFormatted((action != null) ? action.GetInvocationList().Length : 0);
|
|
pluginLog.Information(defaultInterpolatedStringHandler.ToStringAndClear());
|
|
this.OnChauffeurMountReady?.Invoke(text4, result9);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case 4:
|
|
if (text == "CHAT" && parts.Length >= 2)
|
|
{
|
|
string text7 = parts[1];
|
|
log.Information("[CrossProcessIPC] Chat: " + text7);
|
|
this.OnChatMessageReceived?.Invoke(text7);
|
|
}
|
|
break;
|
|
case 7:
|
|
if (text == "COMMAND" && parts.Length >= 2)
|
|
{
|
|
string text10 = parts[1];
|
|
log.Information("[CrossProcessIPC] Command: " + text10);
|
|
this.OnCommandReceived?.Invoke(text10);
|
|
}
|
|
break;
|
|
case 12:
|
|
if (text == "HELPER_READY" && parts.Length >= 3)
|
|
{
|
|
string text13 = parts[1];
|
|
if (ushort.TryParse(parts[2], out var result15))
|
|
{
|
|
log.Information($"[CrossProcessIPC] Helper ready: {text13}@{result15}");
|
|
this.OnHelperReady?.Invoke(text13, result15);
|
|
}
|
|
}
|
|
break;
|
|
case 28:
|
|
if (text == "REQUEST_HELPER_ANNOUNCEMENTS")
|
|
{
|
|
log.Information("[CrossProcessIPC] Request for helper announcements received");
|
|
this.OnRequestHelperAnnouncements?.Invoke();
|
|
}
|
|
break;
|
|
case 17:
|
|
if (text == "CHAUFFEUR_ARRIVED" && parts.Length >= 3)
|
|
{
|
|
string text2 = parts[1];
|
|
if (ushort.TryParse(parts[2], out var result2))
|
|
{
|
|
log.Information($"[CrossProcessIPC] Chauffeur arrived for: {text2}@{result2}");
|
|
this.OnChauffeurArrived?.Invoke(text2, result2);
|
|
}
|
|
}
|
|
break;
|
|
case 27:
|
|
if (text == "CHAUFFEUR_PASSENGER_MOUNTED")
|
|
{
|
|
log.Information("[CrossProcessIPC] Chauffeur passenger mounted signal received");
|
|
IPluginLog pluginLog2 = log;
|
|
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler2 = new DefaultInterpolatedStringHandler(59, 1);
|
|
defaultInterpolatedStringHandler2.AppendLiteral("[CrossProcessIPC] OnChauffeurPassengerMounted subscribers: ");
|
|
Action? action2 = this.OnChauffeurPassengerMounted;
|
|
defaultInterpolatedStringHandler2.AppendFormatted((action2 != null) ? action2.GetInvocationList().Length : 0);
|
|
pluginLog2.Information(defaultInterpolatedStringHandler2.ToStringAndClear());
|
|
this.OnChauffeurPassengerMounted?.Invoke();
|
|
}
|
|
break;
|
|
case 13:
|
|
if (text == "HELPER_STATUS" && parts.Length >= 4)
|
|
{
|
|
string arg = parts[1];
|
|
if (ushort.TryParse(parts[2], out var result))
|
|
{
|
|
string arg2 = parts[3];
|
|
this.OnHelperStatusUpdate?.Invoke(arg, result, arg2);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[CrossProcessIPC] Error in event handler: " + ex2.Message);
|
|
}
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[CrossProcessIPC] Error processing message: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private void SendMessage(string message)
|
|
{
|
|
try
|
|
{
|
|
if (mmf == null)
|
|
{
|
|
return;
|
|
}
|
|
using MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor(0L, 4096L, MemoryMappedFileAccess.Write);
|
|
byte[] buffer = Encoding.UTF8.GetBytes(message);
|
|
if (buffer.Length > 4095)
|
|
{
|
|
log.Warning($"[CrossProcessIPC] Message too large: {buffer.Length} bytes");
|
|
}
|
|
else
|
|
{
|
|
byte[] clearBuffer = new byte[4096];
|
|
accessor.WriteArray(0L, clearBuffer, 0, 4096);
|
|
accessor.WriteArray(0L, buffer, 0, buffer.Length);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[CrossProcessIPC] Failed to send message: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
public void AnnounceHelper()
|
|
{
|
|
if (configuration.IsHighLevelHelper)
|
|
{
|
|
IPlayerCharacter localPlayer = Plugin.ClientState?.LocalPlayer;
|
|
if (localPlayer != null)
|
|
{
|
|
string name = localPlayer.Name.ToString();
|
|
ushort worldId = (ushort)localPlayer.HomeWorld.RowId;
|
|
SendMessage($"HELPER_AVAILABLE|{name}|{worldId}");
|
|
log.Information($"[CrossProcessIPC] Announced as helper: {name}@{worldId}");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void RequestHelper(string characterName, ushort worldId)
|
|
{
|
|
SendMessage($"HELPER_REQUEST|{characterName}|{worldId}");
|
|
log.Information($"[CrossProcessIPC] Requested helper: {characterName}@{worldId}");
|
|
}
|
|
|
|
public void DismissHelper()
|
|
{
|
|
SendMessage("HELPER_DISMISS");
|
|
log.Information("[CrossProcessIPC] Dismissed helper");
|
|
}
|
|
|
|
public void SendChatMessage(string message)
|
|
{
|
|
SendMessage("CHAT|" + message);
|
|
log.Information("[CrossProcessIPC] Chat: " + message);
|
|
}
|
|
|
|
public void SendCommand(string command)
|
|
{
|
|
SendMessage("COMMAND|" + command);
|
|
log.Information("[CrossProcessIPC] Command: " + command);
|
|
}
|
|
|
|
public void NotifyHelperInParty(string name, ushort worldId)
|
|
{
|
|
SendMessage($"HELPER_IN_PARTY|{name}|{worldId}");
|
|
log.Information($"[CrossProcessIPC] Notified: Helper in party {name}@{worldId}");
|
|
}
|
|
|
|
public void NotifyHelperInDuty(string name, ushort worldId)
|
|
{
|
|
SendMessage($"HELPER_IN_DUTY|{name}|{worldId}");
|
|
log.Information($"[CrossProcessIPC] Notified: Helper in duty {name}@{worldId}");
|
|
}
|
|
|
|
public void NotifyHelperReady(string name, ushort worldId)
|
|
{
|
|
SendMessage($"HELPER_READY|{name}|{worldId}");
|
|
log.Information($"[CrossProcessIPC] Notified: Helper ready {name}@{worldId}");
|
|
}
|
|
|
|
public void RequestHelperAnnouncements()
|
|
{
|
|
SendMessage("REQUEST_HELPER_ANNOUNCEMENTS");
|
|
log.Information("[CrossProcessIPC] Requesting helper announcements from all clients");
|
|
}
|
|
|
|
public void SendChauffeurSummonRequest(string questerName, ushort questerWorld, uint zoneId, Vector3 targetPos, Vector3 questerPos, bool isAttuneAetheryte)
|
|
{
|
|
SendMessage($"CHAUFFEUR_SUMMON|{questerName}|{questerWorld}|{zoneId}|{targetPos.X}|{targetPos.Y}|{targetPos.Z}|{questerPos.X}|{questerPos.Y}|{questerPos.Z}|{isAttuneAetheryte}");
|
|
log.Information($"[CrossProcessIPC] Chauffeur summon: {questerName}@{questerWorld} zone {zoneId} quester@({questerPos.X:F2},{questerPos.Y:F2},{questerPos.Z:F2}) AttuneAetheryte={isAttuneAetheryte}");
|
|
}
|
|
|
|
public void SendChauffeurMountReady(string questerName, ushort questerWorld)
|
|
{
|
|
SendMessage($"CHAUFFEUR_MOUNT_READY|{questerName}|{questerWorld}");
|
|
log.Information($"[CrossProcessIPC] Chauffeur mount ready for RidePillion: {questerName}@{questerWorld}");
|
|
}
|
|
|
|
public void SendChauffeurPassengerMounted()
|
|
{
|
|
SendMessage("CHAUFFEUR_PASSENGER_MOUNTED");
|
|
log.Debug("[CrossProcessIPC] Sent: CHAUFFEUR_PASSENGER_MOUNTED");
|
|
}
|
|
|
|
public void SendChauffeurReadyForPickup(string helperName)
|
|
{
|
|
SendMessage("CHAUFFEUR_READY|" + helperName);
|
|
log.Information("[CrossProcessIPC] Chauffeur ready: " + helperName);
|
|
}
|
|
|
|
public void SendChauffeurArrived(string questerName, ushort questerWorld)
|
|
{
|
|
SendMessage($"CHAUFFEUR_ARRIVED|{questerName}|{questerWorld}");
|
|
log.Information($"[CrossProcessIPC] Chauffeur arrived for: {questerName}@{questerWorld}");
|
|
}
|
|
|
|
public void SendChauffeurZoneUpdate(string characterName, ushort worldId, uint zoneId, string zoneName)
|
|
{
|
|
SendMessage($"CHAUFFEUR_ZONE_UPDATE|{characterName}|{worldId}|{zoneId}|{zoneName}");
|
|
log.Information($"[CrossProcessIPC] Zone update: {characterName}@{worldId} -> {zoneName} ({zoneId})");
|
|
}
|
|
|
|
public void BroadcastHelperStatus(string helperName, ushort helperWorld, string status)
|
|
{
|
|
SendMessage($"HELPER_STATUS|{helperName}|{helperWorld}|{status}");
|
|
}
|
|
|
|
public void BroadcastQuesterPosition(string questerName, ushort questerWorld, uint zoneId, Vector3 position)
|
|
{
|
|
SendMessage($"QUESTER_POSITION|{questerName}|{questerWorld}|{zoneId}|{position.X.ToString(CultureInfo.InvariantCulture)}|{position.Y.ToString(CultureInfo.InvariantCulture)}|{position.Z.ToString(CultureInfo.InvariantCulture)}");
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
isRunning = false;
|
|
listenerThread?.Join(1000);
|
|
mmf?.Dispose();
|
|
log.Information("[CrossProcessIPC] Disposed");
|
|
}
|
|
}
|