Compare commits

..

No commits in common. "master" and "v0.1" have entirely different histories.
master ... v0.1

18 changed files with 197 additions and 1607 deletions

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "LLib"]
path = LLib
url = https://git.carvel.li/liza/LLib.git

1
LLib

@ -1 +0,0 @@
Subproject commit e206782c1106e1a5292a06af61783faef1ac0c42

View File

@ -2,8 +2,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squadronista", "Squadronista\Squadronista.csproj", "{71E7BF47-88EE-48F4-8FBE-F89F20F34F38}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squadronista", "Squadronista\Squadronista.csproj", "{71E7BF47-88EE-48F4-8FBE-F89F20F34F38}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LLib", "LLib\LLib.csproj", "{5D61CA78-142C-4DC8-A780-268DB96D8D5B}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -14,9 +12,5 @@ Global
{71E7BF47-88EE-48F4-8FBE-F89F20F34F38}.Debug|Any CPU.Build.0 = Debug|Any CPU {71E7BF47-88EE-48F4-8FBE-F89F20F34F38}.Debug|Any CPU.Build.0 = Debug|Any CPU
{71E7BF47-88EE-48F4-8FBE-F89F20F34F38}.Release|Any CPU.ActiveCfg = Release|Any CPU {71E7BF47-88EE-48F4-8FBE-F89F20F34F38}.Release|Any CPU.ActiveCfg = Release|Any CPU
{71E7BF47-88EE-48F4-8FBE-F89F20F34F38}.Release|Any CPU.Build.0 = Release|Any CPU {71E7BF47-88EE-48F4-8FBE-F89F20F34F38}.Release|Any CPU.Build.0 = Release|Any CPU
{5D61CA78-142C-4DC8-A780-268DB96D8D5B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D61CA78-142C-4DC8-A780-268DB96D8D5B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D61CA78-142C-4DC8-A780-268DB96D8D5B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D61CA78-142C-4DC8-A780-268DB96D8D5B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

File diff suppressed because it is too large Load Diff

View File

@ -1,45 +0,0 @@
using System;
namespace Squadronista.Solver;
internal class Attributes : IEquatable<Attributes>
{
public required int PhysicalAbility { get; init; }
public required int MentalAbility { get; init; }
public required int TacticalAbility { get; init; }
public bool Equals(Attributes? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return PhysicalAbility == other.PhysicalAbility && MentalAbility == other.MentalAbility && TacticalAbility == other.TacticalAbility;
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((Attributes)obj);
}
public override int GetHashCode()
{
return HashCode.Combine(PhysicalAbility, MentalAbility, TacticalAbility);
}
public static bool operator ==(Attributes? left, Attributes? right)
{
return Equals(left, right);
}
public static bool operator !=(Attributes? left, Attributes? right)
{
return !Equals(left, right);
}
public override string ToString()
{
return $"{PhysicalAbility} / {MentalAbility} / {TacticalAbility}";
}
}

View File

@ -6,8 +6,11 @@ namespace Squadronista.Solver;
/// Bonus stats from training, these apply to the whole squadron (e.g. if you have 120/80/80, and no members selected, /// Bonus stats from training, these apply to the whole squadron (e.g. if you have 120/80/80, and no members selected,
/// the mission would be at 120/80/80). /// the mission would be at 120/80/80).
/// </summary> /// </summary>
internal sealed class BonusAttributes : Attributes, IEquatable<BonusAttributes> internal sealed class BonusAttributes : IEquatable<BonusAttributes>
{ {
public required int PhysicalAbility { get; init; }
public required int MentalAbility { get; init; }
public required int TacticalAbility { get; init; }
public required int Cap { get; init; } public required int Cap { get; init; }
public BonusAttributes ApplyTraining(Training training) public BonusAttributes ApplyTraining(Training training)
@ -54,7 +57,7 @@ internal sealed class BonusAttributes : Attributes, IEquatable<BonusAttributes>
} }
} }
private static void Fix(ref int mainStat, int mainGained, ref int otherStatA, int otherGainedA, ref int otherStatB, int otherGainedB) private void Fix(ref int mainStat, int mainGained, ref int otherStatA, int otherGainedA, ref int otherStatB, int otherGainedB)
{ {
if (mainStat >= 0 || mainGained > 0) if (mainStat >= 0 || mainGained > 0)
return; return;

View File

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
@ -6,16 +5,16 @@ using Lumina.Excel.GeneratedSheets;
namespace Squadronista.Solver; namespace Squadronista.Solver;
internal sealed class SquadronMember : IEquatable<SquadronMember> internal sealed class SquadronMember
{ {
public required string Name { get; init; } public required string Name { get; init; }
public required int Level { get; init; } public required int Level { get; init; }
public required uint ClassJob { get; init; } public required uint ClassJob { get; init; }
// TODO
public required Race Race { get; init; } public required Race Race { get; init; }
public required int EnlistmentTimestamp { get; init; }
public uint Experience { get; init; } // TODO
public int Experience { get; init; }
public int PhysicalAbility => GrowthParams[Level].PhysicalAbility; public int PhysicalAbility => GrowthParams[Level].PhysicalAbility;
public int MentalAbility => GrowthParams[Level].MentalAbility; public int MentalAbility => GrowthParams[Level].MentalAbility;
public int TacticalAbility => GrowthParams[Level].TacticalAbility; public int TacticalAbility => GrowthParams[Level].TacticalAbility;
@ -34,44 +33,8 @@ internal sealed class SquadronMember : IEquatable<SquadronMember>
{ {
growthAsList.Add((growth.Physical[i], growth.Mental[i], growth.Tactical[i])); growthAsList.Add((growth.Physical[i], growth.Mental[i], growth.Tactical[i]));
} }
growthAsList.Add((growth.Unknown123, growth.Unknown184, growth.Unknown245)); growthAsList.Add((growth.Unknown123, growth.Unknown184, growth.Unknown245));
GrowthParams = growthAsList; GrowthParams = growthAsList;
return this; return this;
} }
public bool Equals(SquadronMember? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Name == other.Name && Level == other.Level && ClassJob == other.ClassJob && Race == other.Race &&
Experience == other.Experience && PhysicalAbility == other.PhysicalAbility && MentalAbility == other.MentalAbility && TacticalAbility == other.TacticalAbility;
}
public override bool Equals(object? obj)
{
return ReferenceEquals(this, obj) || obj is SquadronMember other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(Name, Level, ClassJob, (int)Race, Experience);
}
public static bool operator ==(SquadronMember? left, SquadronMember? right)
{
return Equals(left, right);
}
public static bool operator !=(SquadronMember? left, SquadronMember? right)
{
return !Equals(left, right);
}
public Attributes ToAttributes() => new()
{
PhysicalAbility = PhysicalAbility,
MentalAbility = MentalAbility,
TacticalAbility = TacticalAbility,
};
} }

