forked from aly/qstbak
584 lines
16 KiB
C#
584 lines
16 KiB
C#
using System;
|
|
using System.Collections.Frozen;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using Dalamud.Plugin;
|
|
using Dalamud.Plugin.Ipc;
|
|
using Dalamud.Plugin.Ipc.Exceptions;
|
|
using Dalamud.Plugin.Services;
|
|
using LLib.GameData;
|
|
using Microsoft.Extensions.Logging;
|
|
using Questionable.Data;
|
|
using Questionable.Model.Questing;
|
|
|
|
namespace Questionable.External;
|
|
|
|
internal sealed class BossModIpc : IDisposable
|
|
{
|
|
private sealed record Strategy(string Module, string Track, string Option);
|
|
|
|
public enum EPreset
|
|
{
|
|
Active,
|
|
Passive
|
|
}
|
|
|
|
public enum EVariant
|
|
{
|
|
VBM,
|
|
BMR
|
|
}
|
|
|
|
private const string ActivePresetName = "Questionable";
|
|
|
|
private const string PassivePresetName = "Questionable (Passive)";
|
|
|
|
private readonly IObjectTable _objectTable;
|
|
|
|
private readonly IDalamudPluginInterface _pluginInterface;
|
|
|
|
private readonly Configuration _configuration;
|
|
|
|
private readonly ICommandManager _commandManager;
|
|
|
|
private readonly TerritoryData _territoryData;
|
|
|
|
private readonly IFramework _framework;
|
|
|
|
private readonly ILogger<BossModIpc> _logger;
|
|
|
|
private readonly ICallGateSubscriber<string, string?> _getPreset;
|
|
|
|
private readonly ICallGateSubscriber<string, bool, bool> _createPreset;
|
|
|
|
private readonly ICallGateSubscriber<string, bool> _setPreset;
|
|
|
|
private readonly ICallGateSubscriber<bool> _clearPreset;
|
|
|
|
private readonly ICallGateSubscriber<string, string, string, string, bool> _addTransientStrategy;
|
|
|
|
private readonly ICallGateSubscriber<string, bool> _clearTransientStrategies;
|
|
|
|
private readonly ICallGateSubscriber<string, bool> _deletePreset;
|
|
|
|
private readonly ICallGateSubscriber<List<string>, bool, List<string>> _setConfig;
|
|
|
|
private bool _soloDutyZoneConfigured;
|
|
|
|
private static readonly Strategy[] VbmAiStrategies = new Strategy[14]
|
|
{
|
|
new Strategy("BossMod.Autorotation.xan.TankAI", "Stance", "Enabled"),
|
|
new Strategy("BossMod.Autorotation.xan.TankAI", "Personal mits", "Enabled"),
|
|
new Strategy("BossMod.Autorotation.xan.TankAI", "Invuln", "Enabled"),
|
|
new Strategy("BossMod.Autorotation.xan.TankAI", "Interject2", "Enabled"),
|
|
new Strategy("BossMod.Autorotation.xan.TankAI", "Arms' Length", "Enabled"),
|
|
new Strategy("BossMod.Autorotation.xan.HealerAI", "Heal", "Enabled"),
|
|
new Strategy("BossMod.Autorotation.xan.HealerAI", "Esuna2", "Enabled"),
|
|
new Strategy("BossMod.Autorotation.xan.HealerAI", "Raise", "Swiftcast"),
|
|
new Strategy("BossMod.Autorotation.xan.MeleeAI", "Second Wind", "Enabled"),
|
|
new Strategy("BossMod.Autorotation.xan.MeleeAI", "Bloodbath", "Enabled"),
|
|
new Strategy("BossMod.Autorotation.xan.MeleeAI", "Stun", "Enabled"),
|
|
new Strategy("BossMod.Autorotation.xan.RangedAI", "Head Graze", "Enabled"),
|
|
new Strategy("BossMod.Autorotation.xan.RangedAI", "Second Wind", "Enabled"),
|
|
new Strategy("BossMod.Autorotation.MiscAI.NormalMovement", "Destination", "Pathfind")
|
|
};
|
|
|
|
private static readonly Strategy[] BmrAiStrategies = new Strategy[14]
|
|
{
|
|
new Strategy("BossMod.Autorotation.MiscAI.TankAI", "Stance", "Auto"),
|
|
new Strategy("BossMod.Autorotation.MiscAI.TankAI", "Mitigations", "Auto"),
|
|
new Strategy("BossMod.Autorotation.MiscAI.TankAI", "Invuln", "Auto"),
|
|
new Strategy("BossMod.Autorotation.MiscAI.TankAI", "Interject", "Auto"),
|
|
new Strategy("BossMod.Autorotation.MiscAI.TankAI", "ArmsLength", "Auto"),
|
|
new Strategy("BossMod.Autorotation.MiscAI.HealerAI", "Heal", "Auto"),
|
|
new Strategy("BossMod.Autorotation.MiscAI.HealerAI", "Esuna", "Auto"),
|
|
new Strategy("BossMod.Autorotation.MiscAI.HealerAI", "Raise", "Auto"),
|
|
new Strategy("BossMod.Autorotation.MiscAI.MeleeAI", "SecondWind", "Auto"),
|
|
new Strategy("BossMod.Autorotation.MiscAI.MeleeAI", "Bloodbath", "Auto"),
|
|
new Strategy("BossMod.Autorotation.MiscAI.MeleeAI", "Stun", "Auto"),
|
|
new Strategy("BossMod.Autorotation.MiscAI.RangedAI", "HeadGraze", "Auto"),
|
|
new Strategy("BossMod.Autorotation.MiscAI.RangedAI", "SecondWind", "Auto"),
|
|
new Strategy("BossMod.Autorotation.MiscAI.NormalMovement", "Destination", "Pathfind")
|
|
};
|
|
|
|
private static readonly Strategy[] ActivePositioningStrategies = new Strategy[2]
|
|
{
|
|
new Strategy("BossMod.Autorotation.MiscAI.StayCloseToTarget", "Range", "3"),
|
|
new Strategy("BossMod.Autorotation.MiscAI.GoToPositional", "Positional", "Any")
|
|
};
|
|
|
|
private static readonly FrozenDictionary<EClassJob, string> JobModules = new Dictionary<EClassJob, string>
|
|
{
|
|
{
|
|
EClassJob.Paladin,
|
|
"BossMod.Autorotation.xan.PLD"
|
|
},
|
|
{
|
|
EClassJob.Monk,
|
|
"BossMod.Autorotation.xan.MNK"
|
|
},
|
|
{
|
|
EClassJob.Dragoon,
|
|
"BossMod.Autorotation.xan.DRG"
|
|
},
|
|
{
|
|
EClassJob.Bard,
|
|
"BossMod.Autorotation.xan.BRD"
|
|
},
|
|
{
|
|
EClassJob.WhiteMage,
|
|
"BossMod.Autorotation.xan.WHM"
|
|
},
|
|
{
|
|
EClassJob.BlackMage,
|
|
"BossMod.Autorotation.xan.BLM"
|
|
},
|
|
{
|
|
EClassJob.Summoner,
|
|
"BossMod.Autorotation.xan.SMN"
|
|
},
|
|
{
|
|
EClassJob.Scholar,
|
|
"BossMod.Autorotation.xan.SCH"
|
|
},
|
|
{
|
|
EClassJob.Ninja,
|
|
"BossMod.Autorotation.xan.NIN"
|
|
},
|
|
{
|
|
EClassJob.Machinist,
|
|
"BossMod.Autorotation.xan.MCH"
|
|
},
|
|
{
|
|
EClassJob.DarkKnight,
|
|
"BossMod.Autorotation.xan.DRK"
|
|
},
|
|
{
|
|
EClassJob.Astrologian,
|
|
"BossMod.Autorotation.xan.AST"
|
|
},
|
|
{
|
|
EClassJob.Samurai,
|
|
"BossMod.Autorotation.xan.SAM"
|
|
},
|
|
{
|
|
EClassJob.RedMage,
|
|
"BossMod.Autorotation.xan.RDM"
|
|
},
|
|
{
|
|
EClassJob.Gunbreaker,
|
|
"BossMod.Autorotation.xan.GNB"
|
|
},
|
|
{
|
|
EClassJob.Dancer,
|
|
"BossMod.Autorotation.xan.DNC"
|
|
},
|
|
{
|
|
EClassJob.Reaper,
|
|
"BossMod.Autorotation.xan.RPR"
|
|
},
|
|
{
|
|
EClassJob.Sage,
|
|
"BossMod.Autorotation.xan.SGE"
|
|
},
|
|
{
|
|
EClassJob.Viper,
|
|
"BossMod.Autorotation.xan.VPR"
|
|
},
|
|
{
|
|
EClassJob.Pictomancer,
|
|
"BossMod.Autorotation.xan.PCT"
|
|
}
|
|
}.ToFrozenDictionary();
|
|
|
|
public EVariant? Variant => DetectVariantFromPlugins();
|
|
|
|
public BossModIpc(IObjectTable objectTable, IDalamudPluginInterface pluginInterface, Configuration configuration, ICommandManager commandManager, TerritoryData territoryData, IFramework framework, ILogger<BossModIpc> logger)
|
|
{
|
|
_objectTable = objectTable;
|
|
_pluginInterface = pluginInterface;
|
|
_configuration = configuration;
|
|
_commandManager = commandManager;
|
|
_territoryData = territoryData;
|
|
_framework = framework;
|
|
_logger = logger;
|
|
_getPreset = pluginInterface.GetIpcSubscriber<string, string>("BossMod.Presets.Get");
|
|
_createPreset = pluginInterface.GetIpcSubscriber<string, bool, bool>("BossMod.Presets.Create");
|
|
_setPreset = pluginInterface.GetIpcSubscriber<string, bool>("BossMod.Presets.SetActive");
|
|
_clearPreset = pluginInterface.GetIpcSubscriber<bool>("BossMod.Presets.ClearActive");
|
|
_addTransientStrategy = pluginInterface.GetIpcSubscriber<string, string, string, string, bool>("BossMod.Presets.AddTransientStrategy");
|
|
_clearTransientStrategies = pluginInterface.GetIpcSubscriber<string, bool>("BossMod.Presets.ClearTransientPresetStrategies");
|
|
_deletePreset = pluginInterface.GetIpcSubscriber<string, bool>("BossMod.Presets.Delete");
|
|
_setConfig = pluginInterface.GetIpcSubscriber<List<string>, bool, List<string>>("BossMod.Configuration");
|
|
_framework.RunOnFrameworkThread(delegate
|
|
{
|
|
if (!IsSupported())
|
|
{
|
|
return;
|
|
}
|
|
try
|
|
{
|
|
_deletePreset.InvokeFunc("Questionable - Quest Battles");
|
|
}
|
|
catch (IpcError)
|
|
{
|
|
}
|
|
try
|
|
{
|
|
_deletePreset.InvokeFunc("Questionable");
|
|
}
|
|
catch (IpcError)
|
|
{
|
|
}
|
|
try
|
|
{
|
|
_deletePreset.InvokeFunc("Questionable (Passive)");
|
|
}
|
|
catch (IpcError)
|
|
{
|
|
}
|
|
});
|
|
}
|
|
|
|
public bool IsSupported()
|
|
{
|
|
try
|
|
{
|
|
return _getPreset.HasFunction;
|
|
}
|
|
catch (IpcError)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private EVariant? DetectVariantFromPlugins()
|
|
{
|
|
IExposedPlugin exposedPlugin = _pluginInterface.InstalledPlugins.FirstOrDefault(delegate(IExposedPlugin p)
|
|
{
|
|
string internalName = p.InternalName;
|
|
return ((internalName == "BossMod" || internalName == "BossModReborn") ? true : false) && p.IsLoaded;
|
|
});
|
|
if (exposedPlugin == null)
|
|
{
|
|
return null;
|
|
}
|
|
return (exposedPlugin.InternalName == "BossModReborn") ? EVariant.BMR : EVariant.VBM;
|
|
}
|
|
|
|
private bool CreateShellPreset(EPreset preset)
|
|
{
|
|
try
|
|
{
|
|
string presetName = GetPresetName(preset);
|
|
IEnumerable<string> source = (from x in GetAiStrategies().Concat((preset == EPreset.Active) ? ((IEnumerable<Strategy>)ActivePositioningStrategies) : ((IEnumerable<Strategy>)Array.Empty<Strategy>()))
|
|
select x.Module).Distinct();
|
|
string jobModule = GetJobModule(GetCurrentJob());
|
|
if (jobModule != null)
|
|
{
|
|
source = source.Append(jobModule);
|
|
}
|
|
string value = string.Join(',', source.Select((string x) => "\"" + x + "\":[]"));
|
|
string arg = $"{{\"Name\":\"{presetName}\",\"Modules\":{{ {value} }}}}";
|
|
if (!_createPreset.InvokeFunc(arg, arg2: true))
|
|
{
|
|
_logger.LogWarning("BossMod rejected shell preset {Preset}", preset);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
catch (IpcError exception)
|
|
{
|
|
_logger.LogWarning(exception, "Failed to create shell preset {Preset}", preset);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void ApplyTransientStrategies(EPreset preset)
|
|
{
|
|
string presetName = GetPresetName(preset);
|
|
Strategy[] aiStrategies = GetAiStrategies();
|
|
foreach (Strategy strategy in aiStrategies)
|
|
{
|
|
AddStrategy(presetName, strategy.Module, strategy.Track, strategy.Option);
|
|
}
|
|
if (preset != EPreset.Active)
|
|
{
|
|
return;
|
|
}
|
|
aiStrategies = ActivePositioningStrategies;
|
|
foreach (Strategy strategy2 in aiStrategies)
|
|
{
|
|
AddStrategy(presetName, strategy2.Module, strategy2.Track, strategy2.Option);
|
|
}
|
|
EClassJob currentJob = GetCurrentJob();
|
|
if (JobModules.TryGetValue(currentJob, out string value))
|
|
{
|
|
AddStrategy(presetName, value, "AOE", "AOE");
|
|
AddStrategy(presetName, value, "Targeting", "Manual");
|
|
if (currentJob != EClassJob.Monk && currentJob != EClassJob.Sage)
|
|
{
|
|
AddStrategy(presetName, value, "Buffs", (Variant == EVariant.VBM) ? "Automatic" : "Auto");
|
|
}
|
|
if (currentJob == EClassJob.Pictomancer)
|
|
{
|
|
AddStrategy(presetName, value, "Motifs", "Downtime");
|
|
}
|
|
}
|
|
else if (currentJob == EClassJob.Warrior)
|
|
{
|
|
AddStrategy(presetName, "BossMod.Autorotation.VeynWAR", "AOE", "AutoFinishCombo");
|
|
}
|
|
else
|
|
{
|
|
_logger.LogDebug("No BossMod rotation module mapped for job {Job}", currentJob);
|
|
}
|
|
}
|
|
|
|
private void ClearTransientStrategies(EPreset preset)
|
|
{
|
|
try
|
|
{
|
|
_clearTransientStrategies.InvokeFunc(GetPresetName(preset));
|
|
}
|
|
catch (IpcError exception)
|
|
{
|
|
_logger.LogWarning(exception, "Failed to clear transient strategies for {Preset}", preset);
|
|
}
|
|
}
|
|
|
|
private void AddStrategy(string presetName, string module, string track, string option)
|
|
{
|
|
try
|
|
{
|
|
if (!_addTransientStrategy.InvokeFunc(presetName, module, track, option))
|
|
{
|
|
_logger.LogWarning("BossMod rejected transient strategy {Module}.{Track}={Option}", module, track, option);
|
|
}
|
|
}
|
|
catch (IpcError exception)
|
|
{
|
|
_logger.LogWarning(exception, "Failed to add transient strategy {Module}.{Track}={Option}", module, track, option);
|
|
}
|
|
}
|
|
|
|
private EClassJob GetCurrentJob()
|
|
{
|
|
return ((EClassJob?)_objectTable.LocalPlayer?.ClassJob.RowId).GetValueOrDefault().AsJob();
|
|
}
|
|
|
|
private static string? GetJobModule(EClassJob job)
|
|
{
|
|
if (!JobModules.TryGetValue(job, out string value))
|
|
{
|
|
if (job != EClassJob.Warrior)
|
|
{
|
|
return null;
|
|
}
|
|
return "BossMod.Autorotation.VeynWAR";
|
|
}
|
|
return value;
|
|
}
|
|
|
|
public void AddTransientStrategy(EPreset preset, string moduleType, string track, string value)
|
|
{
|
|
AddStrategy(GetPresetName(preset), moduleType, track, value);
|
|
}
|
|
|
|
public void SetRangeStrategy(EPreset preset, bool isRanged)
|
|
{
|
|
AddTransientStrategy(preset, "BossMod.Autorotation.MiscAI.StayCloseToTarget", "Range", isRanged ? "15" : "3");
|
|
}
|
|
|
|
public bool EnableAi(EPreset preset)
|
|
{
|
|
if (!CreateShellPreset(preset))
|
|
{
|
|
return false;
|
|
}
|
|
ApplyTransientStrategies(preset);
|
|
try
|
|
{
|
|
if (!_setPreset.InvokeFunc(GetPresetName(preset)))
|
|
{
|
|
_logger.LogWarning("BossMod rejected preset activation for {Preset}", preset);
|
|
return false;
|
|
}
|
|
}
|
|
catch (IpcError ipcError)
|
|
{
|
|
_logger.LogWarning("Unable to set BossMod preset {Preset}: {Message}", preset, ipcError.Message);
|
|
return false;
|
|
}
|
|
string aiCommand = GetAiCommand();
|
|
if (aiCommand != null)
|
|
{
|
|
_logger.LogDebug("Enabling BossMod AI: variant={Variant}, preset={Preset}, command='{Command} on'", Variant, preset, aiCommand);
|
|
_commandManager.ProcessCommand(aiCommand + " on");
|
|
return true;
|
|
}
|
|
_logger.LogWarning("Cannot enable BossMod AI: no variant detected");
|
|
return false;
|
|
}
|
|
|
|
public void EnableQuestBattleAi()
|
|
{
|
|
EnableAi(EPreset.Active);
|
|
ConfigureSoloDutyZone();
|
|
}
|
|
|
|
public void DisableQuestBattleAi()
|
|
{
|
|
ReleaseSoloDutyZone();
|
|
DisableAi();
|
|
}
|
|
|
|
public void DisableAi()
|
|
{
|
|
string aiCommand = GetAiCommand();
|
|
if (aiCommand != null)
|
|
{
|
|
_logger.LogDebug("Disabling BossMod AI: command='{Command} off'", aiCommand);
|
|
_commandManager.ProcessCommand(aiCommand + " off");
|
|
}
|
|
ClearTransientStrategies(EPreset.Active);
|
|
ClearTransientStrategies(EPreset.Passive);
|
|
try
|
|
{
|
|
_clearPreset.InvokeFunc();
|
|
}
|
|
catch (IpcError exception)
|
|
{
|
|
_logger.LogWarning(exception, "Failed to clear BossMod preset");
|
|
}
|
|
}
|
|
|
|
public bool IsConfiguredToRunSoloInstance(ElementId questId, SinglePlayerDutyOptions? dutyOptions)
|
|
{
|
|
if (!IsSupported())
|
|
{
|
|
return false;
|
|
}
|
|
if (!_configuration.SinglePlayerDuties.RunSoloInstancesWithBossMod)
|
|
{
|
|
return false;
|
|
}
|
|
byte index = dutyOptions?.Index ?? 0;
|
|
if (!_territoryData.TryGetContentFinderConditionForSoloInstance(questId, index, out TerritoryData.ContentFinderConditionData contentFinderConditionData))
|
|
{
|
|
return false;
|
|
}
|
|
if (_configuration.SinglePlayerDuties.BlacklistedSinglePlayerDutyCfcIds.Contains(contentFinderConditionData.ContentFinderConditionId))
|
|
{
|
|
return false;
|
|
}
|
|
if (_configuration.SinglePlayerDuties.WhitelistedSinglePlayerDutyCfcIds.Contains(contentFinderConditionData.ContentFinderConditionId))
|
|
{
|
|
return true;
|
|
}
|
|
ushort key = (ushort)((questId is QuestId questId2) ? questId2.Value : 0);
|
|
if (DutyRegistry.SinglePlayerDuties.TryGetValue(key, out SinglePlayerDutyEntry[] value))
|
|
{
|
|
return value.Any((SinglePlayerDutyEntry e) => e.Index == index && e.Enabled);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
ReleaseSoloDutyZone();
|
|
try
|
|
{
|
|
_deletePreset.InvokeFunc("Questionable");
|
|
}
|
|
catch (IpcError)
|
|
{
|
|
}
|
|
try
|
|
{
|
|
_deletePreset.InvokeFunc("Questionable (Passive)");
|
|
}
|
|
catch (IpcError)
|
|
{
|
|
}
|
|
}
|
|
|
|
private string? GetAiCommand()
|
|
{
|
|
EVariant? eVariant = DetectVariantFromPlugins();
|
|
if (!eVariant.HasValue)
|
|
{
|
|
_logger.LogWarning("No BossMod variant detected");
|
|
return null;
|
|
}
|
|
if (eVariant != EVariant.BMR)
|
|
{
|
|
return "/vbmai";
|
|
}
|
|
return "/bmrai";
|
|
}
|
|
|
|
private void SetConfigTransient(string section, string key, string value)
|
|
{
|
|
try
|
|
{
|
|
ICallGateSubscriber<List<string>, bool, List<string>> setConfig = _setConfig;
|
|
int num = 3;
|
|
List<string> list = new List<string>(num);
|
|
CollectionsMarshal.SetCount(list, num);
|
|
Span<string> span = CollectionsMarshal.AsSpan(list);
|
|
span[0] = section;
|
|
span[1] = key;
|
|
span[2] = value;
|
|
List<string> list2 = setConfig.InvokeFunc(list, arg2: false);
|
|
if (list2 != null && list2.Count > 0)
|
|
{
|
|
_logger.LogWarning("Setting BossMod config {Section}.{Key}={Value} returned: {Output}", section, key, value, string.Join("; ", list2));
|
|
}
|
|
}
|
|
catch (IpcError exception)
|
|
{
|
|
_logger.LogWarning(exception, "Failed to set BossMod config {Section}.{Key}={Value}", section, key, value);
|
|
}
|
|
}
|
|
|
|
private void ConfigureSoloDutyZone()
|
|
{
|
|
if (IsSupported())
|
|
{
|
|
SetConfigTransient("ZoneModuleConfig", "EnableQuestBattles", "true");
|
|
SetConfigTransient("Autorotation", "ClearPresetOnCombatEnd", "false");
|
|
_soloDutyZoneConfigured = true;
|
|
}
|
|
}
|
|
|
|
private void ReleaseSoloDutyZone()
|
|
{
|
|
if (_soloDutyZoneConfigured && IsSupported())
|
|
{
|
|
SetConfigTransient("ZoneModuleConfig", "EnableQuestBattles", "false");
|
|
SetConfigTransient("Autorotation", "ClearPresetOnCombatEnd", "true");
|
|
_soloDutyZoneConfigured = false;
|
|
}
|
|
}
|
|
|
|
private static string GetPresetName(EPreset preset)
|
|
{
|
|
return preset switch
|
|
{
|
|
EPreset.Active => "Questionable",
|
|
EPreset.Passive => "Questionable (Passive)",
|
|
_ => throw new ArgumentOutOfRangeException("preset"),
|
|
};
|
|
}
|
|
|
|
private Strategy[] GetAiStrategies()
|
|
{
|
|
if (Variant != EVariant.VBM)
|
|
{
|
|
return BmrAiStrategies;
|
|
}
|
|
return VbmAiStrategies;
|
|
}
|
|
}
|