2902 lines
99 KiB
C#
2902 lines
99 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Dalamud.Game.ClientState.Conditions;
|
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
|
using Dalamud.Game.ClientState.Objects.Types;
|
|
using Dalamud.Game.ClientState.Party;
|
|
using Dalamud.Plugin;
|
|
using Dalamud.Plugin.Services;
|
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|
using FFXIVClientStructs.FFXIV.Client.Game.Control;
|
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
|
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
|
using Lumina.Excel;
|
|
using Lumina.Excel.Sheets;
|
|
using Newtonsoft.Json.Linq;
|
|
using QuestionableCompanion;
|
|
using QuestionableCompanion.Services;
|
|
|
|
public class ChauffeurModeService : IDisposable
|
|
{
|
|
private readonly Configuration config;
|
|
|
|
private readonly IPluginLog log;
|
|
|
|
private readonly IClientState clientState;
|
|
|
|
private readonly ICondition condition;
|
|
|
|
private readonly IFramework framework;
|
|
|
|
private readonly ICommandManager commandManager;
|
|
|
|
private readonly IDataManager dataManager;
|
|
|
|
private readonly IPartyList partyList;
|
|
|
|
private readonly IObjectTable objectTable;
|
|
|
|
private readonly QuestionableIPC questionableIPC;
|
|
|
|
private readonly CrossProcessIPC crossProcessIPC;
|
|
|
|
private readonly PartyInviteService partyInviteService;
|
|
|
|
private readonly PartyInviteAutoAccept partyInviteAutoAccept;
|
|
|
|
private readonly IDalamudPluginInterface pluginInterface;
|
|
|
|
private readonly MemoryHelper memoryHelper;
|
|
|
|
private readonly MovementMonitorService? movementMonitor;
|
|
|
|
private readonly VNavmeshIPC vnavmeshIPC;
|
|
|
|
private bool isWaitingForHelper;
|
|
|
|
private bool isTransportingQuester;
|
|
|
|
private bool hasExecutedRidePillion;
|
|
|
|
private Vector3? targetPosition;
|
|
|
|
private uint targetZoneId;
|
|
|
|
private string? questerName;
|
|
|
|
private DateTime lastZoneUpdate = DateTime.MinValue;
|
|
|
|
private bool isDisposed;
|
|
|
|
private CancellationTokenSource? helperWorkflowCts;
|
|
|
|
private Vector3? lastMoveToPosition;
|
|
|
|
private DateTime? lastZoneChangeTime;
|
|
|
|
private bool isFollowingQuester;
|
|
|
|
private DateTime lastFollowCheck = DateTime.MinValue;
|
|
|
|
private Vector3? lastQuesterPosition;
|
|
|
|
private uint lastQuesterZone;
|
|
|
|
private string? followingQuesterName;
|
|
|
|
private static readonly HashSet<uint> BLACKLISTED_ZONES = new HashSet<uint> { 478u };
|
|
|
|
private static readonly Dictionary<uint, uint> FLYING_INDICATOR_QUESTS = new Dictionary<uint, uint> { { 1669u, 402u } };
|
|
|
|
private Dictionary<string, string> helperStatuses = new Dictionary<string, string>();
|
|
|
|
private Dictionary<string, DateTime> discoveredQuesters = new Dictionary<string, DateTime>();
|
|
|
|
private readonly HashSet<uint> restrictedZones = new HashSet<uint>
|
|
{
|
|
128u, 129u, 130u, 131u, 132u, 133u, 418u, 419u, 819u, 820u,
|
|
962u, 963u, 1185u, 1186u, 250u
|
|
};
|
|
|
|
public bool IsWaitingForHelper => isWaitingForHelper;
|
|
|
|
public bool IsTransportingQuester => isTransportingQuester;
|
|
|
|
public string? GetHelperStatus(string helperKey)
|
|
{
|
|
if (!helperStatuses.TryGetValue(helperKey, out string status))
|
|
{
|
|
return null;
|
|
}
|
|
return status;
|
|
}
|
|
|
|
public List<string> GetDiscoveredQuesters()
|
|
{
|
|
DateTime now = DateTime.Now;
|
|
foreach (string stale in (from kvp in discoveredQuesters
|
|
where (now - kvp.Value).TotalSeconds > 60.0
|
|
select kvp.Key).ToList())
|
|
{
|
|
discoveredQuesters.Remove(stale);
|
|
}
|
|
return discoveredQuesters.Keys.ToList();
|
|
}
|
|
|
|
public ChauffeurModeService(Configuration config, IPluginLog log, IClientState clientState, ICondition condition, IFramework framework, ICommandManager commandManager, IDataManager dataManager, IPartyList partyList, IObjectTable objectTable, QuestionableIPC questionableIPC, CrossProcessIPC crossProcessIPC, PartyInviteService partyInviteService, PartyInviteAutoAccept partyInviteAutoAccept, IDalamudPluginInterface pluginInterface, MemoryHelper memoryHelper, MovementMonitorService? movementMonitor = null)
|
|
{
|
|
this.config = config;
|
|
this.log = log;
|
|
this.clientState = clientState;
|
|
this.condition = condition;
|
|
this.framework = framework;
|
|
this.commandManager = commandManager;
|
|
this.dataManager = dataManager;
|
|
this.partyList = partyList;
|
|
this.objectTable = objectTable;
|
|
this.questionableIPC = questionableIPC;
|
|
this.crossProcessIPC = crossProcessIPC;
|
|
this.partyInviteService = partyInviteService;
|
|
this.partyInviteAutoAccept = partyInviteAutoAccept;
|
|
this.pluginInterface = pluginInterface;
|
|
this.memoryHelper = memoryHelper;
|
|
this.movementMonitor = movementMonitor;
|
|
vnavmeshIPC = new VNavmeshIPC(pluginInterface);
|
|
crossProcessIPC.OnChauffeurSummonRequest += OnChauffeurSummonRequest;
|
|
crossProcessIPC.OnChauffeurReadyForPickup += OnChauffeurReadyForPickup;
|
|
crossProcessIPC.OnChauffeurArrived += OnChauffeurArrived;
|
|
crossProcessIPC.OnChauffeurZoneUpdate += OnChauffeurZoneUpdate;
|
|
crossProcessIPC.OnChauffeurMountReady += OnChauffeurMountReady;
|
|
crossProcessIPC.OnChauffeurPassengerMounted += OnChauffeurPassengerMounted;
|
|
crossProcessIPC.OnHelperStatusUpdate += OnHelperStatusUpdate;
|
|
crossProcessIPC.OnQuesterPositionUpdate += OnQuesterPositionUpdate;
|
|
clientState.TerritoryChanged += OnTerritoryChanged;
|
|
if (config.IsHighLevelHelper)
|
|
{
|
|
framework.RunOnTick(delegate
|
|
{
|
|
BroadcastHelperStatusPeriodically();
|
|
}, TimeSpan.FromSeconds(10L));
|
|
log.Information("[ChauffeurMode] Periodic helper status broadcast enabled (every 10s)");
|
|
}
|
|
framework.Update += OnFrameworkUpdate;
|
|
log.Information("[ChauffeurMode] Service initialized");
|
|
}
|
|
|
|
private void OnFrameworkUpdate(IFramework framework)
|
|
{
|
|
if (config.IsHighLevelHelper && config.EnableHelperFollowing && (DateTime.Now - lastFollowCheck).TotalSeconds >= (double)config.HelperFollowCheckInterval)
|
|
{
|
|
CheckHelperFollowing();
|
|
}
|
|
if (config.IsQuester && !string.IsNullOrEmpty(config.AssignedHelperForFollowing) && config.EnableHelperFollowing)
|
|
{
|
|
DateTime now = DateTime.Now;
|
|
if ((now - lastFollowCheck).TotalSeconds >= 5.0)
|
|
{
|
|
BroadcastQuesterPosition();
|
|
lastFollowCheck = now;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void CheckWaitTerritoryTask()
|
|
{
|
|
if (!questionableIPC.IsRunning())
|
|
{
|
|
return;
|
|
}
|
|
if (lastZoneChangeTime.HasValue)
|
|
{
|
|
double timeSinceZoneChange = (DateTime.Now - lastZoneChangeTime.Value).TotalSeconds;
|
|
if (timeSinceZoneChange < 8.0)
|
|
{
|
|
log.Debug($"[WaitTerritory] Territory Load State: Waiting for zone load before checking Wait tasks (elapsed: {timeSinceZoneChange:F1}s / 8.0s)");
|
|
return;
|
|
}
|
|
}
|
|
if (clientState.LocalPlayer == null)
|
|
{
|
|
return;
|
|
}
|
|
object task = questionableIPC.GetCurrentTask();
|
|
if (task == null)
|
|
{
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
if (!(task is JObject jObject))
|
|
{
|
|
return;
|
|
}
|
|
JToken taskNameToken = jObject["TaskName"];
|
|
if (taskNameToken == null)
|
|
{
|
|
return;
|
|
}
|
|
string taskName = taskNameToken.ToString();
|
|
if (string.IsNullOrEmpty(taskName))
|
|
{
|
|
return;
|
|
}
|
|
Match waitTerritoryMatch = new Regex("Wait\\(territory:\\s*(.+?)\\s*\\((\\d+)\\)\\)").Match(taskName);
|
|
if (!waitTerritoryMatch.Success)
|
|
{
|
|
return;
|
|
}
|
|
string territoryName = waitTerritoryMatch.Groups[1].Value.Trim();
|
|
uint territoryId = uint.Parse(waitTerritoryMatch.Groups[2].Value);
|
|
if (clientState.TerritoryType == territoryId)
|
|
{
|
|
log.Debug($"[WaitTerritory] Already in target territory {territoryName} ({territoryId}) - skipping teleport");
|
|
return;
|
|
}
|
|
string mappedName = MapTerritoryName(territoryName);
|
|
log.Information($"[WaitTerritory] Wait(territory) detected: {territoryName} ({territoryId}) → Auto-teleporting to {mappedName}");
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
string content = "/li " + mappedName;
|
|
commandManager.ProcessCommand(content);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[WaitTerritory] Failed to teleport to " + mappedName + ": " + ex2.Message);
|
|
}
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[WaitTerritory] Error checking Wait(territory) task: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
public void CheckTaskDistance()
|
|
{
|
|
if (!config.ChauffeurModeEnabled || !config.IsQuester || !questionableIPC.IsAvailable || !questionableIPC.IsRunning())
|
|
{
|
|
return;
|
|
}
|
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
|
if (localPlayer == null)
|
|
{
|
|
return;
|
|
}
|
|
if (lastZoneChangeTime.HasValue)
|
|
{
|
|
double timeSinceZoneChange = (DateTime.Now - lastZoneChangeTime.Value).TotalSeconds;
|
|
if (timeSinceZoneChange < 8.0)
|
|
{
|
|
log.Debug($"[ChauffeurMode] Territory Load State: Waiting for zone load before checking summon (elapsed: {timeSinceZoneChange:F1}s / 8.0s)");
|
|
return;
|
|
}
|
|
}
|
|
ushort currentZoneId = clientState.TerritoryType;
|
|
if (BLACKLISTED_ZONES.Contains(currentZoneId))
|
|
{
|
|
log.Debug($"[ChauffeurMode] Zone {currentZoneId} is blacklisted (no flying), cannot use Chauffeur Mode");
|
|
return;
|
|
}
|
|
if (IsRestrictedZone(currentZoneId))
|
|
{
|
|
log.Debug($"[ChauffeurMode] Zone {currentZoneId} is restricted (Main City), cannot use Chauffeur Mode");
|
|
return;
|
|
}
|
|
if (IsSoloDutyOrInstance(currentZoneId))
|
|
{
|
|
log.Debug($"[ChauffeurMode] Zone {currentZoneId} is a Solo Duty/Instance, cannot use Chauffeur Mode");
|
|
return;
|
|
}
|
|
if (!IsMountingAllowed(currentZoneId))
|
|
{
|
|
log.Debug($"[ChauffeurMode] Zone {currentZoneId} does not allow mounting, cannot use Chauffeur Mode");
|
|
return;
|
|
}
|
|
if (HasFlyingInZone(currentZoneId))
|
|
{
|
|
log.Debug($"[ChauffeurMode] Flying already unlocked in zone {currentZoneId}, no helper needed");
|
|
return;
|
|
}
|
|
string currentQuestStr = questionableIPC.GetCurrentQuestId();
|
|
if (!string.IsNullOrEmpty(currentQuestStr) && uint.TryParse(currentQuestStr, out var currentQuestId) && FLYING_INDICATOR_QUESTS.TryGetValue(currentQuestId, out var flyingZoneId) && flyingZoneId == currentZoneId)
|
|
{
|
|
log.Debug($"[ChauffeurMode] Current quest {currentQuestId} indicates flying is already unlocked in zone {currentZoneId} - no helper needed");
|
|
return;
|
|
}
|
|
object task = questionableIPC.GetCurrentTask();
|
|
if (task == null)
|
|
{
|
|
log.Debug("[ChauffeurMode] No current task");
|
|
return;
|
|
}
|
|
var (taskPosition, isAttuneAetheryte) = ParseTaskPositionWithType(task);
|
|
if (!taskPosition.HasValue)
|
|
{
|
|
log.Debug("[ChauffeurMode] Could not parse task position");
|
|
return;
|
|
}
|
|
Vector3 currentPosition = localPlayer.Position;
|
|
float distance = Vector3.Distance(currentPosition, taskPosition.Value);
|
|
float threshold = (isAttuneAetheryte ? 10f : config.ChauffeurDistanceThreshold);
|
|
log.Information($"[ChauffeurMode] Current Position: ({currentPosition.X:F2}, {currentPosition.Y:F2}, {currentPosition.Z:F2})");
|
|
log.Information($"[ChauffeurMode] Target Position: ({taskPosition.Value.X:F2}, {taskPosition.Value.Y:F2}, {taskPosition.Value.Z:F2})");
|
|
log.Information($"[ChauffeurMode] Distance to task: {distance:F2} yalms (threshold: {threshold})");
|
|
if (distance > threshold)
|
|
{
|
|
log.Information($"[ChauffeurMode] Task distance ({distance:F2} yalms) exceeds threshold, checking combat status");
|
|
if (condition[ConditionFlag.InCombat])
|
|
{
|
|
log.Information("[ChauffeurMode] Player is in combat - waiting for combat to end before summoning helper");
|
|
return;
|
|
}
|
|
log.Information("[ChauffeurMode] Not in combat - summoning helper");
|
|
SummonHelper(taskPosition.Value, currentZoneId);
|
|
}
|
|
else
|
|
{
|
|
log.Debug($"[ChauffeurMode] Task is close enough ({distance:F2} yalms), no helper needed");
|
|
}
|
|
}
|
|
|
|
private (Vector3? position, bool isAttuneAetheryte) ParseTaskPositionWithType(object task)
|
|
{
|
|
try
|
|
{
|
|
if (task is JObject jObject)
|
|
{
|
|
JToken taskNameToken = jObject["TaskName"];
|
|
if (taskNameToken == null)
|
|
{
|
|
log.Warning("[ChauffeurMode] Task has no TaskName property");
|
|
return (position: null, isAttuneAetheryte: false);
|
|
}
|
|
string taskName = taskNameToken.ToString();
|
|
if (string.IsNullOrEmpty(taskName))
|
|
{
|
|
log.Warning("[ChauffeurMode] TaskName is empty");
|
|
return (position: null, isAttuneAetheryte: false);
|
|
}
|
|
bool isAttuneAetheryte = taskName.StartsWith("AttuneAetheryte(");
|
|
return (position: ParseTaskName(taskName), isAttuneAetheryte: isAttuneAetheryte);
|
|
}
|
|
log.Warning("[ChauffeurMode] Task is not a JObject");
|
|
return (position: null, isAttuneAetheryte: false);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[ChauffeurMode] Error parsing task position: " + ex.Message);
|
|
return (position: null, isAttuneAetheryte: false);
|
|
}
|
|
}
|
|
|
|
private Vector3? ParseTaskPosition(object task)
|
|
{
|
|
return ParseTaskPositionWithType(task).position;
|
|
}
|
|
|
|
private string MapTerritoryName(string territoryName)
|
|
{
|
|
if (territoryName.Contains("Dravanian Hinterlands", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
log.Information("[ChauffeurMode] Mapping 'Dravanian Hinterlands' → 'Epilogue Gate'");
|
|
return "Epilogue Gate";
|
|
}
|
|
if (territoryName.Contains("Old Gridania", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
log.Information("[ChauffeurMode] Mapping 'Old Gridania' → 'Mih Khetto'");
|
|
return "Mih Khetto";
|
|
}
|
|
if (territoryName.Contains("Upper Decks", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
log.Information("[ChauffeurMode] Mapping 'Upper Decks' → 'Aftcastle'");
|
|
return "Aftcastle";
|
|
}
|
|
if (territoryName.Contains("Coerthas Central Highlands", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
log.Information("[ChauffeurMode] Mapping 'Coerthas Central Highlands' → 'Camp Dragonhead'");
|
|
return "Camp Dragonhead";
|
|
}
|
|
if (territoryName.Contains("The Pillars", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
log.Information("[ChauffeurMode] Mapping 'The Pillars' → 'The Last Vigil'");
|
|
return "The Last Vigil";
|
|
}
|
|
return territoryName;
|
|
}
|
|
|
|
private Vector3? ParseTaskName(string taskName)
|
|
{
|
|
try
|
|
{
|
|
log.Debug("[ChauffeurMode] Parsing task: " + taskName);
|
|
Match waitTerritoryMatch = new Regex("Wait\\(territory:\\s*(.+?)\\s*\\((\\d+)\\)\\)").Match(taskName);
|
|
if (waitTerritoryMatch.Success)
|
|
{
|
|
string territoryName = waitTerritoryMatch.Groups[1].Value.Trim();
|
|
uint territoryId = uint.Parse(waitTerritoryMatch.Groups[2].Value);
|
|
log.Information("[ChauffeurMode] ========================================");
|
|
log.Information("[ChauffeurMode] === WAIT(TERRITORY) TASK DETECTED ===");
|
|
log.Information("[ChauffeurMode] ========================================");
|
|
log.Information($"[ChauffeurMode] Territory: {territoryName} (ID: {territoryId})");
|
|
log.Information("[ChauffeurMode] This means Questionable has no aetheryte shortcut!");
|
|
log.Information("[ChauffeurMode] Auto-teleporting via Lifestream...");
|
|
string mappedName = MapTerritoryName(territoryName);
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
string text = "/li " + mappedName;
|
|
commandManager.ProcessCommand(text);
|
|
log.Information("[ChauffeurMode] ✓ Teleport command sent: " + text);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] Failed to send teleport command: " + ex2.Message);
|
|
}
|
|
});
|
|
return null;
|
|
}
|
|
Match moveToMatch = new Regex("MoveTo\\(<([-\\d.]+),\\s*([-\\d.]+),\\s*([-\\d.]+)>\\)").Match(taskName);
|
|
if (moveToMatch.Success)
|
|
{
|
|
float x = float.Parse(moveToMatch.Groups[1].Value, CultureInfo.InvariantCulture);
|
|
float y = float.Parse(moveToMatch.Groups[2].Value, CultureInfo.InvariantCulture);
|
|
float z = float.Parse(moveToMatch.Groups[3].Value, CultureInfo.InvariantCulture);
|
|
Vector3 position = new Vector3(x, y, z);
|
|
lastMoveToPosition = position;
|
|
log.Debug($"[ChauffeurMode] Parsed MoveTo position: ({x:F2}, {y:F2}, {z:F2})");
|
|
return position;
|
|
}
|
|
Match attuneMatch = new Regex("AttuneAetheryte\\((.+?)\\)").Match(taskName);
|
|
if (attuneMatch.Success)
|
|
{
|
|
string aetheryteName = attuneMatch.Groups[1].Value;
|
|
log.Information("[ChauffeurMode] ========================================");
|
|
log.Information("[ChauffeurMode] === ATTUNE AETHERYTE TASK DETECTED ===");
|
|
log.Information("[ChauffeurMode] ========================================");
|
|
log.Information("[ChauffeurMode] Aetheryte: " + aetheryteName);
|
|
if (lastMoveToPosition.HasValue)
|
|
{
|
|
log.Information($"[ChauffeurMode] Using last MoveTo position: ({lastMoveToPosition.Value.X:F2}, {lastMoveToPosition.Value.Y:F2}, {lastMoveToPosition.Value.Z:F2})");
|
|
log.Information("[ChauffeurMode] Using reduced threshold of 10 yalms for AttuneAetheryte");
|
|
return lastMoveToPosition;
|
|
}
|
|
log.Warning("[ChauffeurMode] No previous MoveTo position found for AttuneAetheryte task");
|
|
return null;
|
|
}
|
|
Match objectIdMatch = new Regex("(?:Interact|Talk)\\((\\d+)\\)").Match(taskName);
|
|
if (objectIdMatch.Success)
|
|
{
|
|
uint objectId = uint.Parse(objectIdMatch.Groups[1].Value);
|
|
log.Debug($"[ChauffeurMode] Parsed ObjectId: {objectId}");
|
|
Vector3? position2 = GetObjectPosition(objectId);
|
|
if (position2.HasValue)
|
|
{
|
|
log.Debug($"[ChauffeurMode] Found object position: ({position2.Value.X}, {position2.Value.Y}, {position2.Value.Z})");
|
|
return position2;
|
|
}
|
|
}
|
|
log.Warning("[ChauffeurMode] Could not parse task format: " + taskName);
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[ChauffeurMode] Error parsing task position: " + ex.Message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private Vector3? GetObjectPosition(uint objectId)
|
|
{
|
|
try
|
|
{
|
|
ExcelSheet<EObj> eObjSheet = dataManager.GetExcelSheet<EObj>();
|
|
if (eObjSheet != null && eObjSheet.GetRowOrDefault(objectId).HasValue)
|
|
{
|
|
log.Debug($"[ChauffeurMode] Found EObj: {objectId}");
|
|
}
|
|
ExcelSheet<Level> levelSheet = dataManager.GetExcelSheet<Level>();
|
|
if (levelSheet != null)
|
|
{
|
|
foreach (Level level in levelSheet)
|
|
{
|
|
if (level.Object.RowId == objectId)
|
|
{
|
|
Vector3 position = new Vector3(level.X, level.Y, level.Z);
|
|
log.Debug($"[ChauffeurMode] Found Level position for object {objectId}: ({position.X}, {position.Y}, {position.Z})");
|
|
return position;
|
|
}
|
|
}
|
|
}
|
|
log.Warning($"[ChauffeurMode] Could not find position for object {objectId}");
|
|
return null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[ChauffeurMode] Error getting object position: " + ex.Message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private unsafe bool HasFlyingInZone(uint zoneId)
|
|
{
|
|
try
|
|
{
|
|
ExcelSheet<TerritoryType> territorySheet = dataManager.GetExcelSheet<TerritoryType>();
|
|
if (territorySheet == null)
|
|
{
|
|
log.Debug("[ChauffeurMode] TerritoryType sheet is null");
|
|
return false;
|
|
}
|
|
TerritoryType? territory = territorySheet.GetRowOrDefault(zoneId);
|
|
if (!territory.HasValue)
|
|
{
|
|
log.Debug($"[ChauffeurMode] Territory {zoneId} not found");
|
|
return false;
|
|
}
|
|
if (!territory.Value.Mount)
|
|
{
|
|
log.Debug($"[ChauffeurMode] Zone {zoneId} does not allow mounting");
|
|
return false;
|
|
}
|
|
RowRef<AetherCurrentCompFlgSet> aetherCurrentCompFlgSet = territory.Value.AetherCurrentCompFlgSet;
|
|
if (!aetherCurrentCompFlgSet.IsValid || aetherCurrentCompFlgSet.RowId == 0)
|
|
{
|
|
log.Debug($"[ChauffeurMode] Zone {zoneId} has no aether currents (AetherCurrentCompFlgSet invalid or 0)");
|
|
return false;
|
|
}
|
|
PlayerState* playerState = PlayerState.Instance();
|
|
if (playerState == null)
|
|
{
|
|
log.Debug("[ChauffeurMode] PlayerState is null");
|
|
return false;
|
|
}
|
|
byte aetherCurrentId = (byte)aetherCurrentCompFlgSet.RowId;
|
|
bool hasFlying = playerState->IsAetherCurrentZoneComplete(aetherCurrentId);
|
|
log.Debug($"[ChauffeurMode] Zone {zoneId} (AetherCurrentId: {aetherCurrentId}) flying check: {hasFlying}");
|
|
return hasFlying;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[ChauffeurMode] Error checking flying availability: " + ex.Message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async void SummonHelper(Vector3 targetPos, uint zoneId)
|
|
{
|
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
|
if (localPlayer == null)
|
|
{
|
|
return;
|
|
}
|
|
if (condition[ConditionFlag.InCombat])
|
|
{
|
|
log.Warning("[ChauffeurMode] Still in combat - cannot summon helper yet");
|
|
return;
|
|
}
|
|
log.Information("[ChauffeurMode] ========================================");
|
|
log.Information("[ChauffeurMode] === SUMMONING HELPER ===");
|
|
log.Information("[ChauffeurMode] ========================================");
|
|
if (!string.IsNullOrEmpty(config.PreferredHelper))
|
|
{
|
|
string preferredHelper = config.PreferredHelper;
|
|
log.Information("[ChauffeurMode] [QUESTER] Preferred Helper: " + preferredHelper);
|
|
if (!helperStatuses.TryGetValue(preferredHelper, out string status))
|
|
{
|
|
log.Warning("[ChauffeurMode] [QUESTER] No status received from preferred helper yet - walking to destination");
|
|
return;
|
|
}
|
|
log.Information("[ChauffeurMode] [QUESTER] Helper status: " + status);
|
|
if (status != "Available")
|
|
{
|
|
log.Warning("[ChauffeurMode] [QUESTER] Preferred helper is " + status + " - walking to destination instead");
|
|
log.Warning("[ChauffeurMode] [QUESTER] Continuing quest without helper");
|
|
return;
|
|
}
|
|
}
|
|
log.Information("[ChauffeurMode] Stopping Questionable to wait for helper");
|
|
if (movementMonitor != null && movementMonitor.IsMonitoring)
|
|
{
|
|
log.Information("[ChauffeurMode] [QUESTER] Stopping Movement Monitor during transport");
|
|
movementMonitor.StopMonitoring();
|
|
}
|
|
log.Information("[ChauffeurMode] Stopping Questionable to wait for helper");
|
|
TaskCompletionSource<bool> stopTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
commandManager.ProcessCommand("/qst stop");
|
|
stopTask.SetResult(result: true);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] Error stopping Questionable: " + ex2.Message);
|
|
stopTask.SetResult(result: false);
|
|
}
|
|
});
|
|
await stopTask.Task;
|
|
log.Information("[ChauffeurMode] [QUESTER] Enabling auto-accept for party invites");
|
|
log.Information($"[ChauffeurMode] [QUESTER] Current role - IsQuester: {config.IsQuester}, IsHelper: {config.IsHighLevelHelper}");
|
|
partyInviteAutoAccept.EnableAutoAccept();
|
|
log.Information("[ChauffeurMode] [QUESTER] Auto-accept enabled - will accept invites for 30 seconds");
|
|
string questerName = localPlayer.Name.ToString();
|
|
ushort questerWorld = (ushort)localPlayer.HomeWorld.RowId;
|
|
targetPosition = targetPos;
|
|
targetZoneId = zoneId;
|
|
isWaitingForHelper = true;
|
|
Vector3 questerPos = localPlayer.Position;
|
|
bool isAttuneAetheryte = false;
|
|
try
|
|
{
|
|
StepData stepData = questionableIPC.GetCurrentStepData();
|
|
if (stepData != null && stepData.InteractionType == "AttuneAetheryte")
|
|
{
|
|
isAttuneAetheryte = true;
|
|
log.Information("[ChauffeurMode] Current step is AttuneAetheryte - Helper will find landable spot");
|
|
}
|
|
else
|
|
{
|
|
log.Information("[ChauffeurMode] Current step InteractionType: " + (stepData?.InteractionType ?? "null") + " - Helper will go to exact position");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Warning("[ChauffeurMode] Failed to get step data: " + ex.Message);
|
|
}
|
|
log.Information("[ChauffeurMode] Requesting helper pickup");
|
|
log.Information($"[ChauffeurMode] Quester: {questerName}@{questerWorld}");
|
|
log.Information($"[ChauffeurMode] Zone: {zoneId}");
|
|
log.Information($"[ChauffeurMode] Quester Position: ({questerPos.X:F2}, {questerPos.Y:F2}, {questerPos.Z:F2})");
|
|
log.Information($"[ChauffeurMode] Target: ({targetPos.X:F2}, {targetPos.Y:F2}, {targetPos.Z:F2})");
|
|
log.Information($"[ChauffeurMode] AttuneAetheryte: {isAttuneAetheryte}");
|
|
crossProcessIPC.SendChauffeurSummonRequest(questerName, questerWorld, zoneId, targetPos, questerPos, isAttuneAetheryte);
|
|
}
|
|
|
|
public bool IsRestrictedZone(uint zoneId)
|
|
{
|
|
return restrictedZones.Contains(zoneId);
|
|
}
|
|
|
|
public bool IsSoloDutyOrInstance(uint zoneId)
|
|
{
|
|
try
|
|
{
|
|
ExcelSheet<TerritoryType> territorySheet = dataManager.GetExcelSheet<TerritoryType>();
|
|
if (territorySheet == null)
|
|
{
|
|
return false;
|
|
}
|
|
TerritoryType? territory = territorySheet.GetRowOrDefault(zoneId);
|
|
if (!territory.HasValue)
|
|
{
|
|
return false;
|
|
}
|
|
uint intendedUse = territory.Value.TerritoryIntendedUse.RowId;
|
|
switch (intendedUse)
|
|
{
|
|
case 8u:
|
|
case 9u:
|
|
log.Debug($"[ChauffeurMode] Zone {zoneId} is Solo Duty/Quest Battle (IntendedUse: {intendedUse})");
|
|
return true;
|
|
case 2u:
|
|
case 3u:
|
|
case 4u:
|
|
case 5u:
|
|
log.Debug($"[ChauffeurMode] Zone {zoneId} is party content (IntendedUse: {intendedUse})");
|
|
return true;
|
|
default:
|
|
if (intendedUse == 13 || intendedUse == 16 || intendedUse == 17)
|
|
{
|
|
log.Debug($"[ChauffeurMode] Zone {zoneId} is special content (IntendedUse: {intendedUse})");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[ChauffeurMode] Error checking solo duty status: " + ex.Message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public bool IsMountingAllowed(uint zoneId)
|
|
{
|
|
try
|
|
{
|
|
ExcelSheet<TerritoryType> territorySheet = dataManager.GetExcelSheet<TerritoryType>();
|
|
if (territorySheet == null)
|
|
{
|
|
return false;
|
|
}
|
|
TerritoryType? territory = territorySheet.GetRowOrDefault(zoneId);
|
|
if (!territory.HasValue)
|
|
{
|
|
return false;
|
|
}
|
|
return territory.Value.Mount;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[ChauffeurMode] Error checking mount permission: " + ex.Message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public List<(uint Id, string Name, byte Seats)> GetMultiSeaterMounts()
|
|
{
|
|
List<(uint, string, byte)> mounts = new List<(uint, string, byte)>();
|
|
try
|
|
{
|
|
ExcelSheet<Mount> mountSheet = dataManager.GetExcelSheet<Mount>();
|
|
if (mountSheet == null)
|
|
{
|
|
log.Error("[ChauffeurMode] Could not load Mount sheet");
|
|
return mounts;
|
|
}
|
|
foreach (Mount mount in mountSheet)
|
|
{
|
|
if (mount.ExtraSeats > 0)
|
|
{
|
|
string name = mount.Singular.ToString();
|
|
if (!string.IsNullOrEmpty(name))
|
|
{
|
|
mounts.Add((mount.RowId, name, mount.ExtraSeats));
|
|
}
|
|
}
|
|
}
|
|
log.Debug($"[ChauffeurMode] Found {mounts.Count} multi-seater mounts");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[ChauffeurMode] Error loading multi-seater mounts: " + ex.Message);
|
|
}
|
|
return mounts;
|
|
}
|
|
|
|
private void OnChauffeurSummonRequest(string questerName, ushort questerWorld, uint zoneId, Vector3 targetPos, Vector3 questerPos, bool isAttuneAetheryte)
|
|
{
|
|
if (!config.ChauffeurModeEnabled)
|
|
{
|
|
return;
|
|
}
|
|
if (!config.IsHighLevelHelper)
|
|
{
|
|
log.Debug("[ChauffeurMode] Not a helper, ignoring summon");
|
|
return;
|
|
}
|
|
if (config.CurrentHelperStatus == HelperStatus.Transporting)
|
|
{
|
|
log.Warning($"[ChauffeurMode] [HELPER] Already transporting {config.AssignedQuester} - rejecting summon from {questerName}@{questerWorld}");
|
|
return;
|
|
}
|
|
if (config.CurrentHelperStatus == HelperStatus.InDungeon)
|
|
{
|
|
log.Warning($"[ChauffeurMode] [HELPER] Currently in dungeon - rejecting summon from {questerName}@{questerWorld}");
|
|
return;
|
|
}
|
|
this.questerName = $"{questerName}@{questerWorld}";
|
|
targetZoneId = zoneId;
|
|
targetPosition = targetPos;
|
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
|
if (localPlayer != null)
|
|
{
|
|
string myName = localPlayer.Name.ToString();
|
|
ushort myWorld = (ushort)localPlayer.HomeWorld.RowId;
|
|
if (myName == questerName && myWorld == questerWorld)
|
|
{
|
|
log.Debug("[ChauffeurMode] Ignoring own summon request");
|
|
return;
|
|
}
|
|
}
|
|
log.Information("[ChauffeurMode] ========================================");
|
|
log.Information("[ChauffeurMode] === HELPER SUMMON REQUEST ===");
|
|
log.Information("[ChauffeurMode] ========================================");
|
|
log.Information($"[ChauffeurMode] Quester: {questerName}@{questerWorld}");
|
|
log.Information($"[ChauffeurMode] Zone: {zoneId}");
|
|
log.Information($"[ChauffeurMode] Target: ({targetPos.X:F2}, {targetPos.Y:F2}, {targetPos.Z:F2})");
|
|
log.Information($"[ChauffeurMode] Quester Position: ({questerPos.X:F2}, {questerPos.Y:F2}, {questerPos.Z:F2})");
|
|
if (BLACKLISTED_ZONES.Contains(zoneId))
|
|
{
|
|
log.Warning($"[ChauffeurMode] Zone {zoneId} is blacklisted (no flying available), cannot use Chauffeur Mode");
|
|
return;
|
|
}
|
|
if (IsRestrictedZone(zoneId))
|
|
{
|
|
log.Warning($"[ChauffeurMode] Zone {zoneId} is restricted (Main City), cannot follow");
|
|
return;
|
|
}
|
|
if (IsSoloDutyOrInstance(zoneId))
|
|
{
|
|
log.Warning($"[ChauffeurMode] Zone {zoneId} is a Solo Duty/Instance, cannot follow");
|
|
return;
|
|
}
|
|
if (!IsMountingAllowed(zoneId))
|
|
{
|
|
log.Warning($"[ChauffeurMode] Zone {zoneId} does not allow mounting, cannot use Chauffeur Mode");
|
|
return;
|
|
}
|
|
if (config.ChauffeurMountId == 0)
|
|
{
|
|
log.Error("[ChauffeurMode] No mount configured! Please select a multi-seater mount in settings");
|
|
return;
|
|
}
|
|
if (isTransportingQuester)
|
|
{
|
|
log.Warning($"[ChauffeurMode] [HELPER] Already transporting a quester! Ignoring new request from {questerName}@{questerWorld}");
|
|
return;
|
|
}
|
|
this.questerName = questerName;
|
|
targetPosition = targetPos;
|
|
targetZoneId = zoneId;
|
|
isTransportingQuester = true;
|
|
config.AssignedQuester = $"{questerName}@{questerWorld}";
|
|
config.CurrentHelperStatus = HelperStatus.Transporting;
|
|
config.Save();
|
|
log.Information("[ChauffeurMode] [HELPER] Assigned to quester: " + config.AssignedQuester + " (Status: Transporting)");
|
|
if (localPlayer != null)
|
|
{
|
|
string helperName = localPlayer.Name.ToString();
|
|
ushort helperWorld = (ushort)localPlayer.HomeWorld.RowId;
|
|
crossProcessIPC.BroadcastHelperStatus(helperName, helperWorld, "Transporting");
|
|
}
|
|
helperWorkflowCts?.Cancel();
|
|
helperWorkflowCts?.Dispose();
|
|
helperWorkflowCts = new CancellationTokenSource();
|
|
CancellationTokenSource cts = helperWorkflowCts;
|
|
Task.Run(async delegate
|
|
{
|
|
await HelperWorkflow(questerName, questerWorld, zoneId, targetPos, questerPos, isAttuneAetheryte, cts.Token);
|
|
}, cts.Token);
|
|
}
|
|
|
|
private unsafe async Task HelperWorkflow(string questerName, ushort questerWorld, uint zoneId, Vector3 targetPos, Vector3 questerPos, bool isAttuneAetheryte, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Starting helper workflow");
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Thread ID: {Thread.CurrentThread.ManagedThreadId}");
|
|
if (cancellationToken.IsCancellationRequested)
|
|
{
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Workflow cancelled before start");
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
ResetHelperTransportState();
|
|
});
|
|
return;
|
|
}
|
|
TaskCompletionSource<(bool success, ushort helperWorld, uint helperZone, uint questerZone)> worldCheckTask = new TaskCompletionSource<(bool, ushort, uint, uint)>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
IPlayerCharacter localPlayer2 = clientState.LocalPlayer;
|
|
if (localPlayer2 == null)
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] LocalPlayer is null!");
|
|
worldCheckTask.SetResult((false, 0, 0u, 0u));
|
|
}
|
|
else
|
|
{
|
|
ushort item = (ushort)localPlayer2.CurrentWorld.RowId;
|
|
ushort territoryType = clientState.TerritoryType;
|
|
worldCheckTask.SetResult((true, item, territoryType, zoneId));
|
|
}
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Error checking world: " + ex2.Message);
|
|
worldCheckTask.SetResult((false, 0, 0u, 0u));
|
|
}
|
|
});
|
|
var (flag, helperCurrentWorld, helperCurrentZone, questerTargetZone) = await worldCheckTask.Task;
|
|
if (!flag)
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Failed to check helper world!");
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
ResetHelperTransportState();
|
|
});
|
|
return;
|
|
}
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Helper on world {helperCurrentWorld}, zone {helperCurrentZone}");
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Quester needs pickup in zone {questerTargetZone}");
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Quester invite ID: {questerName}@{questerWorld}");
|
|
ushort currentZone = clientState.TerritoryType;
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Step 1: Checking mount status");
|
|
TaskCompletionSource<bool> isMountedTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
bool flag2 = IsMounted();
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Currently mounted: {flag2}");
|
|
isMountedTask.SetResult(flag2);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Error checking mount: " + ex2.Message);
|
|
isMountedTask.SetResult(result: false);
|
|
}
|
|
});
|
|
if (!(await isMountedTask.Task))
|
|
{
|
|
TaskCompletionSource<bool> canMountTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
bool flag2 = !condition[ConditionFlag.InCombat] && !condition[ConditionFlag.Mounted] && !condition[ConditionFlag.Casting] && !condition[ConditionFlag.BetweenAreas] && !condition[ConditionFlag.Jumping] && !condition[ConditionFlag.OccupiedInQuestEvent] && !condition[ConditionFlag.OccupiedInCutSceneEvent] && !condition[ConditionFlag.BoundByDuty] && !condition[ConditionFlag.BoundByDuty56] && !condition[ConditionFlag.BoundByDuty95];
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Can mount in Helper's zone {helperCurrentZone}: {flag2}");
|
|
canMountTask.SetResult(flag2);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Error checking mount conditions: " + ex2.Message);
|
|
canMountTask.SetResult(result: false);
|
|
}
|
|
});
|
|
if (!(await canMountTask.Task))
|
|
{
|
|
log.Warning("[ChauffeurMode] [WORKFLOW] Cannot mount in Helper's current zone - will try after teleport");
|
|
}
|
|
else
|
|
{
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Not mounted, summoning mount {config.ChauffeurMountId}");
|
|
TaskCompletionSource<bool> mountTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Executing mount summon on framework thread");
|
|
bool result = SummonMountDirect(config.ChauffeurMountId);
|
|
mountTask.SetResult(result);
|
|
});
|
|
if (!(await mountTask.Task))
|
|
{
|
|
log.Warning("[ChauffeurMode] [WORKFLOW] Failed to summon mount - will try after teleport");
|
|
}
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Mount summon command sent, waiting for mount animation");
|
|
await Task.Delay(3000);
|
|
TaskCompletionSource<bool> verifyTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
bool flag2 = IsMounted();
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Mount verification: {flag2}");
|
|
verifyTask.SetResult(flag2);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Error verifying mount: " + ex2.Message);
|
|
verifyTask.SetResult(result: false);
|
|
}
|
|
});
|
|
if (!(await verifyTask.Task))
|
|
{
|
|
log.Warning("[ChauffeurMode] [WORKFLOW] Mount verification failed - will try after teleport");
|
|
}
|
|
}
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Mount verified successfully");
|
|
}
|
|
else
|
|
{
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Already mounted, skipping mount summon");
|
|
}
|
|
bool didTeleport = false;
|
|
if (currentZone != zoneId)
|
|
{
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Step 2: Teleporting to zone {zoneId}");
|
|
if (!(await TeleportToZone(zoneId)))
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Failed to teleport to zone");
|
|
return;
|
|
}
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Waiting 10s for zone load and player spawn");
|
|
await Task.Delay(10000);
|
|
didTeleport = true;
|
|
}
|
|
else
|
|
{
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Already in zone {zoneId}");
|
|
}
|
|
if (didTeleport)
|
|
{
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Step 2.5: Waiting additional 3s for player spawn and loading screen to complete");
|
|
await Task.Delay(3000);
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Verifying zone after teleport");
|
|
TaskCompletionSource<bool> verifyZoneTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
ushort territoryType = clientState.TerritoryType;
|
|
bool flag2 = territoryType == zoneId;
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Current zone: {territoryType}, Target zone: {zoneId}, Match: {flag2}");
|
|
verifyZoneTask.SetResult(flag2);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Error verifying zone: " + ex2.Message);
|
|
verifyZoneTask.SetResult(result: false);
|
|
}
|
|
});
|
|
bool inCorrectZone = await verifyZoneTask.Task;
|
|
if (!inCorrectZone)
|
|
{
|
|
log.Warning("[ChauffeurMode] [WORKFLOW] Not in correct zone yet! Will keep checking for up to 30 seconds...");
|
|
int maxAttempts = 10;
|
|
for (int attempt = 1; attempt <= maxAttempts; attempt++)
|
|
{
|
|
if (inCorrectZone)
|
|
{
|
|
break;
|
|
}
|
|
await Task.Delay(3000);
|
|
TaskCompletionSource<bool> verifyZoneTaskRetry = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
ushort territoryType = clientState.TerritoryType;
|
|
bool flag2 = territoryType == zoneId;
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Zone check attempt {attempt}/{maxAttempts} - Current zone: {territoryType}, Target zone: {zoneId}, Match: {flag2}");
|
|
verifyZoneTaskRetry.SetResult(flag2);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error($"[ChauffeurMode] [WORKFLOW] Error verifying zone (attempt {attempt}): {ex2.Message}");
|
|
verifyZoneTaskRetry.SetResult(result: false);
|
|
}
|
|
});
|
|
inCorrectZone = await verifyZoneTaskRetry.Task;
|
|
if (inCorrectZone)
|
|
{
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Zone verified after {attempt} attempts!");
|
|
break;
|
|
}
|
|
}
|
|
if (!inCorrectZone)
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Still not in correct zone after 30 seconds! Aborting.");
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
ResetHelperTransportState();
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Zone verified - waiting 2s for loading screen to fully complete");
|
|
await Task.Delay(2000);
|
|
log.Information("[ChauffeurMode] [WORKFLOW] In correct zone - checking mount status");
|
|
TaskCompletionSource<bool> checkMountTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
bool flag2 = IsMounted();
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Mount status after teleport: {flag2}");
|
|
checkMountTask.SetResult(flag2);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Error checking mount: " + ex2.Message);
|
|
checkMountTask.SetResult(result: false);
|
|
}
|
|
});
|
|
if (!(await checkMountTask.Task))
|
|
{
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Not mounted after teleport - waiting 1s then re-mounting");
|
|
await Task.Delay(1000);
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Re-mounting now");
|
|
TaskCompletionSource<bool> remountTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Executing re-mount on framework thread");
|
|
bool result = SummonMountDirect(config.ChauffeurMountId);
|
|
remountTask.SetResult(result);
|
|
});
|
|
if (!(await remountTask.Task))
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Failed to re-summon mount after teleport");
|
|
return;
|
|
}
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Re-mount command sent, waiting for mount animation");
|
|
await Task.Delay(3000);
|
|
TaskCompletionSource<bool> verifyRemountTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
bool flag2 = IsMounted();
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Re-mount verification: {flag2}");
|
|
verifyRemountTask.SetResult(flag2);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Error verifying re-mount: " + ex2.Message);
|
|
verifyRemountTask.SetResult(result: false);
|
|
}
|
|
});
|
|
if (!(await verifyRemountTask.Task))
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Re-mount verification failed - not mounted after teleport!");
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
ResetHelperTransportState();
|
|
});
|
|
return;
|
|
}
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Re-mount verified successfully");
|
|
}
|
|
else
|
|
{
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Still mounted after teleport - no need to re-mount");
|
|
}
|
|
}
|
|
Vector3 finalTargetPos = targetPos;
|
|
if (isAttuneAetheryte)
|
|
{
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Step 2.9: AttuneAetheryte detected - finding landable spot");
|
|
if (vnavmeshIPC.IsReady())
|
|
{
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Searching for landable spot near target ({targetPos.X:F2}, {targetPos.Y:F2}, {targetPos.Z:F2})");
|
|
Vector3? landableSpot = vnavmeshIPC.FindPointOnFloor(targetPos, allowUnlandable: false, 15f);
|
|
if (landableSpot.HasValue)
|
|
{
|
|
float distance = Vector3.Distance(targetPos, landableSpot.Value);
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Found landable spot {distance:F2} yalms from target: ({landableSpot.Value.X:F2}, {landableSpot.Value.Y:F2}, {landableSpot.Value.Z:F2})");
|
|
finalTargetPos = landableSpot.Value;
|
|
}
|
|
else
|
|
{
|
|
log.Warning("[ChauffeurMode] [WORKFLOW] No landable spot found, using original target position");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
log.Warning("[ChauffeurMode] [WORKFLOW] vnavmesh not ready, using original target position");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Step 2.9: Not AttuneAetheryte - using exact target position");
|
|
}
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Step 3: Navigating to quester at ({questerPos.X:F2}, {questerPos.Y:F2}, {questerPos.Z:F2})");
|
|
await NavigateToPosition(questerPos);
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Arrived at quester position");
|
|
TaskCompletionSource<float> finalDistTask = new TaskCompletionSource<float>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
IPlayerCharacter localPlayer2 = clientState.LocalPlayer;
|
|
if (localPlayer2 != null)
|
|
{
|
|
float result = Vector3.Distance(localPlayer2.Position, questerPos);
|
|
finalDistTask.SetResult(result);
|
|
}
|
|
else
|
|
{
|
|
finalDistTask.SetResult(999f);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
finalDistTask.SetResult(999f);
|
|
}
|
|
});
|
|
float finalDist = await finalDistTask.Task;
|
|
if (finalDist > 5f)
|
|
{
|
|
log.Warning($"[ChauffeurMode] [WORKFLOW] Still too far ({finalDist:F2}y), moving closer manually");
|
|
Vector3 closerPos = new Vector3(questerPos.X, questerPos.Y - 1f, questerPos.Z);
|
|
await NavigateToPosition(closerPos);
|
|
await Task.Delay(1000);
|
|
}
|
|
else
|
|
{
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Distance OK: {finalDist:F2}y");
|
|
}
|
|
if (cancellationToken.IsCancellationRequested)
|
|
{
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Workflow cancelled before party formation");
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
ResetHelperTransportState();
|
|
});
|
|
return;
|
|
}
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Step 4: Ensuring party formation");
|
|
bool inParty = false;
|
|
for (int partyAttempt = 0; partyAttempt < 10; partyAttempt++)
|
|
{
|
|
TaskCompletionSource<bool> partyCheckTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
bool result = partyList.Length > 0;
|
|
partyCheckTask.SetResult(result);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Error checking party: " + ex2.Message);
|
|
partyCheckTask.SetResult(result: false);
|
|
}
|
|
});
|
|
inParty = await partyCheckTask.Task;
|
|
if (inParty)
|
|
{
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Party formed! ({partyList.Length} members)");
|
|
break;
|
|
}
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Not in party yet, sending invite (attempt {partyAttempt + 1}/10)");
|
|
TaskCompletionSource<bool> inviteTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
bool result = partyInviteService.InviteToParty(questerName, questerWorld);
|
|
inviteTask.SetResult(result);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Error inviting: " + ex2.Message);
|
|
inviteTask.SetResult(result: false);
|
|
}
|
|
});
|
|
await inviteTask.Task;
|
|
await Task.Delay(2000);
|
|
}
|
|
if (!inParty)
|
|
{
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Failed to form party after 10 attempts (20s)");
|
|
log.Error("[ChauffeurMode] [WORKFLOW] Resetting helper state");
|
|
ResetChauffeurState();
|
|
return;
|
|
}
|
|
log.Information("[ChauffeurMode] [HELPER] ========================================");
|
|
log.Information("[ChauffeurMode] [HELPER] === SIGNALING MOUNT READY ===");
|
|
log.Information("[ChauffeurMode] [HELPER] ========================================");
|
|
log.Information($"[ChauffeurMode] [HELPER] Sending mount ready signal to: {questerName}@{questerWorld}");
|
|
log.Information($"[ChauffeurMode] [HELPER] Helper is mounted: {IsMounted()}");
|
|
log.Information($"[ChauffeurMode] [HELPER] Helper position: ({clientState.LocalPlayer?.Position.X:F2}, {clientState.LocalPlayer?.Position.Y:F2}, {clientState.LocalPlayer?.Position.Z:F2})");
|
|
crossProcessIPC.SendChauffeurMountReady(questerName, questerWorld);
|
|
log.Information("[ChauffeurMode] [HELPER] Mount ready signal sent via IPC");
|
|
log.Information("[ChauffeurMode] [WORKFLOW] Waiting 8 seconds for quester to mount...");
|
|
await Task.Delay(8000);
|
|
log.Information($"[ChauffeurMode] [WORKFLOW] Step 6: Transporting to target ({finalTargetPos.X:F2}, {finalTargetPos.Y:F2}, {finalTargetPos.Z:F2})");
|
|
isTransportingQuester = true;
|
|
await NavigateToPositionWithPassengerMonitoring(finalTargetPos, questerPos, questerName, questerWorld);
|
|
log.Information("[ChauffeurMode] [HELPER] Arrived at destination");
|
|
log.Information("[ChauffeurMode] [HELPER] Dismounting at destination");
|
|
for (int partyAttempt = 0; partyAttempt < 3; partyAttempt++)
|
|
{
|
|
TaskCompletionSource<bool> checkMountTask2 = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
bool result = condition[ConditionFlag.Mounted];
|
|
checkMountTask2.SetResult(result);
|
|
});
|
|
if (!(await checkMountTask2.Task))
|
|
{
|
|
log.Information("[ChauffeurMode] [HELPER] Already dismounted");
|
|
break;
|
|
}
|
|
log.Information($"[ChauffeurMode] [HELPER] Dismount attempt {partyAttempt + 1}/3");
|
|
await framework.RunOnFrameworkThread(delegate
|
|
{
|
|
ActionManager* ptr = ActionManager.Instance();
|
|
if (ptr != null)
|
|
{
|
|
ptr->UseAction(ActionType.Mount, 0u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null);
|
|
log.Information("[ChauffeurMode] [HELPER] Dismount action executed via ActionManager");
|
|
}
|
|
else
|
|
{
|
|
log.Error("[ChauffeurMode] [HELPER] ActionManager is null!");
|
|
}
|
|
});
|
|
await Task.Delay(2000);
|
|
}
|
|
TaskCompletionSource<bool> dismountTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
bool flag2 = condition[ConditionFlag.Mounted];
|
|
log.Information($"[ChauffeurMode] [HELPER] After dismount - Still mounted: {flag2}");
|
|
dismountTask.SetResult(!flag2);
|
|
});
|
|
await dismountTask.Task;
|
|
isTransportingQuester = false;
|
|
hasExecutedRidePillion = false;
|
|
config.AssignedQuester = "";
|
|
config.CurrentHelperStatus = HelperStatus.Available;
|
|
config.Save();
|
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
|
if (localPlayer != null)
|
|
{
|
|
string helperName = localPlayer.Name.ToString();
|
|
ushort helperWorld = (ushort)localPlayer.HomeWorld.RowId;
|
|
crossProcessIPC.BroadcastHelperStatus(helperName, helperWorld, "Available");
|
|
}
|
|
log.Information("[ChauffeurMode] [HELPER] Transport complete - FLAGS RESET + STATUS AVAILABLE (before notification)");
|
|
log.Information($"[ChauffeurMode] [HELPER] Notifying Quester of arrival: {questerName}@{questerWorld}");
|
|
crossProcessIPC.SendChauffeurArrived(questerName, questerWorld);
|
|
log.Information("[ChauffeurMode] [HELPER] Waiting for quester to restart Questionable and checking for AttuneAetheryte task...");
|
|
await Task.Delay(3000);
|
|
bool isAttuneAetheryteTask = false;
|
|
await framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
object currentTask = questionableIPC.GetCurrentTask();
|
|
if (currentTask != null && currentTask is JObject jObject)
|
|
{
|
|
JToken jToken = jObject["TaskName"];
|
|
if (jToken != null)
|
|
{
|
|
string text = jToken.ToString();
|
|
if (text.StartsWith("AttuneAetheryte("))
|
|
{
|
|
isAttuneAetheryteTask = true;
|
|
log.Information("[ChauffeurMode] [HELPER] ✓ AttuneAetheryte task detected: " + text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [HELPER] Error checking quester task: " + ex2.Message);
|
|
}
|
|
});
|
|
if (isAttuneAetheryteTask && targetPosition.HasValue)
|
|
{
|
|
log.Information("[ChauffeurMode] [HELPER] AttuneAetheryte detected - flying 10 yalms away from target before dismount");
|
|
Vector3 direction = Vector3.Normalize(await framework.RunOnFrameworkThread(() => clientState.LocalPlayer?.Position ?? Vector3.Zero) - targetPosition.Value);
|
|
Vector3 flyAwayPosition = targetPosition.Value + direction * 10f;
|
|
log.Information($"[ChauffeurMode] [HELPER] Flying to position 10 yalms away: ({flyAwayPosition.X:F2}, {flyAwayPosition.Y:F2}, {flyAwayPosition.Z:F2})");
|
|
await framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
string text = $"/vnav flyto {flyAwayPosition.X:F2} {flyAwayPosition.Y:F2} {flyAwayPosition.Z:F2}";
|
|
commandManager.ProcessCommand(text);
|
|
log.Information("[ChauffeurMode] [HELPER] Sent vnav flyto command: " + text);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [HELPER] Failed to send vnav flyto command: " + ex2.Message);
|
|
}
|
|
});
|
|
DateTime timeout = DateTime.Now.AddSeconds(10.0);
|
|
while (DateTime.Now < timeout)
|
|
{
|
|
float distanceToTarget = Vector3.Distance(await framework.RunOnFrameworkThread(() => clientState.LocalPlayer?.Position ?? Vector3.Zero), targetPosition.Value);
|
|
if (distanceToTarget >= 10f)
|
|
{
|
|
log.Information($"[ChauffeurMode] [HELPER] Successfully flew away (distance: {distanceToTarget:F2} yalms)");
|
|
break;
|
|
}
|
|
await Task.Delay(500);
|
|
}
|
|
await framework.RunOnFrameworkThread(delegate
|
|
{
|
|
commandManager.ProcessCommand("/vnav stop");
|
|
});
|
|
await Task.Delay(1000);
|
|
}
|
|
log.Information("[ChauffeurMode] [HELPER] Disbanding party");
|
|
await framework.RunOnFrameworkThread(delegate
|
|
{
|
|
memoryHelper.SendChatMessage("/leave");
|
|
log.Information("[ChauffeurMode] [HELPER] /leave command sent via UIModule");
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[ChauffeurMode] Helper workflow error: " + ex.Message);
|
|
log.Error("[ChauffeurMode] Stack trace: " + ex.StackTrace);
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
ResetHelperTransportState();
|
|
});
|
|
}
|
|
}
|
|
|
|
private async Task<bool> TeleportToZone(uint zoneId)
|
|
{
|
|
try
|
|
{
|
|
ExcelSheet<TerritoryType> territorySheet = dataManager.GetExcelSheet<TerritoryType>();
|
|
if (territorySheet == null)
|
|
{
|
|
return false;
|
|
}
|
|
TerritoryType? territory = territorySheet.GetRowOrDefault(zoneId);
|
|
if (!territory.HasValue)
|
|
{
|
|
return false;
|
|
}
|
|
uint aetheryteId = territory.Value.Aetheryte.RowId;
|
|
if (aetheryteId == 0)
|
|
{
|
|
log.Warning($"[ChauffeurMode] No aetheryte found for zone {zoneId}");
|
|
return false;
|
|
}
|
|
ExcelSheet<Lumina.Excel.Sheets.Aetheryte> aetheryteSheet = dataManager.GetExcelSheet<Lumina.Excel.Sheets.Aetheryte>();
|
|
if (aetheryteSheet == null)
|
|
{
|
|
log.Warning("[ChauffeurMode] Could not load Aetheryte sheet");
|
|
return false;
|
|
}
|
|
Lumina.Excel.Sheets.Aetheryte? aetheryte = aetheryteSheet.GetRowOrDefault(aetheryteId);
|
|
if (!aetheryte.HasValue)
|
|
{
|
|
log.Warning($"[ChauffeurMode] Aetheryte {aetheryteId} not found");
|
|
return false;
|
|
}
|
|
string aetheryteName = aetheryte.Value.PlaceName.ValueNullable?.Name.ToString() ?? "";
|
|
if (string.IsNullOrEmpty(aetheryteName))
|
|
{
|
|
log.Warning($"[ChauffeurMode] Aetheryte {aetheryteId} has no name");
|
|
return false;
|
|
}
|
|
string territoryName = territory.Value.PlaceName.ValueNullable?.Name.ToString() ?? "";
|
|
string mappedName = MapTerritoryName(territoryName);
|
|
log.Information($"[ChauffeurMode] Teleporting to {mappedName} (Territory: {territoryName}, Aetheryte: {aetheryteName}, ID: {aetheryteId})");
|
|
TaskCompletionSource<bool> tpTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
commandManager.ProcessCommand("/li " + mappedName);
|
|
tpTask.SetResult(result: true);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] Error teleporting: " + ex2.Message);
|
|
tpTask.SetResult(result: false);
|
|
}
|
|
});
|
|
await tpTask.Task;
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[ChauffeurMode] Teleport error: " + ex.Message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private unsafe bool SummonMountDirect(uint mountId)
|
|
{
|
|
try
|
|
{
|
|
log.Information($"[ChauffeurMode] [MOUNT] Summoning mount ID: {mountId}");
|
|
ActionManager* actionManager = ActionManager.Instance();
|
|
if (actionManager == null)
|
|
{
|
|
log.Error("[ChauffeurMode] [MOUNT] ActionManager is null");
|
|
return false;
|
|
}
|
|
bool result = actionManager->UseAction(ActionType.Mount, mountId, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null);
|
|
log.Information($"[ChauffeurMode] [MOUNT] ActionManager.UseAction result: {result}");
|
|
if (!result)
|
|
{
|
|
log.Warning("[ChauffeurMode] [MOUNT] ActionManager failed, trying command fallback");
|
|
string mountName = GetMountName(mountId);
|
|
if (!string.IsNullOrEmpty(mountName))
|
|
{
|
|
commandManager.ProcessCommand("/mount \"" + mountName + "\"");
|
|
log.Information("[ChauffeurMode] [MOUNT] Command sent: /mount \"" + mountName + "\"");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[ChauffeurMode] [MOUNT] Exception: " + ex.Message);
|
|
log.Error("[ChauffeurMode] [MOUNT] StackTrace: " + ex.StackTrace);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private string GetMountName(uint mountId)
|
|
{
|
|
try
|
|
{
|
|
ExcelSheet<Mount> mountSheet = dataManager.GetExcelSheet<Mount>();
|
|
if (mountSheet == null)
|
|
{
|
|
return "";
|
|
}
|
|
return mountSheet.GetRowOrDefault(mountId)?.Singular.ToString() ?? "";
|
|
}
|
|
catch
|
|
{
|
|
return "";
|
|
}
|
|
}
|
|
|
|
private bool IsMounted()
|
|
{
|
|
try
|
|
{
|
|
if (clientState.LocalPlayer == null)
|
|
{
|
|
return false;
|
|
}
|
|
return condition[ConditionFlag.Mounted];
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[ChauffeurMode] IsMounted error: " + ex.Message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async Task NavigateToPosition(Vector3 targetPos)
|
|
{
|
|
try
|
|
{
|
|
log.Information("[ChauffeurMode] [NAV] ========================================");
|
|
log.Information($"[ChauffeurMode] [NAV] Starting navigation to ({targetPos.X:F2}, {targetPos.Y:F2}, {targetPos.Z:F2})");
|
|
log.Information($"[ChauffeurMode] [NAV] Thread ID: {Thread.CurrentThread.ManagedThreadId}");
|
|
TaskCompletionSource<bool> stopNavTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
commandManager.ProcessCommand("/vnav stop");
|
|
log.Information("[ChauffeurMode] [NAV] Stopped any existing navigation");
|
|
stopNavTask.SetResult(result: true);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [NAV] Error stopping nav: " + ex2.Message);
|
|
stopNavTask.SetResult(result: false);
|
|
}
|
|
});
|
|
await stopNavTask.Task;
|
|
await Task.Delay(500);
|
|
TaskCompletionSource<bool> flyTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
string text = $"/vnav flyto {targetPos.X.ToString(CultureInfo.InvariantCulture)} {targetPos.Y.ToString(CultureInfo.InvariantCulture)} {targetPos.Z.ToString(CultureInfo.InvariantCulture)}";
|
|
log.Information("[ChauffeurMode] [NAV] Executing: " + text);
|
|
commandManager.ProcessCommand(text);
|
|
flyTask.SetResult(result: true);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [NAV] Command error: " + ex2.Message);
|
|
flyTask.SetResult(result: false);
|
|
}
|
|
});
|
|
await flyTask.Task;
|
|
await Task.Delay(1000);
|
|
DateTime startTime = DateTime.Now;
|
|
TimeSpan timeout = TimeSpan.FromMinutes(5L);
|
|
DateTime lastLogTime = DateTime.Now;
|
|
int stuckCounter = 0;
|
|
float lastDistance = float.MaxValue;
|
|
while (DateTime.Now - startTime < timeout)
|
|
{
|
|
if (!isTransportingQuester)
|
|
{
|
|
log.Information("[ChauffeurMode] [NAV] Transport cancelled, stopping navigation");
|
|
TaskCompletionSource<bool> cancelTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
commandManager.ProcessCommand("/vnav stop");
|
|
cancelTask.SetResult(result: true);
|
|
}
|
|
catch
|
|
{
|
|
cancelTask.SetResult(result: false);
|
|
}
|
|
});
|
|
await cancelTask.Task;
|
|
return;
|
|
}
|
|
TaskCompletionSource<Vector3?> posTask = new TaskCompletionSource<Vector3?>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
|
posTask.SetResult(localPlayer?.Position);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [NAV] Error getting player position: " + ex2.Message);
|
|
posTask.SetResult(null);
|
|
}
|
|
});
|
|
Vector3? playerPos = await posTask.Task;
|
|
if (!playerPos.HasValue)
|
|
{
|
|
log.Warning("[ChauffeurMode] [NAV] Could not get player position");
|
|
break;
|
|
}
|
|
float distance = Vector3.Distance(playerPos.Value, targetPos);
|
|
if (Math.Abs(distance - lastDistance) < 1f)
|
|
{
|
|
stuckCounter++;
|
|
if (stuckCounter > 10)
|
|
{
|
|
log.Warning($"[ChauffeurMode] [NAV] Stuck at distance {distance:F2}, aborting");
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
stuckCounter = 0;
|
|
}
|
|
lastDistance = distance;
|
|
float stopDistance = Math.Clamp(config.ChauffeurStopDistance, 2f, 15f);
|
|
if (distance < stopDistance)
|
|
{
|
|
log.Information($"Arrived at destination, distance {distance:F2} yalms");
|
|
TaskCompletionSource<bool> arrivedTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
commandManager.ProcessCommand("/vnav stop");
|
|
log.Information("[ChauffeurMode] [NAV] Navigation stopped");
|
|
arrivedTask.SetResult(result: true);
|
|
}
|
|
catch
|
|
{
|
|
arrivedTask.SetResult(result: false);
|
|
}
|
|
});
|
|
await arrivedTask.Task;
|
|
return;
|
|
}
|
|
if ((DateTime.Now - lastLogTime).TotalSeconds >= 5.0)
|
|
{
|
|
log.Information($"[ChauffeurMode] [NAV] Distance to target: {distance:F2} yalms");
|
|
lastLogTime = DateTime.Now;
|
|
}
|
|
await Task.Delay(1000);
|
|
}
|
|
log.Warning("[ChauffeurMode] [NAV] Navigation timeout");
|
|
TaskCompletionSource<bool> timeoutTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
commandManager.ProcessCommand("/vnav stop");
|
|
log.Information("[ChauffeurMode] [NAV] Navigation stopped (timeout)");
|
|
timeoutTask.SetResult(result: true);
|
|
}
|
|
catch
|
|
{
|
|
timeoutTask.SetResult(result: false);
|
|
}
|
|
});
|
|
await timeoutTask.Task;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[ChauffeurMode] [NAV] Navigation error: " + ex.Message);
|
|
log.Error("[ChauffeurMode] [NAV] StackTrace: " + ex.StackTrace);
|
|
}
|
|
}
|
|
|
|
private async Task NavigateToPositionWithPassengerMonitoring(Vector3 targetPos, Vector3 questerStartPos, string questerName, ushort questerWorld)
|
|
{
|
|
float arrivalThreshold = Math.Clamp(config.ChauffeurStopDistance, 2f, 15f);
|
|
int stuckCounter = 0;
|
|
float lastDistance = 0f;
|
|
for (int attempt = 1; attempt <= 5; attempt++)
|
|
{
|
|
log.Information($"[ChauffeurMode] [TRANSPORT] === ATTEMPT {attempt}/{5} ===");
|
|
log.Information("[ChauffeurMode] [TRANSPORT] Starting navigation to target");
|
|
TaskCompletionSource<bool> startNavTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
commandManager.ProcessCommand("/vnav stop");
|
|
Thread.Sleep(500);
|
|
string text = $"/vnav flyto {targetPos.X.ToString(CultureInfo.InvariantCulture)} {targetPos.Y.ToString(CultureInfo.InvariantCulture)} {targetPos.Z.ToString(CultureInfo.InvariantCulture)}";
|
|
commandManager.ProcessCommand(text);
|
|
log.Information("[ChauffeurMode] [TRANSPORT] Navigation started: " + text);
|
|
startNavTask.SetResult(result: true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[ChauffeurMode] [TRANSPORT] Failed to start navigation: " + ex.Message);
|
|
startNavTask.SetResult(result: false);
|
|
}
|
|
});
|
|
await startNavTask.Task;
|
|
await Task.Delay(2000);
|
|
TaskCompletionSource<Vector3?> helperStartPosTask = new TaskCompletionSource<Vector3?>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
Vector3? result = clientState.LocalPlayer?.Position;
|
|
helperStartPosTask.SetResult(result);
|
|
}
|
|
catch
|
|
{
|
|
helperStartPosTask.SetResult(null);
|
|
}
|
|
});
|
|
Vector3? helperStartPos = await helperStartPosTask.Task;
|
|
if (!helperStartPos.HasValue)
|
|
{
|
|
log.Error("[ChauffeurMode] [TRANSPORT] Could not get helper position");
|
|
continue;
|
|
}
|
|
log.Information($"[ChauffeurMode] [TRANSPORT] Helper start: ({helperStartPos.Value.X:F2}, {helperStartPos.Value.Y:F2}, {helperStartPos.Value.Z:F2})");
|
|
log.Information("[ChauffeurMode] [TRANSPORT] Waiting 5 seconds to check if helper moved...");
|
|
await Task.Delay(5000);
|
|
TaskCompletionSource<Vector3?> helperCurrentPosTask = new TaskCompletionSource<Vector3?>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
Vector3? result = clientState.LocalPlayer?.Position;
|
|
helperCurrentPosTask.SetResult(result);
|
|
}
|
|
catch
|
|
{
|
|
helperCurrentPosTask.SetResult(null);
|
|
}
|
|
});
|
|
Vector3? helperCurrentPos = await helperCurrentPosTask.Task;
|
|
bool questerMovingWithUs = true;
|
|
if (helperCurrentPos.HasValue)
|
|
{
|
|
float helperDistanceMoved = Vector3.Distance(helperStartPos.Value, helperCurrentPos.Value);
|
|
log.Information($"[ChauffeurMode] [TRANSPORT] Helper moved {helperDistanceMoved:F2} yalms after 5 seconds");
|
|
if (helperDistanceMoved < 10f)
|
|
{
|
|
log.Warning($"[ChauffeurMode] [TRANSPORT] ❌ Helper barely moved ({helperDistanceMoved:F2}y) - quester likely not on mount!");
|
|
questerMovingWithUs = false;
|
|
}
|
|
else
|
|
{
|
|
log.Information($"[ChauffeurMode] [TRANSPORT] ✓ Helper moved significantly ({helperDistanceMoved:F2}y) - quester is on mount!");
|
|
}
|
|
}
|
|
if (!questerMovingWithUs)
|
|
{
|
|
log.Warning("[ChauffeurMode] [TRANSPORT] Quester failed to stay on mount - returning to quester position");
|
|
TaskCompletionSource<bool> stopTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
commandManager.ProcessCommand("/vnav stop");
|
|
stopTask.SetResult(result: true);
|
|
}
|
|
catch
|
|
{
|
|
stopTask.SetResult(result: false);
|
|
}
|
|
});
|
|
await stopTask.Task;
|
|
await Task.Delay(1000);
|
|
log.Information("[ChauffeurMode] [TRANSPORT] Returning to quester for retry...");
|
|
await NavigateToPosition(questerStartPos);
|
|
await Task.Delay(2000);
|
|
log.Information("[ChauffeurMode] [TRANSPORT] Signaling mount ready for retry...");
|
|
crossProcessIPC.SendChauffeurMountReady(questerName, questerWorld);
|
|
DateTime waitStart = DateTime.Now;
|
|
TimeSpan waitMax = TimeSpan.FromSeconds(30L);
|
|
bool mounted = false;
|
|
while (DateTime.Now - waitStart < waitMax)
|
|
{
|
|
TaskCompletionSource<bool> checkTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
bool result = condition[ConditionFlag.RidingPillion];
|
|
checkTask.SetResult(result);
|
|
}
|
|
catch
|
|
{
|
|
checkTask.SetResult(result: false);
|
|
}
|
|
});
|
|
if (await checkTask.Task)
|
|
{
|
|
log.Information("[ChauffeurMode] [TRANSPORT] ✓ Quester mounted for retry!");
|
|
mounted = true;
|
|
break;
|
|
}
|
|
await Task.Delay(1000);
|
|
}
|
|
if (!mounted)
|
|
{
|
|
log.Error("[ChauffeurMode] [TRANSPORT] Quester failed to mount after retry - aborting");
|
|
return;
|
|
}
|
|
continue;
|
|
}
|
|
log.Information("[ChauffeurMode] [TRANSPORT] ✓ Quester confirmed on mount - continuing to destination");
|
|
DateTime arrivalStart = DateTime.Now;
|
|
TimeSpan arrivalTimeout = TimeSpan.FromMinutes(5L);
|
|
while (DateTime.Now - arrivalStart < arrivalTimeout)
|
|
{
|
|
if (!isTransportingQuester)
|
|
{
|
|
log.Information("[ChauffeurMode] [TRANSPORT] Transport cancelled");
|
|
TaskCompletionSource<bool> cancelTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
commandManager.ProcessCommand("/vnav stop");
|
|
cancelTask.SetResult(result: true);
|
|
}
|
|
catch
|
|
{
|
|
cancelTask.SetResult(result: false);
|
|
}
|
|
});
|
|
await cancelTask.Task;
|
|
return;
|
|
}
|
|
TaskCompletionSource<Vector3?> posTask = new TaskCompletionSource<Vector3?>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
Vector3? result = clientState.LocalPlayer?.Position;
|
|
posTask.SetResult(result);
|
|
}
|
|
catch
|
|
{
|
|
posTask.SetResult(null);
|
|
}
|
|
});
|
|
Vector3? currentPos = await posTask.Task;
|
|
if (currentPos.HasValue)
|
|
{
|
|
float distance = Vector3.Distance(currentPos.Value, targetPos);
|
|
if (lastDistance > 0f && Math.Abs(distance - lastDistance) < 1f)
|
|
{
|
|
stuckCounter++;
|
|
if (stuckCounter >= 5)
|
|
{
|
|
log.Warning($"[ChauffeurMode] [TRANSPORT] Stuck for 5 seconds at distance {distance:F2}");
|
|
log.Information("[ChauffeurMode] [TRANSPORT] Moving 5 yalms backwards to unstuck");
|
|
await framework.RunOnFrameworkThread(delegate
|
|
{
|
|
commandManager.ProcessCommand("/vnav stop");
|
|
});
|
|
await Task.Delay(500);
|
|
Vector3 direction = Vector3.Normalize(currentPos.Value - targetPos);
|
|
Vector3 backwardsPos = currentPos.Value + direction * 5f;
|
|
log.Information($"[ChauffeurMode] [TRANSPORT] Moving to backwards position: ({backwardsPos.X:F2}, {backwardsPos.Y:F2}, {backwardsPos.Z:F2})");
|
|
await framework.RunOnFrameworkThread(delegate
|
|
{
|
|
commandManager.ProcessCommand($"/vnav {backwardsPos.X} {backwardsPos.Y} {backwardsPos.Z}");
|
|
});
|
|
await Task.Delay(3000);
|
|
await framework.RunOnFrameworkThread(delegate
|
|
{
|
|
commandManager.ProcessCommand("/vnav stop");
|
|
});
|
|
log.Information("[ChauffeurMode] [TRANSPORT] Unstuck complete, considering arrived");
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
stuckCounter = 0;
|
|
}
|
|
lastDistance = distance;
|
|
if (distance <= arrivalThreshold)
|
|
{
|
|
log.Information($"[ChauffeurMode] [TRANSPORT] Arrived at destination (distance: {distance:F2} yalms, threshold: {arrivalThreshold:F1})");
|
|
TaskCompletionSource<bool> arrivedTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
commandManager.ProcessCommand("/vnav stop");
|
|
arrivedTask.SetResult(result: true);
|
|
}
|
|
catch
|
|
{
|
|
arrivedTask.SetResult(result: false);
|
|
}
|
|
});
|
|
await arrivedTask.Task;
|
|
return;
|
|
}
|
|
}
|
|
await Task.Delay(1000);
|
|
}
|
|
log.Warning("[ChauffeurMode] [TRANSPORT] Arrival timeout - but quester was on mount, so considering it success");
|
|
return;
|
|
}
|
|
log.Error($"[ChauffeurMode] [TRANSPORT] Failed after {5} attempts - giving up");
|
|
ResetChauffeurState();
|
|
}
|
|
|
|
private void OnChauffeurReadyForPickup(string helperName)
|
|
{
|
|
if (config.ChauffeurModeEnabled && config.IsQuester && isWaitingForHelper)
|
|
{
|
|
log.Information("[ChauffeurMode] ========================================");
|
|
log.Information("[ChauffeurMode] === HELPER READY ===");
|
|
log.Information("[ChauffeurMode] ========================================");
|
|
log.Information("[ChauffeurMode] Helper " + helperName + " is ready for pickup");
|
|
log.Information("[ChauffeurMode] [QUESTER] Waiting for mount ready signal...");
|
|
}
|
|
}
|
|
|
|
private unsafe void OnChauffeurMountReady(string questerName, ushort questerWorld)
|
|
{
|
|
if (!config.ChauffeurModeEnabled || !config.IsQuester || !isWaitingForHelper)
|
|
{
|
|
return;
|
|
}
|
|
if (hasExecutedRidePillion)
|
|
{
|
|
log.Debug("[ChauffeurMode] [QUESTER] RidePillion already executed this session, ignoring");
|
|
return;
|
|
}
|
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
|
if (localPlayer == null)
|
|
{
|
|
return;
|
|
}
|
|
string myName = localPlayer.Name.ToString();
|
|
ushort myWorld = (ushort)localPlayer.HomeWorld.RowId;
|
|
if (myName != questerName || myWorld != questerWorld)
|
|
{
|
|
log.Debug($"[ChauffeurMode] [QUESTER] Mount ready signal is for {questerName}@{questerWorld}, not for me ({myName}@{myWorld}) - ignoring");
|
|
return;
|
|
}
|
|
log.Information("[ChauffeurMode] ========================================");
|
|
log.Information("[ChauffeurMode] === MOUNT READY FOR RIDEPILLION ===");
|
|
log.Information("[ChauffeurMode] ========================================");
|
|
log.Information($"[ChauffeurMode] [QUESTER] This signal is for ME: {myName}@{myWorld}");
|
|
hasExecutedRidePillion = true;
|
|
Task.Run(async delegate
|
|
{
|
|
_ = 11;
|
|
try
|
|
{
|
|
TaskCompletionSource<bool> isMountedTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
bool flag = IsMounted();
|
|
log.Information($"[ChauffeurMode] [QUESTER] Currently mounted: {flag}");
|
|
isMountedTask.SetResult(flag);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [QUESTER] Error checking mount: " + ex2.Message);
|
|
isMountedTask.SetResult(result: false);
|
|
}
|
|
});
|
|
if (await isMountedTask.Task)
|
|
{
|
|
log.Information("[ChauffeurMode] [QUESTER] Dismounting before RidePillion (Condition 4 active)");
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
log.Information($"[ChauffeurMode] [QUESTER] Dismount attempt {i + 1}/3 using ActionManager");
|
|
await framework.RunOnFrameworkThread(delegate
|
|
{
|
|
ActionManager* ptr = ActionManager.Instance();
|
|
if (ptr != null)
|
|
{
|
|
ptr->UseAction(ActionType.Mount, 0u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null);
|
|
log.Information("[ChauffeurMode] [QUESTER] Dismount action executed via ActionManager");
|
|
}
|
|
else
|
|
{
|
|
log.Error("[ChauffeurMode] [QUESTER] ActionManager is null!");
|
|
}
|
|
});
|
|
await Task.Delay(2000);
|
|
TaskCompletionSource<bool> checkTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
bool flag = condition[ConditionFlag.Mounted];
|
|
log.Information($"[ChauffeurMode] [QUESTER] After dismount attempt {i + 1} - Still mounted: {flag}");
|
|
checkTask.SetResult(flag);
|
|
});
|
|
if (!(await checkTask.Task))
|
|
{
|
|
log.Information("[ChauffeurMode] [QUESTER] Successfully dismounted!");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
log.Information("[ChauffeurMode] [QUESTER] Not mounted, no dismount needed");
|
|
}
|
|
log.Information("[ChauffeurMode] [QUESTER] Finding Helper in party using IPartyList");
|
|
TaskCompletionSource<string?> helperNameTask = new TaskCompletionSource<string>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
IPlayerCharacter localPlayer2 = clientState.LocalPlayer;
|
|
if (localPlayer2 == null)
|
|
{
|
|
log.Error("[ChauffeurMode] [QUESTER] Local player is null");
|
|
helperNameTask.SetResult(null);
|
|
}
|
|
else
|
|
{
|
|
string text = localPlayer2.Name.ToString();
|
|
log.Information("[ChauffeurMode] [QUESTER] My name: " + text);
|
|
if (partyList != null && partyList.Length > 1)
|
|
{
|
|
log.Information($"[ChauffeurMode] [QUESTER] Party size: {partyList.Length}");
|
|
for (int j = 0; j < partyList.Length; j++)
|
|
{
|
|
IPartyMember partyMember = partyList[j];
|
|
if (partyMember != null)
|
|
{
|
|
string text2 = partyMember.Name.ToString();
|
|
log.Information($"[ChauffeurMode] [QUESTER] Party member [{j}]: {text2}");
|
|
if (text2 != text)
|
|
{
|
|
log.Information("[ChauffeurMode] [QUESTER] Found Helper in party: " + text2);
|
|
helperNameTask.SetResult(text2);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
log.Warning("[ChauffeurMode] [QUESTER] No Helper found in party (only found self)");
|
|
helperNameTask.SetResult(null);
|
|
}
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [QUESTER] Error finding Helper: " + ex2.Message);
|
|
helperNameTask.SetResult(null);
|
|
}
|
|
});
|
|
string helperName = await helperNameTask.Task;
|
|
if (string.IsNullOrEmpty(helperName))
|
|
{
|
|
log.Error("[ChauffeurMode] [QUESTER] Cannot execute RidePillion - Helper not found in party");
|
|
}
|
|
else
|
|
{
|
|
log.Information("[ChauffeurMode] [QUESTER] Targeting Helper: " + helperName);
|
|
await framework.RunOnFrameworkThread(delegate
|
|
{
|
|
TargetSystem* ptr = TargetSystem.Instance();
|
|
if (ptr == null)
|
|
{
|
|
log.Error("[ChauffeurMode] [QUESTER] TargetSystem is null!");
|
|
}
|
|
else
|
|
{
|
|
if (partyList != null)
|
|
{
|
|
for (int j = 0; j < partyList.Length; j++)
|
|
{
|
|
IPartyMember partyMember = partyList[j];
|
|
if (partyMember != null && partyMember.Name.ToString() == helperName)
|
|
{
|
|
IGameObject gameObject = partyMember.GameObject;
|
|
if (gameObject != null)
|
|
{
|
|
ptr->Target = (GameObject*)gameObject.Address;
|
|
log.Information("[ChauffeurMode] [QUESTER] Targeted Helper via TargetSystem: " + helperName);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
log.Warning("[ChauffeurMode] [QUESTER] Could not find Helper GameObject to target");
|
|
}
|
|
});
|
|
await Task.Delay(1000);
|
|
log.Information("[ChauffeurMode] [QUESTER] ========================================");
|
|
log.Information("[ChauffeurMode] [QUESTER] === EXECUTING RIDEPILLION ===");
|
|
log.Information("[ChauffeurMode] [QUESTER] ========================================");
|
|
log.Information("[ChauffeurMode] [QUESTER] Helper name: " + helperName);
|
|
log.Information($"[ChauffeurMode] [QUESTER] Party size: {partyList.Length}");
|
|
for (int i2 = 0; i2 < 3; i2++)
|
|
{
|
|
log.Information($"[ChauffeurMode] [QUESTER] --- RidePillion attempt {i2 + 1}/3 ---");
|
|
await framework.RunOnFrameworkThread(delegate
|
|
{
|
|
log.Information("[ChauffeurMode] [QUESTER] Searching for Helper in party...");
|
|
if (partyList != null)
|
|
{
|
|
for (int j = 0; j < partyList.Length; j++)
|
|
{
|
|
IPartyMember partyMember = partyList[j];
|
|
if (partyMember != null)
|
|
{
|
|
string text = partyMember.Name.ToString();
|
|
log.Information($"[ChauffeurMode] [QUESTER] Party member {j}: {text}");
|
|
if (text == helperName)
|
|
{
|
|
log.Information("[ChauffeurMode] [QUESTER] Found Helper: " + helperName);
|
|
IGameObject gameObject = partyMember.GameObject;
|
|
if (gameObject != null)
|
|
{
|
|
log.Information($"[ChauffeurMode] [QUESTER] Helper GameObject address: 0x{gameObject.Address:X}");
|
|
log.Information($"[ChauffeurMode] [QUESTER] Helper ObjectKind: {gameObject.ObjectKind}");
|
|
log.Information($"[ChauffeurMode] [QUESTER] Helper Position: ({gameObject.Position.X:F2}, {gameObject.Position.Y:F2}, {gameObject.Position.Z:F2})");
|
|
BattleChara* address = (BattleChara*)gameObject.Address;
|
|
log.Information($"[ChauffeurMode] [QUESTER] BattleChara pointer: 0x{address:X}");
|
|
log.Information("[ChauffeurMode] [QUESTER] Calling MemoryHelper.ExecuteRidePillion(battleChara, 10)...");
|
|
bool value = memoryHelper.ExecuteRidePillion(address);
|
|
log.Information($"[ChauffeurMode] [QUESTER] RidePillion Memory call result: {value}");
|
|
return;
|
|
}
|
|
log.Warning("[ChauffeurMode] [QUESTER] Helper GameObject is NULL!");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
log.Warning("[ChauffeurMode] [QUESTER] Could not find Helper in party to execute RidePillion");
|
|
});
|
|
await Task.Delay(2000);
|
|
TaskCompletionSource<bool> checkTask2 = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
bool flag = condition[ConditionFlag.RidingPillion];
|
|
log.Information($"[ChauffeurMode] [QUESTER] After RidePillion attempt {i2 + 1} - Mounted as passenger: {flag}");
|
|
checkTask2.SetResult(flag);
|
|
});
|
|
if (await checkTask2.Task)
|
|
{
|
|
log.Information("[ChauffeurMode] [QUESTER] Successfully mounted as passenger!");
|
|
break;
|
|
}
|
|
}
|
|
TaskCompletionSource<bool> mountedTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
bool flag = condition[ConditionFlag.RidingPillion];
|
|
log.Information($"[ChauffeurMode] [QUESTER] Mounted as passenger (Condition 10): {flag}");
|
|
mountedTask.SetResult(flag);
|
|
});
|
|
if (await mountedTask.Task)
|
|
{
|
|
log.Information("[ChauffeurMode] [QUESTER] Successfully mounted as passenger!");
|
|
log.Information("[ChauffeurMode] [QUESTER] Sending mounted signal to Helper");
|
|
TaskCompletionSource<bool> signalTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
crossProcessIPC.SendChauffeurPassengerMounted();
|
|
log.Information("[ChauffeurMode] [QUESTER] Passenger mounted signal sent");
|
|
signalTask.SetResult(result: true);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [QUESTER] Signal error: " + ex2.Message);
|
|
signalTask.SetResult(result: false);
|
|
}
|
|
});
|
|
await signalTask.Task;
|
|
}
|
|
else
|
|
{
|
|
log.Warning("[ChauffeurMode] [QUESTER] RidePillion may have failed - not detected as passenger");
|
|
}
|
|
log.Information("[ChauffeurMode] [QUESTER] Waiting for transport to complete...");
|
|
log.Information("[ChauffeurMode] [QUESTER] Movement Monitor is stopped during transport");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[ChauffeurMode] [QUESTER] Error during mount ready: " + ex.Message);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void OnChauffeurPassengerMounted()
|
|
{
|
|
log.Debug("[ChauffeurMode] OnChauffeurPassengerMounted event fired (handled inline)");
|
|
}
|
|
|
|
private void OnHelperStatusUpdate(string helperName, ushort helperWorld, string status)
|
|
{
|
|
if (!config.IsQuester)
|
|
{
|
|
return;
|
|
}
|
|
ExcelSheet<World> worldSheet = dataManager.GetExcelSheet<World>();
|
|
string worldName = "Unknown";
|
|
if (worldSheet != null)
|
|
{
|
|
foreach (World world in worldSheet)
|
|
{
|
|
if (world.RowId == helperWorld)
|
|
{
|
|
worldName = world.Name.ExtractText();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
string helperKey = helperName + "@" + worldName;
|
|
helperStatuses[helperKey] = status;
|
|
log.Debug("[ChauffeurMode] [QUESTER] Helper status updated: " + helperKey + " = " + status);
|
|
}
|
|
|
|
private void BroadcastHelperStatusPeriodically()
|
|
{
|
|
if (isDisposed)
|
|
{
|
|
log.Debug("[ChauffeurMode] [HELPER] Periodic broadcast stopped (service disposed)");
|
|
return;
|
|
}
|
|
if (!config.IsHighLevelHelper)
|
|
{
|
|
if (!isDisposed)
|
|
{
|
|
framework.RunOnTick(delegate
|
|
{
|
|
BroadcastHelperStatusPeriodically();
|
|
}, TimeSpan.FromSeconds(10L));
|
|
}
|
|
return;
|
|
}
|
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
|
if (localPlayer != null)
|
|
{
|
|
try
|
|
{
|
|
string helperName = localPlayer.Name.ToString();
|
|
ushort helperWorld = (ushort)localPlayer.HomeWorld.RowId;
|
|
string status = config.CurrentHelperStatus switch
|
|
{
|
|
HelperStatus.Available => "Available",
|
|
HelperStatus.Transporting => "Transporting",
|
|
HelperStatus.InDungeon => "InDungeon",
|
|
_ => "Available",
|
|
};
|
|
crossProcessIPC.BroadcastHelperStatus(helperName, helperWorld, status);
|
|
log.Debug("[ChauffeurMode] [HELPER] Periodic status broadcast: " + status);
|
|
}
|
|
catch (ObjectDisposedException)
|
|
{
|
|
log.Debug("[ChauffeurMode] [HELPER] Periodic broadcast stopped (IPC disposed)");
|
|
return;
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [HELPER] Error in periodic broadcast: " + ex2.Message);
|
|
}
|
|
}
|
|
if (!isDisposed)
|
|
{
|
|
framework.RunOnTick(delegate
|
|
{
|
|
BroadcastHelperStatusPeriodically();
|
|
}, TimeSpan.FromSeconds(10L));
|
|
}
|
|
}
|
|
|
|
private void OnChauffeurArrived(string questerName, ushort questerWorld)
|
|
{
|
|
if (!config.ChauffeurModeEnabled || !config.IsQuester || !isWaitingForHelper)
|
|
{
|
|
return;
|
|
}
|
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
|
if (localPlayer == null)
|
|
{
|
|
return;
|
|
}
|
|
string myName = localPlayer.Name.ToString();
|
|
ushort myWorld = (ushort)localPlayer.HomeWorld.RowId;
|
|
if (myName != questerName || myWorld != questerWorld)
|
|
{
|
|
log.Debug($"[ChauffeurMode] [QUESTER] Arrived signal is for {questerName}@{questerWorld}, not for me ({myName}@{myWorld}) - ignoring");
|
|
return;
|
|
}
|
|
log.Information("[ChauffeurMode] ========================================");
|
|
log.Information("[ChauffeurMode] === ARRIVED AT DESTINATION ===");
|
|
log.Information("[ChauffeurMode] ========================================");
|
|
log.Information($"[ChauffeurMode] [QUESTER] This signal is for ME: {myName}@{myWorld}");
|
|
Task.Run(async delegate
|
|
{
|
|
_ = 6;
|
|
try
|
|
{
|
|
TaskCompletionSource<bool> isMountedTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
bool flag = IsMounted();
|
|
log.Information($"[ChauffeurMode] [QUESTER] Currently mounted: {flag}");
|
|
isMountedTask.SetResult(flag);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [QUESTER] Error checking mount: " + ex2.Message);
|
|
isMountedTask.SetResult(result: false);
|
|
}
|
|
});
|
|
bool num = await isMountedTask.Task;
|
|
log.Information("[ChauffeurMode] [QUESTER] ========================================");
|
|
log.Information("[ChauffeurMode] [QUESTER] === ARRIVED AT DESTINATION ===");
|
|
log.Information("[ChauffeurMode] [QUESTER] ========================================");
|
|
if (num)
|
|
{
|
|
log.Information("[ChauffeurMode] [QUESTER] Dismounting from RidePillion (Condition 10 active)");
|
|
TaskCompletionSource<bool> dismountTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
try
|
|
{
|
|
commandManager.ProcessCommand("/ridepillion");
|
|
log.Information("[ChauffeurMode] [QUESTER] /ridepillion command sent to dismount");
|
|
dismountTask.SetResult(result: true);
|
|
}
|
|
catch (Exception ex2)
|
|
{
|
|
log.Error("[ChauffeurMode] [QUESTER] Command error: " + ex2.Message);
|
|
dismountTask.SetResult(result: false);
|
|
}
|
|
});
|
|
await dismountTask.Task;
|
|
log.Information("[ChauffeurMode] [QUESTER] Waiting 3 seconds for dismount...");
|
|
await Task.Delay(3000);
|
|
TaskCompletionSource<bool> verifyTask = new TaskCompletionSource<bool>();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
bool flag = condition[ConditionFlag.Mounted] || condition[ConditionFlag.RidingPillion];
|
|
log.Information($"[ChauffeurMode] [QUESTER] After dismount - Still mounted: {flag}");
|
|
verifyTask.SetResult(!flag);
|
|
});
|
|
await verifyTask.Task;
|
|
}
|
|
else
|
|
{
|
|
log.Information("[ChauffeurMode] [QUESTER] Not mounted as passenger, skipping dismount");
|
|
}
|
|
log.Information("[ChauffeurMode] [QUESTER] Leaving party");
|
|
await framework.RunOnFrameworkThread(delegate
|
|
{
|
|
memoryHelper.SendChatMessage("/leave");
|
|
log.Information("[ChauffeurMode] [QUESTER] /leave command sent via UIModule");
|
|
});
|
|
await Task.Delay(1000);
|
|
log.Information("[ChauffeurMode] [QUESTER] ========================================");
|
|
log.Information("[ChauffeurMode] [QUESTER] === RESUMING QUESTIONABLE ===");
|
|
log.Information("[ChauffeurMode] [QUESTER] ========================================");
|
|
await framework.RunOnFrameworkThread(delegate
|
|
{
|
|
commandManager.ProcessCommand("/qst start");
|
|
log.Information("[ChauffeurMode] [QUESTER] /qst start command sent");
|
|
});
|
|
isWaitingForHelper = false;
|
|
hasExecutedRidePillion = false;
|
|
targetPosition = null;
|
|
targetZoneId = 0u;
|
|
log.Information("[ChauffeurMode] Chauffeur transport complete - FLAGS RESET");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[ChauffeurMode] [QUESTER] Error during arrival: " + ex.Message);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void OnTerritoryChanged(ushort territoryId)
|
|
{
|
|
if (!config.ChauffeurModeEnabled)
|
|
{
|
|
return;
|
|
}
|
|
if (isWaitingForHelper && targetZoneId != 0 && targetZoneId != territoryId)
|
|
{
|
|
log.Information($"[ChauffeurMode] Zone changed ({targetZoneId} -> {territoryId}) while waiting for helper, resetting state");
|
|
ResetChauffeurState();
|
|
}
|
|
if (config.IsQuester && !((DateTime.Now - lastZoneUpdate).TotalSeconds < 5.0))
|
|
{
|
|
lastZoneUpdate = DateTime.Now;
|
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
|
if (localPlayer != null)
|
|
{
|
|
string zoneName = GetZoneName(territoryId);
|
|
log.Information($"[ChauffeurMode] Zone changed: {zoneName} ({territoryId})");
|
|
log.Information("[ChauffeurMode] Territory Load State: Zone change detected, starting 8-second broadcast delay");
|
|
lastZoneChangeTime = DateTime.Now;
|
|
log.Information("[ChauffeurMode] [QUESTER] Sending zone update to helper");
|
|
crossProcessIPC.SendChauffeurZoneUpdate(localPlayer.Name.ToString(), (ushort)localPlayer.HomeWorld.RowId, territoryId, zoneName);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnChauffeurZoneUpdate(string questerName, ushort questerWorld, uint zoneId, string zoneName)
|
|
{
|
|
if (!config.ChauffeurModeEnabled || !config.IsHighLevelHelper)
|
|
{
|
|
return;
|
|
}
|
|
log.Debug($"[ChauffeurMode] Zone update received: {questerName}@{questerWorld} -> {zoneName} ({zoneId})");
|
|
log.Debug("[ChauffeurMode] Auto-follow disabled, waiting for explicit summon");
|
|
if (isTransportingQuester)
|
|
{
|
|
log.Information("[ChauffeurMode] Quester moved to different zone (" + zoneName + "), cancelling transport");
|
|
ResetChauffeurState();
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
commandManager.ProcessCommand("/vnav stop");
|
|
log.Information("[ChauffeurMode] Navigation stopped");
|
|
});
|
|
}
|
|
}
|
|
|
|
private string GetZoneName(uint territoryId)
|
|
{
|
|
try
|
|
{
|
|
ExcelSheet<TerritoryType> territorySheet = dataManager.GetExcelSheet<TerritoryType>();
|
|
if (territorySheet == null)
|
|
{
|
|
return $"Zone {territoryId}";
|
|
}
|
|
TerritoryType? territory = territorySheet.GetRowOrDefault(territoryId);
|
|
if (!territory.HasValue)
|
|
{
|
|
return $"Zone {territoryId}";
|
|
}
|
|
return territory.Value.PlaceName.ValueNullable?.Name.ToString() ?? $"Zone {territoryId}";
|
|
}
|
|
catch
|
|
{
|
|
return $"Zone {territoryId}";
|
|
}
|
|
}
|
|
|
|
private void ResetHelperTransportState()
|
|
{
|
|
log.Warning("[ChauffeurMode] [HELPER] Resetting transport state due to workflow abort");
|
|
isTransportingQuester = false;
|
|
if (config.IsHighLevelHelper && !string.IsNullOrEmpty(config.AssignedQuester))
|
|
{
|
|
log.Information("[ChauffeurMode] [HELPER] Clearing assigned quester: " + config.AssignedQuester);
|
|
config.AssignedQuester = "";
|
|
config.CurrentHelperStatus = HelperStatus.Available;
|
|
config.Save();
|
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
|
if (localPlayer != null)
|
|
{
|
|
string helperName = localPlayer.Name.ToString();
|
|
ushort helperWorld = (ushort)localPlayer.HomeWorld.RowId;
|
|
crossProcessIPC.BroadcastHelperStatus(helperName, helperWorld, "Available");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ResetChauffeurState()
|
|
{
|
|
log.Warning("[ChauffeurMode] ========================================");
|
|
log.Warning("[ChauffeurMode] === RESETTING CHAUFFEUR STATE ===");
|
|
log.Warning("[ChauffeurMode] ========================================");
|
|
log.Warning($"[ChauffeurMode] IsWaitingForHelper: {isWaitingForHelper}, IsTransportingQuester: {isTransportingQuester}");
|
|
if (helperWorkflowCts != null)
|
|
{
|
|
log.Information("[ChauffeurMode] Cancelling running helper workflow");
|
|
helperWorkflowCts.Cancel();
|
|
helperWorkflowCts.Dispose();
|
|
helperWorkflowCts = null;
|
|
}
|
|
isWaitingForHelper = false;
|
|
isTransportingQuester = false;
|
|
hasExecutedRidePillion = false;
|
|
targetPosition = null;
|
|
targetZoneId = 0u;
|
|
questerName = null;
|
|
StopNavigation();
|
|
if (config.IsHighLevelHelper)
|
|
{
|
|
if (!string.IsNullOrEmpty(config.AssignedQuester))
|
|
{
|
|
log.Information("[ChauffeurMode] [HELPER] Clearing assigned quester: " + config.AssignedQuester);
|
|
config.AssignedQuester = "";
|
|
}
|
|
config.CurrentHelperStatus = HelperStatus.Available;
|
|
config.Save();
|
|
log.Information("[ChauffeurMode] [HELPER] Status: Available");
|
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
|
if (localPlayer != null)
|
|
{
|
|
string helperName = localPlayer.Name.ToString();
|
|
ushort helperWorld = (ushort)localPlayer.HomeWorld.RowId;
|
|
crossProcessIPC.BroadcastHelperStatus(helperName, helperWorld, "Available");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void CheckHelperFollowing()
|
|
{
|
|
if (!config.EnableHelperFollowing)
|
|
{
|
|
if (isFollowingQuester)
|
|
{
|
|
StopFollowingQuester();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!config.IsHighLevelHelper)
|
|
{
|
|
return;
|
|
}
|
|
if (string.IsNullOrEmpty(config.AssignedQuesterForFollowing))
|
|
{
|
|
if (isFollowingQuester)
|
|
{
|
|
log.Warning("[HelperFollowing] Stopped - no assigned quester configured!");
|
|
StopFollowingQuester();
|
|
}
|
|
return;
|
|
}
|
|
if (condition[ConditionFlag.BoundByDuty] || condition[ConditionFlag.BoundByDuty56] || condition[ConditionFlag.BoundByDuty95])
|
|
{
|
|
log.Debug("[HelperFollowing] Skipping - in duty/dungeon");
|
|
if (isFollowingQuester)
|
|
{
|
|
log.Information("[HelperFollowing] Stopping - entered duty/dungeon");
|
|
StopFollowingQuester();
|
|
}
|
|
return;
|
|
}
|
|
if (isTransportingQuester)
|
|
{
|
|
log.Debug("[HelperFollowing] Skipping - currently transporting quester");
|
|
if (isFollowingQuester)
|
|
{
|
|
log.Information("[HelperFollowing] Stopping - Chauffeur Mode active");
|
|
StopFollowingQuester();
|
|
}
|
|
return;
|
|
}
|
|
ushort currentZone = clientState.TerritoryType;
|
|
if (restrictedZones.Contains(currentZone))
|
|
{
|
|
log.Debug($"[HelperFollowing] Skipping - in restricted zone {currentZone}");
|
|
if (isFollowingQuester)
|
|
{
|
|
log.Information($"[HelperFollowing] Stopping - entered restricted zone {currentZone}");
|
|
StopFollowingQuester();
|
|
}
|
|
return;
|
|
}
|
|
if (BLACKLISTED_ZONES.Contains(currentZone))
|
|
{
|
|
log.Debug($"[HelperFollowing] Skipping - in blacklisted zone {currentZone}");
|
|
if (isFollowingQuester)
|
|
{
|
|
log.Information($"[HelperFollowing] Stopping - entered blacklisted zone {currentZone}");
|
|
StopFollowingQuester();
|
|
}
|
|
return;
|
|
}
|
|
DateTime now = DateTime.Now;
|
|
if ((now - lastFollowCheck).TotalSeconds < (double)config.HelperFollowCheckInterval)
|
|
{
|
|
return;
|
|
}
|
|
lastFollowCheck = now;
|
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
|
if (localPlayer == null)
|
|
{
|
|
return;
|
|
}
|
|
if (!lastQuesterPosition.HasValue || lastQuesterZone == 0)
|
|
{
|
|
if (isFollowingQuester)
|
|
{
|
|
log.Information("[HelperFollowing] Stopped - no quester position data");
|
|
StopFollowingQuester();
|
|
}
|
|
return;
|
|
}
|
|
if (restrictedZones.Contains(lastQuesterZone))
|
|
{
|
|
log.Debug($"[HelperFollowing] Skipping - Quester is in restricted zone {lastQuesterZone}");
|
|
if (isFollowingQuester)
|
|
{
|
|
log.Information($"[HelperFollowing] Stopping - Quester entered restricted zone {lastQuesterZone}");
|
|
StopFollowingQuester();
|
|
}
|
|
return;
|
|
}
|
|
if (BLACKLISTED_ZONES.Contains(lastQuesterZone))
|
|
{
|
|
log.Debug($"[HelperFollowing] Skipping - Quester is in blacklisted zone {lastQuesterZone}");
|
|
if (isFollowingQuester)
|
|
{
|
|
log.Information($"[HelperFollowing] Stopping - Quester entered blacklisted zone {lastQuesterZone}");
|
|
StopFollowingQuester();
|
|
}
|
|
return;
|
|
}
|
|
uint questerZone = lastQuesterZone;
|
|
if (questerZone != currentZone)
|
|
{
|
|
if (!IsMountingAllowed(questerZone))
|
|
{
|
|
log.Debug($"[HelperFollowing] Skipping - Quester's zone {questerZone} does not allow mounting/flying");
|
|
if (isFollowingQuester)
|
|
{
|
|
log.Information($"[HelperFollowing] Stopping - Quester entered non-flying zone {questerZone}");
|
|
StopFollowingQuester();
|
|
}
|
|
return;
|
|
}
|
|
if (HasFlyingInZone(questerZone))
|
|
{
|
|
log.Debug($"[HelperFollowing] Quester can fly in zone {questerZone} - no need to follow");
|
|
if (isFollowingQuester)
|
|
{
|
|
log.Information("[HelperFollowing] Stopping - Quester can fly in their zone");
|
|
StopFollowingQuester();
|
|
}
|
|
return;
|
|
}
|
|
log.Information($"[HelperFollowing] Quester in different zone ({questerZone} vs {currentZone}) - teleporting");
|
|
Task.Run(async delegate
|
|
{
|
|
try
|
|
{
|
|
if (await TeleportToZone(questerZone))
|
|
{
|
|
log.Information($"[HelperFollowing] Successfully teleported to zone {questerZone}");
|
|
}
|
|
else
|
|
{
|
|
log.Warning($"[HelperFollowing] Failed to teleport to zone {questerZone}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[HelperFollowing] Error teleporting to zone: " + ex.Message);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
Vector3 questerPos = lastQuesterPosition.Value;
|
|
float distance = Vector3.Distance(localPlayer.Position, questerPos);
|
|
log.Debug($"[HelperFollowing] Distance to quester: {distance:F1} yalms (threshold: {config.HelperFollowDistance})");
|
|
if (distance > config.HelperFollowDistance)
|
|
{
|
|
log.Information($"[HelperFollowing] Distance {distance:F1} > {config.HelperFollowDistance} - navigating to quester");
|
|
if (!condition[ConditionFlag.Mounted] && config.ChauffeurMountId != 0)
|
|
{
|
|
log.Information("[HelperFollowing] Not mounted - summoning Chauffeur mount");
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
SummonMountDirect(config.ChauffeurMountId);
|
|
});
|
|
return;
|
|
}
|
|
NavigateToQuester(questerPos);
|
|
if (!isFollowingQuester)
|
|
{
|
|
isFollowingQuester = true;
|
|
log.Information("[HelperFollowing] Started following " + followingQuesterName);
|
|
}
|
|
}
|
|
else if (isFollowingQuester)
|
|
{
|
|
log.Debug($"[HelperFollowing] Within range ({distance:F1} yalms) - stopping navigation");
|
|
StopNavigation();
|
|
}
|
|
lastQuesterPosition = questerPos;
|
|
lastQuesterZone = questerZone;
|
|
}
|
|
}
|
|
|
|
private void BroadcastQuesterPosition()
|
|
{
|
|
if (!config.IsQuester || string.IsNullOrEmpty(config.AssignedHelperForFollowing))
|
|
{
|
|
return;
|
|
}
|
|
if (condition[ConditionFlag.BoundByDuty] || condition[ConditionFlag.BoundByDuty56] || condition[ConditionFlag.BoundByDuty95])
|
|
{
|
|
log.Debug("[HelperFollowing] Skipping broadcast - in duty/dungeon");
|
|
return;
|
|
}
|
|
if (lastZoneChangeTime.HasValue)
|
|
{
|
|
double timeSinceZoneChange = (DateTime.Now - lastZoneChangeTime.Value).TotalSeconds;
|
|
if (timeSinceZoneChange < 8.0)
|
|
{
|
|
log.Debug($"[ChauffeurMode] [HelperFollowing] Territory Load State: Waiting for zone load (elapsed: {timeSinceZoneChange:F1}s / 8.0s)");
|
|
return;
|
|
}
|
|
log.Information($"[ChauffeurMode] [HelperFollowing] Territory Load State: Zone load complete ({timeSinceZoneChange:F1}s) - resuming position broadcasts");
|
|
lastZoneChangeTime = null;
|
|
}
|
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
|
if (localPlayer != null)
|
|
{
|
|
string questerName = localPlayer.Name.ToString();
|
|
ushort questerWorld = (ushort)localPlayer.HomeWorld.RowId;
|
|
ushort currentZone = clientState.TerritoryType;
|
|
Vector3 position = localPlayer.Position;
|
|
crossProcessIPC.BroadcastQuesterPosition(questerName, questerWorld, currentZone, position);
|
|
}
|
|
}
|
|
|
|
private void OnQuesterPositionUpdate(string questerName, ushort questerWorld, uint zoneId, Vector3 position)
|
|
{
|
|
if (!config.IsHighLevelHelper)
|
|
{
|
|
return;
|
|
}
|
|
string worldName = (dataManager.GetExcelSheet<World>()?.GetRowOrDefault(questerWorld))?.Name.ToString() ?? questerWorld.ToString();
|
|
string receivedQuesterKey = questerName + "@" + worldName;
|
|
discoveredQuesters[receivedQuesterKey] = DateTime.Now;
|
|
if (config.EnableHelperFollowing)
|
|
{
|
|
if (string.IsNullOrEmpty(config.AssignedQuesterForFollowing))
|
|
{
|
|
log.Debug("[HelperFollowing] No assigned quester - ignoring position update");
|
|
return;
|
|
}
|
|
if (receivedQuesterKey != config.AssignedQuesterForFollowing)
|
|
{
|
|
log.Debug("[HelperFollowing] Ignoring position from " + receivedQuesterKey + " - assigned quester is " + config.AssignedQuesterForFollowing);
|
|
return;
|
|
}
|
|
lastQuesterPosition = position;
|
|
lastQuesterZone = zoneId;
|
|
followingQuesterName = receivedQuesterKey;
|
|
log.Debug($"[HelperFollowing] Updated position for {followingQuesterName}: zone {zoneId}, pos ({position.X:F1},{position.Y:F1},{position.Z:F1})");
|
|
}
|
|
}
|
|
|
|
private IPartyMember? FindQuesterInParty()
|
|
{
|
|
if (partyList == null || partyList.Length == 0)
|
|
{
|
|
return null;
|
|
}
|
|
IPlayerCharacter localPlayer = clientState.LocalPlayer;
|
|
if (localPlayer == null)
|
|
{
|
|
return null;
|
|
}
|
|
string helperName = localPlayer.Name.ToString();
|
|
uint helperWorld = localPlayer.HomeWorld.RowId;
|
|
for (int i = 0; i < partyList.Length; i++)
|
|
{
|
|
IPartyMember member = partyList[i];
|
|
if (member == null)
|
|
{
|
|
continue;
|
|
}
|
|
string memberName = member.Name.ToString();
|
|
uint memberWorld = member.World.RowId;
|
|
if (!(memberName == helperName) || memberWorld != helperWorld)
|
|
{
|
|
if (string.IsNullOrEmpty(config.AssignedQuester))
|
|
{
|
|
log.Debug("[HelperFollowing] No assigned quester - following first party member: " + memberName);
|
|
return member;
|
|
}
|
|
string assignedQuester = config.AssignedQuester;
|
|
string memberKey = $"{memberName}@{memberWorld}";
|
|
if (assignedQuester == memberKey)
|
|
{
|
|
log.Debug("[HelperFollowing] Found assigned quester: " + memberName);
|
|
return member;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private void NavigateToQuester(Vector3 position)
|
|
{
|
|
try
|
|
{
|
|
string command = $"/vnav flyto {position.X.ToString(CultureInfo.InvariantCulture)} {position.Y.ToString(CultureInfo.InvariantCulture)} {position.Z.ToString(CultureInfo.InvariantCulture)}";
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
commandManager.ProcessCommand(command);
|
|
log.Debug("[HelperFollowing] Sent: " + command);
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[HelperFollowing] Error navigating to quester: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private void StopNavigation()
|
|
{
|
|
try
|
|
{
|
|
framework.RunOnFrameworkThread(delegate
|
|
{
|
|
commandManager.ProcessCommand("/vnav stop");
|
|
log.Debug("[HelperFollowing] Stopped navigation");
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Error("[HelperFollowing] Error stopping navigation: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private void StopFollowingQuester()
|
|
{
|
|
if (isFollowingQuester)
|
|
{
|
|
log.Information("[HelperFollowing] Stopped following " + followingQuesterName);
|
|
StopNavigation();
|
|
isFollowingQuester = false;
|
|
followingQuesterName = null;
|
|
lastQuesterPosition = null;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
isDisposed = true;
|
|
if (isFollowingQuester)
|
|
{
|
|
StopFollowingQuester();
|
|
}
|
|
if (crossProcessIPC != null)
|
|
{
|
|
crossProcessIPC.OnChauffeurSummonRequest -= OnChauffeurSummonRequest;
|
|
crossProcessIPC.OnChauffeurReadyForPickup -= OnChauffeurReadyForPickup;
|
|
crossProcessIPC.OnChauffeurArrived -= OnChauffeurArrived;
|
|
crossProcessIPC.OnChauffeurZoneUpdate -= OnChauffeurZoneUpdate;
|
|
crossProcessIPC.OnChauffeurMountReady -= OnChauffeurMountReady;
|
|
crossProcessIPC.OnHelperStatusUpdate -= OnHelperStatusUpdate;
|
|
crossProcessIPC.OnQuesterPositionUpdate -= OnQuesterPositionUpdate;
|
|
}
|
|
if (clientState != null)
|
|
{
|
|
clientState.TerritoryChanged -= OnTerritoryChanged;
|
|
}
|
|
vnavmeshIPC?.Dispose();
|
|
if (framework != null)
|
|
{
|
|
framework.Update -= OnFrameworkUpdate;
|
|
}
|
|
log.Information("[ChauffeurMode] Service disposed");
|
|
}
|
|
}
|