View File

@ -1,11 +0,0 @@
using System;
namespace Squadronista.Solver;
[Flags]
public enum SquadronMemberUiInfo : int
{
Unknown1 = 1,
IsPartOfMission = 2,
NewChemistryAvailable = 8192,
}

View File

@ -38,21 +38,21 @@ internal sealed class SquadronSolver
_newTrainings = _calculatedTrainings.Keys.ToList(); _newTrainings = _calculatedTrainings.Keys.ToList();
} }
public IEnumerable<CalculationResult> SolveFor(SquadronMission mission, Attributes missionAttributes, int requiredMatchingStats) public IEnumerable<CalculationResult> SolveFor(SquadronMission mission)
{ {
int minPhysical = missionAttributes.PhysicalAbility; int minPhysical = mission.PhysicalAbility;
int minMental = missionAttributes.MentalAbility; int minMental = mission.MentalAbility;
int minTactical = missionAttributes.TacticalAbility; int minTactical = mission.TacticalAbility;
bool foundWithoutTraining = false; bool foundWithoutTraining = false;
List<CalculationResult> intermediates = CalculateForAllMemberCombinations(mission.Level, _state.Bonus); List<CalculationResult> intermediates = CalculateForAllMemberCombinations(mission.Level, _state.Bonus);
foreach (var x in intermediates) foreach (var x in intermediates)
{ {
int matchingStats = CountStatMatches(x, minPhysical, minMental, minTactical); //_pluginLog.Information($"{string.Join(" ", x.Members.Select(y => y.Name))} → {x.PhysicalAbility} / {x.MentalAbility} / {x.TacticalAbility}");
if (matchingStats >= requiredMatchingStats) if (x.PhysicalAbility >= minPhysical && x.MentalAbility >= minMental && x.TacticalAbility >= minTactical)
{ {
x.TrainingsCalculated = true; x.TrainingsCalculated = true;
yield return x.WithExtra(mission, matchingStats); yield return x.WithMission(mission);
foundWithoutTraining = true; foundWithoutTraining = true;
} }
@ -65,26 +65,19 @@ internal sealed class SquadronSolver
intermediates = CalculateForAllMemberCombinations(mission.Level, bonus); intermediates = CalculateForAllMemberCombinations(mission.Level, bonus);
foreach (var x in intermediates) foreach (var x in intermediates)
{ {
int matchingStats = CountStatMatches(x, minPhysical, minMental, minTactical); if (x.PhysicalAbility >= minPhysical && x.MentalAbility >= minMental &&
if (matchingStats >= requiredMatchingStats) x.TacticalAbility >= minTactical)
{ {
CalculateTrainingsForBonus(x); CalculateTrainingsForBonus(x);
if (x.TrainingsCalculated) if (x.TrainingsCalculated)
yield return x.WithExtra(mission, matchingStats); yield return x.WithMission(mission);
} }
} }
} }
} }
} }
private static int CountStatMatches(CalculationResult x, int minPhysical, int minMental, int minTactical)
{
return (x.PhysicalAbility >= minPhysical ? 1 : 0) +
(x.MentalAbility >= minMental ? 1 : 0) +
(x.TacticalAbility >= minTactical ? 1 : 0);
}
private List<CalculationResult> CalculateForAllMemberCombinations(int requiredLevel, BonusAttributes bonus) private List<CalculationResult> CalculateForAllMemberCombinations(int requiredLevel, BonusAttributes bonus)
{ {
return _memberCombinations return _memberCombinations
@ -138,7 +131,7 @@ internal sealed class SquadronSolver
if (_calculatedTrainings.TryGetValue(result.Bonus, out var calculatedTraining)) if (_calculatedTrainings.TryGetValue(result.Bonus, out var calculatedTraining))
{ {
//_pluginLog.Information($"Found existing steps: {string.Join(", ", calculatedTraining.Select(x => x.Name))}"); _pluginLog.Information($"Found existing steps: {string.Join(", ", calculatedTraining.Select(x => x.Name))}");
result.Trainings = calculatedTraining; result.Trainings = calculatedTraining;
result.TrainingsCalculated = true; result.TrainingsCalculated = true;
return; return;
@ -189,8 +182,6 @@ internal sealed class SquadronSolver
public sealed class CalculationResult public sealed class CalculationResult
{ {
public SquadronMission? Mission { get; private set; } public SquadronMission? Mission { get; private set; }
public int MatchingAttributes { get; private set; }
public int PhysicalAbility { get; } public int PhysicalAbility { get; }
public int MentalAbility { get; } public int MentalAbility { get; }
public int TacticalAbility { get; } public int TacticalAbility { get; }
@ -198,7 +189,6 @@ internal sealed class SquadronSolver
public BonusAttributes Bonus { get; } public BonusAttributes Bonus { get; }
public IReadOnlyList<Training> Trainings { get; set; } = new List<Training>().AsReadOnly(); public IReadOnlyList<Training> Trainings { get; set; } = new List<Training>().AsReadOnly();
public bool TrainingsCalculated { get; set; } public bool TrainingsCalculated { get; set; }
public int TotalLevel => Members.Sum(x => x.Level);
public CalculationResult(List<SquadronMember> members, BonusAttributes bonus) public CalculationResult(List<SquadronMember> members, BonusAttributes bonus)
{ {
@ -209,27 +199,10 @@ internal sealed class SquadronSolver
Bonus = bonus; Bonus = bonus;
} }
public CalculationResult WithExtra(SquadronMission mission, int matchingAttributes) public CalculationResult WithMission(SquadronMission mission)
{ {
Mission = mission; Mission = mission;
MatchingAttributes = matchingAttributes;
return this; return this;
} }
public int ToSuccessProbability() => MatchingAttributes == 3 ? 100 : 66;
public string ToLabel()
{
if (Trainings.Count == 0)
return $"{ToSuccessProbability()}%%, no training";
else
return $"{ToSuccessProbability()}%%";
}
}
public sealed class CalculationResults
{
public bool IsFlaggedMission { get; set; }
public List<CalculationResult> Results { get; } = new();
} }
} }

View File

@ -5,20 +5,9 @@ namespace Squadronista.Solver;
internal sealed class SquadronState internal sealed class SquadronState
{ {
private readonly Dictionary<(int id, Attributes attributes), Task<SquadronSolver.CalculationResults>> _calculationResults = new(); public required byte Rank { get; init; }
public required IReadOnlyList<SquadronMember> Members { get; init; } public required IReadOnlyList<SquadronMember> Members { get; init; }
public required BonusAttributes Bonus { get; set; } public required BonusAttributes Bonus { get; set; }
public required uint CurrentTraining { get; set; }
public Task<SquadronSolver.CalculationResults>? GetCalculation(SquadronMission mission, Attributes? attributes) public Task<SquadronSolver.CalculationResult?>? CalculationTask { get; set; }
{
if (attributes == null)
return null;
return _calculationResults.GetValueOrDefault((mission.Id, attributes));
}
public void SetCalculation(SquadronMission mission, Attributes attributes, Task<SquadronSolver.CalculationResults> task)
=> _calculationResults[(mission.Id, attributes)] = task;
} }

View File

@ -12,7 +12,7 @@ public class Training
public int CappedMentalGained => CalculateCapped(MentalGained, PhysicalGained, TacticalGained); public int CappedMentalGained => CalculateCapped(MentalGained, PhysicalGained, TacticalGained);
public int CappedTacticalGained => CalculateCapped(TacticalGained, PhysicalGained, MentalGained); public int CappedTacticalGained => CalculateCapped(TacticalGained, PhysicalGained, MentalGained);
private static int CalculateCapped(int mainStat, int otherA, int otherB) private int CalculateCapped(int mainStat, int otherA, int otherB)
{ {
if (mainStat > 0) if (mainStat > 0)
return mainStat; return mainStat;

View File

@ -1,7 +1,4 @@
using System.Collections.Generic; namespace Squadronista;
using Squadronista.Solver;
namespace Squadronista;
internal sealed class SquadronMission internal sealed class SquadronMission
{ {
@ -9,6 +6,7 @@ internal sealed class SquadronMission
public required string Name { get; init; } public required string Name { get; init; }
public required byte Level { get; init; } public required byte Level { get; init; }
public required bool IsFlaggedMission { get; init; } public required bool IsFlaggedMission { get; init; }
public int PhysicalAbility { get; init; }
public required IReadOnlyList<Attributes> PossibleAttributes { get; init; } public int MentalAbility { get; init; }
public int TacticalAbility { get; init; }
} }

View File

@ -1,13 +1,60 @@
<Project Sdk="Dalamud.NET.Sdk/9.0.2"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<Version>4.0</Version> <TargetFramework>net7.0-windows</TargetFramework>
<Version>0.1</Version>
<LangVersion>11.0</LangVersion>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>dist</OutputPath> <OutputPath>dist</OutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DebugType>portable</DebugType>
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<DebugType>portable</DebugType>
</PropertyGroup> </PropertyGroup>
<Import Project="..\LLib\LLib.targets"/> <PropertyGroup>
<Import Project="..\LLib\RenameZip.targets"/> <DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
<DalamudLibPath>$(DALAMUD_HOME)/</DalamudLibPath>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\LLib\LLib.csproj" /> <PackageReference Include="DalamudPackager" Version="2.1.12"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(DalamudLibPath)ImGui.NET.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<Target Name="RenameLatestZip" AfterTargets="PackagePlugin" Condition="'$(Configuration)' == 'Release'">
<Exec Command="rename $(OutDir)$(AssemblyName)\latest.zip $(AssemblyName)-$(Version).zip"/>
</Target>
</Project> </Project>

View File

@ -1,8 +1,9 @@
{ {
"Name": "Squadronista", "Name": "Squadronista",
"Author": "Liza Carvelli", "Author": "Liza Carvelli",
"Punchline": "Simplified Squadron Calculator, heavily inspired by https://ffxivsquadron.com/", "Punchline": "Simplified Squadron Calculator for Flagged Missions, heavily inspired by https://ffxivsquadron.com/",
"Description": "", "Description": "",
"RepoUrl": "https://git.carvel.li/liza/Squadronista", "RepoUrl": "https://git.carvel.li/liza/Squadronista",
"IconUrl": "https://plugins.carvel.li/icons/Squadronista.png" "IconUrl": "https://git.carvel.li/liza/plugin-repo/raw/branch/master/dist/Squadronista.png",
"IsTestingExclusive": true
} }

View File

@ -1,14 +1,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Memory;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using Squadronista.Solver; using Squadronista.Solver;
using Squadronista.Windows; using Squadronista.Windows;
@ -16,29 +17,34 @@ using Race = Squadronista.Solver.Race;
namespace Squadronista; namespace Squadronista;
public sealed class SquadronistaPlugin : IDalamudPlugin public class SquadronistaPlugin : IDalamudPlugin
{ {
private readonly WindowSystem _windowSystem = new(nameof(SquadronistaPlugin)); private readonly WindowSystem _windowSystem = new(nameof(SquadronistaPlugin));
private readonly IDalamudPluginInterface _pluginInterface; private readonly DalamudPluginInterface _pluginInterface;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
private readonly IDataManager _dataManager; private readonly IDataManager _dataManager;
private readonly IAddonLifecycle _addonLifecycle; private readonly IAddonLifecycle _addonLifecycle;
private readonly IReadOnlyDictionary<string, uint> _classNamesToId;
private readonly IReadOnlyList<SquadronMission> _allMissions; private readonly IReadOnlyList<SquadronMission> _allMissions;
private readonly MainWindow _mainWindow; private readonly MainWindow _mainWindow;
public SquadronistaPlugin(IDalamudPluginInterface pluginInterface, IClientState clientState, IPluginLog pluginLog, public SquadronistaPlugin(DalamudPluginInterface pluginInterface, IClientState clientState, IPluginLog pluginLog,
IDataManager dataManager, IAddonLifecycle addonLifecycle, IGameGui gameGui) IDataManager dataManager, IAddonLifecycle addonLifecycle)
{ {
ArgumentNullException.ThrowIfNull(dataManager);
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_clientState = clientState; _clientState = clientState;
_pluginLog = pluginLog; _pluginLog = pluginLog;
_dataManager = dataManager; _dataManager = dataManager;
_addonLifecycle = addonLifecycle; _addonLifecycle = addonLifecycle;
_classNamesToId = dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.ClassJob>()!
.Where(x => x.RowId > 0)
.Where(x => x.Name.ToString().Length > 0)
.ToDictionary(x => x.Name.ToString().ToLower(), x => x.RowId)
.AsReadOnly();
_allMissions = dataManager.GetExcelSheet<GcArmyExpedition>()! _allMissions = dataManager.GetExcelSheet<GcArmyExpedition>()!
.Where(x => x.RowId > 0) .Where(x => x.RowId > 0)
.Select(x => new SquadronMission .Select(x => new SquadronMission
@ -49,15 +55,14 @@ public sealed class SquadronistaPlugin : IDalamudPlugin
// 13 and 14 seems to be a duplicate // 13 and 14 seems to be a duplicate
IsFlaggedMission = x.RowId is 7 or 14 or 15 or 34, IsFlaggedMission = x.RowId is 7 or 14 or 15 or 34,
PossibleAttributes = Enumerable.Range(0, x.RequiredPhysical.Length)
.Select(i => new Attributes // not sure why this is structured the way it is
{ // 'Supply Wagon Escort', for example, has the following physical values:
PhysicalAbility = x.RequiredPhysical[i], // 210, 125, 305, 305, 210, 115
MentalAbility = x.RequiredMental[i], // and the UI shows 115 as required
TacticalAbility = x.RequiredTactical[i], PhysicalAbility = x.RequiredPhysical.Last(),
}) MentalAbility = x.RequiredMental.Last(),
.ToList() TacticalAbility = x.RequiredTactical.Last(),
.AsReadOnly(),
}) })
.ToList() .ToList()
.AsReadOnly(); .AsReadOnly();
@ -76,12 +81,12 @@ public sealed class SquadronistaPlugin : IDalamudPlugin
.AsReadOnly(); .AsReadOnly();
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_clientState.Logout += ResetCharacterSpecificData; _clientState.Logout += Logout;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GcArmyMemberList", UpdateSquadronState); _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GcArmyMemberList", UpdateSquadronState);
_addonLifecycle.RegisterListener(AddonEvent.PostRefresh, "GcArmyExpedition", UpdateExpeditionState); _addonLifecycle.RegisterListener(AddonEvent.PostRefresh, "GcArmyExpedition", UpdateExpeditionState);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GcArmyExpedition", UpdateExpeditionState); _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GcArmyExpedition", UpdateExpeditionState);
_mainWindow = new MainWindow(this, pluginLog, addonLifecycle, gameGui); _mainWindow = new MainWindow(this, pluginLog, addonLifecycle);
_windowSystem.AddWindow(_mainWindow); _windowSystem.AddWindow(_mainWindow);
} }
@ -91,89 +96,57 @@ public sealed class SquadronistaPlugin : IDalamudPlugin
private unsafe void UpdateSquadronState(AddonEvent type, AddonArgs args) private unsafe void UpdateSquadronState(AddonEvent type, AddonArgs args)
{ {
_pluginLog.Information("Updating squadron state from member list"); AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
var gcArmyManager = GcArmyManager.Instance(); if (addon->AtkValuesCount != 133)
if (gcArmyManager == null)
{ {
_pluginLog.Warning("No GcArmyManager"); _pluginLog.Error("Unexpected AddonGcArmyMemberList atkvalues count");
ResetCharacterSpecificData();
return; return;
} }
if (gcArmyManager->Data == null) var atkValues = addon->AtkValues;
{ uint memberCount = atkValues[4].UInt;
_pluginLog.Warning("No GcArmyManager->Data");
ResetCharacterSpecificData();
return;
}
if (gcArmyManager->GetMemberCount() < 4) // can't do any missions like this...
{ if (memberCount < 4)
_pluginLog.Warning($"Not enough squadron members to send on missions, only got {gcArmyManager->GetMemberCount()} members");
ResetCharacterSpecificData();
return; return;
}
IReadOnlyList<SquadronMember> members = Enumerable.Range(0, (int)gcArmyManager->GetMemberCount()) IReadOnlyList<SquadronMember> members = Enumerable.Range(0, (int)memberCount)
.Select(i => .Select(i => new SquadronMember
{ {
var member = gcArmyManager->GetMember((uint)i); Name = ReadAtkString(atkValues[6 + 15 * i])!,
if (member == null) Level = atkValues[10 + 15 * i].Int,
return null; ClassJob = _classNamesToId[ReadAtkString(atkValues[7 + 15 * i])!.ToLower()],
Race = Race.Lalafell, // TODO
return new SquadronMember Experience = 0, // TODO
{ }.InitializeFrom(_dataManager))
Name = _dataManager.GetExcelSheet<ENpcResident>()!.GetRow(member->ENpcResidentId)!
.Singular.ToString(),
Level = member->Level,
ClassJob = member->ClassJob,
Race = (Race)member->Race,
Experience = member->Experience,
EnlistmentTimestamp = member->EnlistmentTimestamp,
}.InitializeFrom(_dataManager);
})
.Where(x => x != null)
.Cast<SquadronMember>()
.OrderBy(x => x.EnlistmentTimestamp)
.ToList() .ToList()
.AsReadOnly(); .AsReadOnly();
byte rank = byte.Parse(ReadAtkString(atkValues[0])!.Split(":")[1].Trim());
var bonus = new BonusAttributes int[] attributes = ReadAtkString(atkValues[1])!.Split(":")[1].Trim()
{ .Split("/")
PhysicalAbility = gcArmyManager->Data->BonusPhysical, .Select(int.Parse)
MentalAbility = gcArmyManager->Data->BonusMental, .ToArray();
TacticalAbility = gcArmyManager->Data->BonusTactical,
Cap = gcArmyManager->Data->BonusPhysical + gcArmyManager->Data->BonusMental + gcArmyManager->Data->BonusTactical,
};
if (SquadronState != null &&
members.SequenceEqual(SquadronState.Members) &&
bonus == SquadronState.Bonus)
{
// nothing changed...
_pluginLog.Verbose("Not updating SquadronState, not changed");
return;
}
SquadronState = new SquadronState SquadronState = new SquadronState
{ {
Members = members, Members = members,
Bonus = bonus, Rank = rank,
CurrentTraining = ((ExtendedGcArmyData*)gcArmyManager->Data)->CurrentTraining, Bonus = new BonusAttributes
{
PhysicalAbility = attributes[0],
MentalAbility = attributes[1],
TacticalAbility = attributes[2],
Cap = attributes.Sum()
},
}; };
_pluginLog.Verbose(
$"Bonus stats: {bonus} (Cap: {bonus.Cap})");
foreach (var member in members) foreach (var member in members)
_pluginLog.Verbose( _pluginLog.Information($"MM → {member.Name}, {member.ClassJob}, Lv{member.Level}");
$"Squadron Member {member.Name}: ClassJob {member.ClassJob}, Lv{member.Level} → {member.ToAttributes()}");
} }
private unsafe void UpdateExpeditionState(AddonEvent type, AddonArgs args) private unsafe void UpdateExpeditionState(AddonEvent type, AddonArgs args)
{ {
AvailableMissions.Clear(); AvailableMissions.Clear();
if (type == AddonEvent.PostSetup)
ResetCharacterSpecificData();
AddonGcArmyExpedition* addonExpedition = (AddonGcArmyExpedition*)args.Addon; AddonGcArmyExpedition* addonExpedition = (AddonGcArmyExpedition*)args.Addon;
if (addonExpedition->AtkUnitBase.AtkValuesCount != 216) if (addonExpedition->AtkUnitBase.AtkValuesCount != 216)
@ -185,13 +158,21 @@ public sealed class SquadronistaPlugin : IDalamudPlugin
var atkValues = addonExpedition->AtkUnitBase.AtkValues; var atkValues = addonExpedition->AtkUnitBase.AtkValues;
int missionCount = atkValues[6].Int; int missionCount = atkValues[6].Int;
_pluginLog.Verbose($"Missions: {missionCount}"); _pluginLog.Information($"Missions → {missionCount}");
AvailableMissions = Enumerable.Range(0, missionCount) AvailableMissions = Enumerable.Range(0, missionCount)
.Where(i => atkValues[8 + 4 * i].Int > 0)
.Select(i => _allMissions.Single(x => x.Id == atkValues[9 + 4 * i].Int)) .Select(i => _allMissions.Single(x => x.Id == atkValues[9 + 4 * i].Int))
.ToList(); .ToList();
} }
private void ResetCharacterSpecificData() private unsafe string? ReadAtkString(AtkValue atkValue)
{
if (atkValue.String != null)
return MemoryHelper.ReadSeStringNullTerminated(new nint(atkValue.String)).ToString();
return null;
}
private void Logout()
{ {
SquadronState = null; SquadronState = null;
} }
@ -202,12 +183,7 @@ public sealed class SquadronistaPlugin : IDalamudPlugin
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GcArmyExpedition", UpdateExpeditionState); _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GcArmyExpedition", UpdateExpeditionState);
_addonLifecycle.UnregisterListener(AddonEvent.PostRefresh, "GcArmyExpedition", UpdateExpeditionState); _addonLifecycle.UnregisterListener(AddonEvent.PostRefresh, "GcArmyExpedition", UpdateExpeditionState);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GcArmyMemberList", UpdateSquadronState); _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GcArmyMemberList", UpdateSquadronState);
_clientState.Logout -= ResetCharacterSpecificData; _clientState.Logout -= Logout;
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
} }
[StructLayout(LayoutKind.Explicit, Size = 0xB28)]
private struct ExtendedGcArmyData {
[FieldOffset(0x2C4)] public ushort CurrentTraining;
}
} }

