1
0
Fork 0
forked from aly/qstbak
qstbak/Questionable/Questionable.External/BossModIpc.cs
2026-08-17 20:29:32 +10:00

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;
}
}