View File

@ -1,74 +1,46 @@
using System; using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.Text;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET; using ImGuiNET;
using LLib.GameUI;
using LLib.ImGui;
using Squadronista.Solver; using Squadronista.Solver;
using Task = System.Threading.Tasks.Task; using Task = System.Threading.Tasks.Task;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Squadronista.Windows; namespace Squadronista.Windows;
internal sealed class MainWindow : LWindow, IDisposable internal sealed class MainWindow : Window, IDisposable
{ {
private readonly SquadronistaPlugin _plugin; private readonly SquadronistaPlugin _plugin;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
private readonly IAddonLifecycle _addonLifecycle; private readonly IAddonLifecycle _addonLifecycle;
private readonly IGameGui _gameGui;
public MainWindow(SquadronistaPlugin plugin, IPluginLog pluginLog, public MainWindow(SquadronistaPlugin plugin, IPluginLog pluginLog, IAddonLifecycle addonLifecycle)
IAddonLifecycle addonLifecycle, IGameGui gameGui)
: base("Squadronista##SquadronistaMainWindow") : base("Squadronista##SquadronistaMainWindow")
{ {
_plugin = plugin; _plugin = plugin;
_pluginLog = pluginLog; _pluginLog = pluginLog;
_addonLifecycle = addonLifecycle; _addonLifecycle = addonLifecycle;
_gameGui = gameGui;
Position = new Vector2(100, 100); Position = new Vector2(100, 100);
PositionCondition = ImGuiCond.Always; PositionCondition = ImGuiCond.Always;
Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse; Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse
;
SizeConstraints = new()
{
MinimumSize = new Vector2(150, 50),
MaximumSize = new Vector2(9999, 9999),
};
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GcArmyExpedition", ExpeditionPostSetup); _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GcArmyExpedition", ExpeditionPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PreFinalize, "GcArmyExpedition", ExpeditionPreFinalize); _addonLifecycle.RegisterListener(AddonEvent.PreFinalize, "GcArmyExpedition", ExpeditionPreFinalize);
_addonLifecycle.RegisterListener(AddonEvent.PostUpdate, "GcArmyExpedition", ExpeditionPostUpdate); _addonLifecycle.RegisterListener(AddonEvent.PostUpdate, "GcArmyExpedition", ExpeditionPostUpdate);
} }
private unsafe void ExpeditionPostSetup(AddonEvent type, AddonArgs args) private void ExpeditionPostSetup(AddonEvent type, AddonArgs args)
{ {
IsOpen = true; IsOpen = true;
_pluginLog.Information("Opening GC member list...");
var openMemmberList = stackalloc AtkValue[]
{
new() { Type = ValueType.Int, Int = 13 },
new() { Type = 0, Int = 0 },
new() { Type = 0, Int = 0 },
new() { Type = 0, Int = 0 },
new() { Type = 0, Int = 0 },
new() { Type = 0, Int = 0 }
};
((AddonGcArmyExpedition*)args.Addon)->AtkUnitBase.FireCallback(6, openMemmberList);
} }
private void ExpeditionPreFinalize(AddonEvent type, AddonArgs args) private void ExpeditionPreFinalize(AddonEvent type, AddonArgs args)
@ -90,116 +62,61 @@ internal sealed class MainWindow : LWindow, IDisposable
Position = new Vector2(x, y); Position = new Vector2(x, y);
} }
public override unsafe void Draw() public override void Draw()
{ {
var agentExpedition = AgentGcArmyExpedition.Instance(); var flaggedMission = _plugin.AvailableMissions.FirstOrDefault(x => x.IsFlaggedMission);
if (agentExpedition == null || agentExpedition->SelectedRow >= _plugin.AvailableMissions.Count) if (flaggedMission == null)
{ {
ImGui.Text($"Could not find mission... ({(agentExpedition != null ? agentExpedition->SelectedRow.ToString(CultureInfo.InvariantCulture) : "null")}; {_plugin.AvailableMissions.Count})"); ImGui.Text("No flagged mission available.");
return; return;
} }
var selectedMission = _plugin.AvailableMissions[agentExpedition->SelectedRow]; ImGui.Text($"{flaggedMission.Name}");
var missionAttributes = FindCurrentAttributeIndex(agentExpedition, selectedMission); ImGui.Indent();
if (missionAttributes != null)
ImGui.Text($"{selectedMission.Name} ({missionAttributes})");
else
ImGui.Text($"{selectedMission.Name}");
var state = _plugin.SquadronState; var state = _plugin.SquadronState;
if (state == null) if (state == null)
{ {
ImGui.TextColored(ImGuiColors.DalamudYellow, "Open Squadron Member list to continue."); ImGui.TextColored(ImGuiColors.DalamudYellow, "Open Squadron Member list to continue.");
} }
else if (state.CurrentTraining != 0)
{
ImGui.TextColored(ImGuiColors.DalamudRed, "Your squadron is currently in training.");
}
else else
{ {
var task = state.GetCalculation(selectedMission, missionAttributes); var task = state.CalculationTask;
if (task != null) if (task != null)
{ {
if (task.IsCompletedSuccessfully) if (task.IsCompletedSuccessfully)
{ {
SquadronSolver.CalculationResults results = task.Result; SquadronSolver.CalculationResult? result = task.Result;
if (results.Results.Count > 0) if (result != null)
{ {
// if the member list window is open, we can trivially check which member is part of the party if (result.Mission?.Id != flaggedMission.Id)
List<string>? activeMembers = null;
if (_gameGui.TryGetAddonByName("GcArmyMemberList", out AtkUnitBase* addonMemberList) &&
LAddon.IsAddonReady(addonMemberList) && addonMemberList->AtkValuesCount == 133)
{ {
var atkValues = addonMemberList->AtkValues; state.CalculationTask = null;
activeMembers = Enumerable.Range(0, (int)atkValues[4].UInt) return;
.Where(i => ((SquadronMemberUiInfo)atkValues[5 + i * 15].Int).HasFlag(SquadronMemberUiInfo.IsPartOfMission))
.Select(i => atkValues[6 + i * 15].ReadAtkString())
.Where(x => !string.IsNullOrEmpty(x))
.Cast<string>()
.ToList();
} }
ImGui.Text("Squadron Members");
ImGui.Indent();
foreach (var member in result.Members)
ImGui.Text($"{member.Name}");
ImGui.Unindent();
foreach (var result in results.Results) if (result.Trainings.Count > 0)
{ {
ImGui.TextColored(
result.MatchingAttributes == 3 ? ImGuiColors.HealerGreen : ImGuiColors.DalamudYellow,
$"{result.ToLabel()}");
ImGui.Indent();
ImGui.Text($"Squadron Members ({SeIconChar.LevelEn.ToIconString()}{result.TotalLevel / 4:N0})");
ImGui.Indent();
foreach (var member in result.Members)
{
if (activeMembers != null)
ImGui.TextColored(
activeMembers.Contains(member.Name)
? ImGuiColors.HealerGreen
: ImGuiColors.DalamudYellow, $"{member.Name}");
else
ImGui.Text($"{member.Name}");
}
ImGui.Unindent();
if (result.Trainings.Count > 0)
{
ImGui.Spacing();
ImGui.Text($"Trainings needed ({result.Trainings.Count})");
ImGui.Indent();
foreach (var training in result.Trainings)
ImGui.TextColored(ImGuiColors.DalamudYellow, $"{training.Name}");
ImGui.Unindent();
}
ImGui.Spacing(); ImGui.Spacing();
ImGui.Text("Final Stats:"); ImGui.Text($"Trainings needed ({result.Trainings.Count})");
ImGui.SameLine(0); ImGui.Indent();
ImGui.TextColored( foreach (var training in result.Trainings)
result.PhysicalAbility >= missionAttributes!.PhysicalAbility ImGui.Text($"{training.Name}");
? ImGuiColors.HealerGreen
: ImGuiColors.DalamudYellow, $"{result.PhysicalAbility}");
ImGui.SameLine(0);
ImGui.Text("/");
ImGui.SameLine(0);
ImGui.TextColored(
result.MentalAbility >= missionAttributes.MentalAbility
? ImGuiColors.HealerGreen
: ImGuiColors.DalamudYellow, $"{result.MentalAbility}");
ImGui.SameLine(0);
ImGui.Text("/");
ImGui.SameLine(0);
ImGui.TextColored(
result.TacticalAbility >= missionAttributes.TacticalAbility
? ImGuiColors.HealerGreen
: ImGuiColors.DalamudYellow, $"{result.TacticalAbility}");
ImGui.Unindent(); ImGui.Unindent();
} }
ImGui.Spacing();
ImGui.TextColored(result.Trainings.Count == 0 ? ImGuiColors.HealerGreen : ImGuiColors.DalamudYellow, $"Final Stats: {result.PhysicalAbility} / {result.MentalAbility} / {result.TacticalAbility}");
} }
else else
{ {
ImGui.TextColored(ImGuiColors.DalamudRed, $"No combination of members/trainings can achieve\n{(selectedMission.IsFlaggedMission ? "all" : "2 out of 3")} attributes for {missionAttributes}."); ImGui.TextColored(ImGuiColors.DalamudRed,
$"No combination of members/trainings can achieve {flaggedMission.PhysicalAbility} / {flaggedMission.MentalAbility} / {flaggedMission.TacticalAbility}.");
ImGui.Text("Level the squadron further and check again."); ImGui.Text("Level the squadron further and check again.");
} }
} }
@ -210,146 +127,26 @@ internal sealed class MainWindow : LWindow, IDisposable
} }
else else
{ {
if (missionAttributes == null) state.CalculationTask = Task.Factory.StartNew(() =>
{
ImGui.Text("No matching mission found...?");
return;
}
state.SetCalculation(selectedMission, missionAttributes, Task.Factory.StartNew(() =>
{ {
var solver = new SquadronSolver(_pluginLog, state, _plugin.Trainings); var solver = new SquadronSolver(_pluginLog, state, _plugin.Trainings);
var allSolutions = solver.SolveFor(flaggedMission).ToList();
if (allSolutions.Count == 0)
return null;
SquadronSolver.CalculationResults results = new SquadronSolver.CalculationResults int shortestTrainings = allSolutions.Min(x => x.Trainings.Count);
{ return allSolutions
IsFlaggedMission = selectedMission.IsFlaggedMission .Where(x => x.Trainings.Count == shortestTrainings)
}; .OrderBy(x => x.Members.Sum(y => y.Level))
.First();
if (selectedMission.IsFlaggedMission) });
{
// only relevant when all 3 stats match
var perfectMatches = solver.SolveFor(selectedMission, missionAttributes, 3).ToList();
if (perfectMatches.Count > 0)
{
results.Results.Add(perfectMatches
.OrderBy(x => x.Trainings.Count)
.ThenBy(x => x.Members.Sum(y => y.Level))
.First());
}
}
else
{
var matches = solver.SolveFor(selectedMission, missionAttributes, 2)
.OrderByDescending(x => x.MatchingAttributes)
.ThenBy(x => x.Trainings.Count)
.ThenBy(x => x.Members.Sum(y => y.Level))
.ToList();
// optimal solution without training
var perfectMatches =
matches.Where(x => x.MatchingAttributes == 3 && x.Trainings.Count == 0).ToList();
if (perfectMatches.Count > 0)
{
results.Results.Add(perfectMatches.First());
matches = matches.Except(perfectMatches)
// we only want lower-level member combinations for any option with training or at 66%
.Where(x => x.TotalLevel < perfectMatches.First().TotalLevel)
.ToList();
}
// optimal solution, with training
var perfectMatchesWithTraining = matches.Where(x => x.MatchingAttributes == 3).ToList();
if (perfectMatchesWithTraining.Count > 0)
{
results.Results.Add(perfectMatchesWithTraining.First());
matches = matches.Except(perfectMatchesWithTraining).ToList();
}
// suboptimal solutions
if (matches.Count > 0)
{
var suboptimalMatch = matches.First();
if (suboptimalMatch.Trainings.Count == 0 && perfectMatchesWithTraining.Count != 0)
results.Results.Insert(results.Results.Count - 1, suboptimalMatch);
else
results.Results.Add(suboptimalMatch);
}
}
return results;
}, default, TaskCreationOptions.LongRunning, TaskScheduler.Default));
} }
} }
} ImGui.Unindent();
private unsafe Attributes? FindCurrentAttributeIndex(AgentGcArmyExpedition* agentExpedition,
SquadronMission selectedMission)
{
AddonGcArmyExpedition* addonExpedition =
(AddonGcArmyExpedition*)LAddon.GetAddonById(agentExpedition->AgentInterface.AddonId);
// should never happen
if (addonExpedition == null || !LAddon.IsAddonReady(&addonExpedition->AtkUnitBase))
return null;
AtkComponentBase* requiredAttribComponent = addonExpedition->RequiredAttributesComponentNode;
if (requiredAttribComponent == null)
return null;
AtkComponentNode* physicalComponent = GetNodeById<AtkComponentNode>(requiredAttribComponent->UldManager, 2);
AtkComponentNode* mentalComponent = GetNodeById<AtkComponentNode>(requiredAttribComponent->UldManager, 4);
AtkComponentNode* tacticalComponent = GetNodeById<AtkComponentNode>(requiredAttribComponent->UldManager, 6);
if (physicalComponent == null || mentalComponent == null || tacticalComponent == null)
{
_pluginLog.Warning("Could not parse required attribute children");
return null;
}
AtkTextNode* physicalText = GetNodeById<AtkTextNode>(physicalComponent->Component->UldManager, 2);
AtkTextNode* mentalText = GetNodeById<AtkTextNode>(mentalComponent->Component->UldManager, 2);
AtkTextNode* tacticalText = GetNodeById<AtkTextNode>(tacticalComponent->Component->UldManager, 2);
if (physicalText == null || mentalText == null || tacticalText == null)
{
_pluginLog.Warning("Could not parse required attribute texts");
return null;
}
int physical = int.Parse(physicalText->NodeText.ToString(), CultureInfo.InvariantCulture);
int mental = int.Parse(mentalText->NodeText.ToString(), CultureInfo.InvariantCulture);
int tactical = int.Parse(tacticalText->NodeText.ToString(), CultureInfo.InvariantCulture);
var newAttributes = new Attributes
{
PhysicalAbility = physical,
MentalAbility = mental,
TacticalAbility = tactical,
};
if (selectedMission.PossibleAttributes.Contains(newAttributes))
return newAttributes;
else
{
_pluginLog.Warning($"Wrong attributes for {selectedMission.Name}: {newAttributes}");
return null;
}
}
private static unsafe T* GetNodeById<T>(AtkUldManager uldManager, uint nodeId, NodeType? type = null)
where T : unmanaged
{
for (var i = 0; i < uldManager.NodeListCount; i++)
{
var n = uldManager.NodeList[i];
if (n->NodeId != nodeId || type != null && n->Type != type.Value) continue;
if (!n->IsVisible()) continue;
return (T*)n;
}
return null;
} }
public void Dispose() public void Dispose()
{ {
_addonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "GcArmyExpedition", ExpeditionPostUpdate);
_addonLifecycle.UnregisterListener(AddonEvent.PreFinalize, "GcArmyExpedition", ExpeditionPreFinalize); _addonLifecycle.UnregisterListener(AddonEvent.PreFinalize, "GcArmyExpedition", ExpeditionPreFinalize);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GcArmyExpedition", ExpeditionPostSetup); _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GcArmyExpedition", ExpeditionPostSetup);
} }

View File

@ -1,86 +1,12 @@
{ {
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net8.0-windows7.0": { "net7.0-windows7.0": {
"DalamudPackager": { "DalamudPackager": {
"type": "Direct", "type": "Direct",
"requested": "[2.1.13, )", "requested": "[2.1.12, )",
"resolved": "2.1.13", "resolved": "2.1.12",
"contentHash": "rMN1omGe8536f4xLMvx9NwfvpAc9YFFfeXJ1t4P4PE6Gu8WCIoFliR1sh07hM+bfODmesk/dvMbji7vNI+B/pQ==" "contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg=="
},
"DotNet.ReproducibleBuilds": {
"type": "Direct",
"requested": "[1.1.1, )",
"resolved": "1.1.1",
"contentHash": "+H2t/t34h6mhEoUvHi8yGXyuZ2GjSovcGYehJrS2MDm2XgmPfZL2Sdxg+uL2lKgZ4M6tTwKHIlxOob2bgh0NRQ==",
"dependencies": {
"Microsoft.SourceLink.AzureRepos.Git": "1.1.1",
"Microsoft.SourceLink.Bitbucket.Git": "1.1.1",
"Microsoft.SourceLink.GitHub": "1.1.1",
"Microsoft.SourceLink.GitLab": "1.1.1"
}
},
"Microsoft.SourceLink.Gitea": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "KOBodmDnlWGIqZt2hT47Q69TIoGhIApDVLCyyj9TT5ct8ju16AbHYcB4XeknoHX562wO1pMS/1DfBIZK+V+sxg==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "8.0.0",
"Microsoft.SourceLink.Common": "8.0.0"
}
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
},
"Microsoft.SourceLink.AzureRepos.Git": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "qB5urvw9LO2bG3eVAkuL+2ughxz2rR7aYgm2iyrB8Rlk9cp2ndvGRCvehk3rNIhRuNtQaeKwctOl1KvWiklv5w==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.Bitbucket.Git": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "cDzxXwlyWpLWaH0em4Idj0H3AmVo3L/6xRXKssYemx+7W52iNskj/SQ4FOmfCb8YQt39otTDNMveCZzYtMoucQ==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.GitLab": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "tvsg47DDLqqedlPeYVE2lmiTpND8F0hkrealQ5hYltSmvruy/Gr5nHAKSsjyw5L3NeM/HLMI5ORv7on/M4qyZw==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"llib": {
"type": "Project",
"dependencies": {
"DalamudPackager": "[2.1.13, )"
}
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "8.0.0", "version": "7.0.0",
"rollForward": "latestMinor", "rollForward": "latestMinor",
"allowPrerelease": false "allowPrerelease": false
} }