Compare commits

...

9 Commits
v0.3 ... master

16 changed files with 1304 additions and 180 deletions

2
LLib

@ -1 +1 @@
Subproject commit e59d291f04473eae0b76712397733e2e25349953 Subproject commit e206782c1106e1a5292a06af61783faef1ac0c42

1017
Squadronista/.editorconfig Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,45 @@
using System;
namespace Squadronista.Solver; namespace Squadronista.Solver;
internal class Attributes internal class Attributes : IEquatable<Attributes>
{ {
public required int PhysicalAbility { get; init; } public required int PhysicalAbility { get; init; }
public required int MentalAbility { get; init; } public required int MentalAbility { get; init; }
public required int TacticalAbility { 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

@ -54,7 +54,7 @@ internal sealed class BonusAttributes : Attributes, IEquatable<BonusAttributes>
} }
} }
private void Fix(ref int mainStat, int mainGained, ref int otherStatA, int otherGainedA, ref int otherStatB, int otherGainedB) private static 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

@ -10,12 +10,12 @@ internal sealed class SquadronMember : IEquatable<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; }
// TODO
public required Race Race { get; init; }
// TODO public required uint ClassJob { get; init; }
public int Experience { get; init; }
public required Race Race { get; init; }
public required int EnlistmentTimestamp { get; init; }
public uint 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,6 +34,7 @@ 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;
@ -43,7 +44,8 @@ internal sealed class SquadronMember : IEquatable<SquadronMember>
{ {
if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(this, other)) return true;
return Name == other.Name && Level == other.Level && ClassJob == other.ClassJob && Race == other.Race && Experience == other.Experience; 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) public override bool Equals(object? obj)
@ -65,4 +67,11 @@ internal sealed class SquadronMember : IEquatable<SquadronMember>
{ {
return !Equals(left, right); return !Equals(left, right);
} }
public Attributes ToAttributes() => new()
{
PhysicalAbility = PhysicalAbility,
MentalAbility = MentalAbility,
TacticalAbility = TacticalAbility,
};
} }

View File

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

View File

@ -38,11 +38,11 @@ internal sealed class SquadronSolver
_newTrainings = _calculatedTrainings.Keys.ToList(); _newTrainings = _calculatedTrainings.Keys.ToList();
} }
public IEnumerable<CalculationResult> SolveFor(SquadronMission mission, int requiredMatchingStats) public IEnumerable<CalculationResult> SolveFor(SquadronMission mission, Attributes missionAttributes, int requiredMatchingStats)
{ {
int minPhysical = mission.CurrentAttributes.PhysicalAbility; int minPhysical = missionAttributes.PhysicalAbility;
int minMental = mission.CurrentAttributes.MentalAbility; int minMental = missionAttributes.MentalAbility;
int minTactical = mission.CurrentAttributes.TacticalAbility; int minTactical = missionAttributes.TacticalAbility;
bool foundWithoutTraining = false; bool foundWithoutTraining = false;
List<CalculationResult> intermediates = CalculateForAllMemberCombinations(mission.Level, _state.Bonus); List<CalculationResult> intermediates = CalculateForAllMemberCombinations(mission.Level, _state.Bonus);
@ -78,7 +78,7 @@ internal sealed class SquadronSolver
} }
} }
private int CountStatMatches(CalculationResult x, int minPhysical, int minMental, int minTactical) private static int CountStatMatches(CalculationResult x, int minPhysical, int minMental, int minTactical)
{ {
return (x.PhysicalAbility >= minPhysical ? 1 : 0) + return (x.PhysicalAbility >= minPhysical ? 1 : 0) +
(x.MentalAbility >= minMental ? 1 : 0) + (x.MentalAbility >= minMental ? 1 : 0) +

View File

@ -5,15 +5,20 @@ namespace Squadronista.Solver;
internal sealed class SquadronState internal sealed class SquadronState
{ {
private readonly Dictionary<SquadronMission, Task<SquadronSolver.CalculationResults>> _calculationResults = new(); 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) public Task<SquadronSolver.CalculationResults>? GetCalculation(SquadronMission mission, Attributes? attributes)
=> _calculationResults.TryGetValue(mission, out var task) ? task : null; {
if (attributes == null)
return null;
public void SetCalculation(SquadronMission mission, Task<SquadronSolver.CalculationResults> task) return _calculationResults.GetValueOrDefault((mission.Id, attributes));
=> _calculationResults[mission] = task; }
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 int CalculateCapped(int mainStat, int otherA, int otherB) private static int CalculateCapped(int mainStat, int otherA, int otherB)
{ {
if (mainStat > 0) if (mainStat > 0)
return mainStat; return mainStat;

View File

@ -1,4 +1,5 @@
using Squadronista.Solver; using System.Collections.Generic;
using Squadronista.Solver;
namespace Squadronista; namespace Squadronista;
@ -9,5 +10,5 @@ internal sealed class SquadronMission
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 Attributes? CurrentAttributes { get; set; } public required IReadOnlyList<Attributes> PossibleAttributes { get; init; }
} }

View File

@ -1,64 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Dalamud.NET.Sdk/9.0.2">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework> <Version>4.0</Version>
<Version>0.3</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>
<PropertyGroup> <Import Project="..\LLib\LLib.targets"/>
<DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath> <Import Project="..\LLib\RenameZip.targets"/>
</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" /> <ProjectReference Include="..\LLib\LLib.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="DalamudPackager" Version="2.1.12"/>
</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,8 @@
{ {
"Name": "Squadronista", "Name": "Squadronista",
"Author": "Liza Carvelli", "Author": "Liza Carvelli",
"Punchline": "Simplified Squadron Calculator for Flagged Missions, heavily inspired by https://ffxivsquadron.com/", "Punchline": "Simplified Squadron Calculator, heavily inspired by https://ffxivsquadron.com/",
"Description": "", "Description": "",
"RepoUrl": "https://git.carvel.li/liza/Squadronista", "RepoUrl": "https://git.carvel.li/liza/Squadronista",
"IconUrl": "https://git.carvel.li/liza/plugin-repo/raw/branch/master/dist/Squadronista.png" "IconUrl": "https://plugins.carvel.li/icons/Squadronista.png"
} }

View File

@ -1,15 +1,14 @@
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;
@ -17,34 +16,29 @@ using Race = Squadronista.Solver.Race;
namespace Squadronista; namespace Squadronista;
public class SquadronistaPlugin : IDalamudPlugin public sealed class SquadronistaPlugin : IDalamudPlugin
{ {
private readonly WindowSystem _windowSystem = new(nameof(SquadronistaPlugin)); private readonly WindowSystem _windowSystem = new(nameof(SquadronistaPlugin));
private readonly DalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _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(DalamudPluginInterface pluginInterface, IClientState clientState, IPluginLog pluginLog, public SquadronistaPlugin(IDalamudPluginInterface pluginInterface, IClientState clientState, IPluginLog pluginLog,
IDataManager dataManager, IAddonLifecycle addonLifecycle, IGameGui gameGui, IFramework framework) IDataManager dataManager, IAddonLifecycle addonLifecycle, IGameGui gameGui)
{ {
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<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
@ -55,6 +49,15 @@ public 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
{
PhysicalAbility = x.RequiredPhysical[i],
MentalAbility = x.RequiredMental[i],
TacticalAbility = x.RequiredTactical[i],
})
.ToList()
.AsReadOnly(),
}) })
.ToList() .ToList()
.AsReadOnly(); .AsReadOnly();
@ -73,12 +76,12 @@ public class SquadronistaPlugin : IDalamudPlugin
.AsReadOnly(); .AsReadOnly();
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_clientState.Logout += Logout; _clientState.Logout += ResetCharacterSpecificData;
_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, pluginInterface, pluginLog, addonLifecycle, gameGui); _mainWindow = new MainWindow(this, pluginLog, addonLifecycle, gameGui);
_windowSystem.AddWindow(_mainWindow); _windowSystem.AddWindow(_mainWindow);
} }
@ -89,75 +92,88 @@ public 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"); _pluginLog.Information("Updating squadron state from member list");
var gcArmyManager = GcArmyManager.Instance();
AtkUnitBase* addon = (AtkUnitBase*)args.Addon; if (gcArmyManager == null)
if (addon->AtkValuesCount != 133)
{ {
_pluginLog.Error("Unexpected AddonGcArmyMemberList atkvalues count"); _pluginLog.Warning("No GcArmyManager");
SquadronState = null; ResetCharacterSpecificData();
return; return;
} }
var atkValues = addon->AtkValues; if (gcArmyManager->Data == null)
uint memberCount = atkValues[4].UInt;
// can't do any missions like this...
if (memberCount < 4)
{ {
SquadronState = null; _pluginLog.Warning("No GcArmyManager->Data");
ResetCharacterSpecificData();
return; return;
} }
IReadOnlyList<SquadronMember> members = Enumerable.Range(0, (int)memberCount) if (gcArmyManager->GetMemberCount() < 4)
.Select(i => new SquadronMember {
_pluginLog.Warning($"Not enough squadron members to send on missions, only got {gcArmyManager->GetMemberCount()} members");
ResetCharacterSpecificData();
return;
}
IReadOnlyList<SquadronMember> members = Enumerable.Range(0, (int)gcArmyManager->GetMemberCount())
.Select(i =>
{ {
Name = ReadAtkString(atkValues[6 + 15 * i])!, var member = gcArmyManager->GetMember((uint)i);
Level = atkValues[10 + 15 * i].Int, if (member == null)
ClassJob = _classNamesToId[ReadAtkString(atkValues[7 + 15 * i])!.ToLower()], return null;
Race = Race.Lalafell, // TODO
Experience = 0, // TODO return new SquadronMember
}.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());
int[] attributes = ReadAtkString(atkValues[1])!.Split(":")[1].Trim()
.Split("/")
.Select(int.Parse)
.ToArray();
var bonus = new BonusAttributes var bonus = new BonusAttributes
{ {
PhysicalAbility = attributes[0], PhysicalAbility = gcArmyManager->Data->BonusPhysical,
MentalAbility = attributes[1], MentalAbility = gcArmyManager->Data->BonusMental,
TacticalAbility = attributes[2], TacticalAbility = gcArmyManager->Data->BonusTactical,
Cap = attributes.Sum() Cap = gcArmyManager->Data->BonusPhysical + gcArmyManager->Data->BonusMental + gcArmyManager->Data->BonusTactical,
}; };
if (SquadronState != null && if (SquadronState != null &&
members.SequenceEqual(SquadronState.Members) && members.SequenceEqual(SquadronState.Members) &&
rank == SquadronState.Rank &&
bonus == SquadronState.Bonus) bonus == SquadronState.Bonus)
{ {
// nothing changed... // nothing changed...
_pluginLog.Verbose("Not updating SquadronState, not changed");
return; return;
} }
SquadronState = new SquadronState SquadronState = new SquadronState
{ {
Members = members, Members = members,
Rank = rank,
Bonus = bonus, Bonus = bonus,
CurrentTraining = ((ExtendedGcArmyData*)gcArmyManager->Data)->CurrentTraining,
}; };
_pluginLog.Verbose(
$"Bonus stats: {bonus} (Cap: {bonus.Cap})");
foreach (var member in members) foreach (var member in members)
_pluginLog.Verbose($"Squadron Member {member.Name}: ClassJob {member.ClassJob}, Lv{member.Level}"); _pluginLog.Verbose(
$"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) if (type == AddonEvent.PostSetup)
SquadronState = null; ResetCharacterSpecificData();
AddonGcArmyExpedition* addonExpedition = (AddonGcArmyExpedition*)args.Addon; AddonGcArmyExpedition* addonExpedition = (AddonGcArmyExpedition*)args.Addon;
if (addonExpedition->AtkUnitBase.AtkValuesCount != 216) if (addonExpedition->AtkUnitBase.AtkValuesCount != 216)
@ -175,14 +191,7 @@ public class SquadronistaPlugin : IDalamudPlugin
.ToList(); .ToList();
} }
private unsafe string? ReadAtkString(AtkValue atkValue) private void ResetCharacterSpecificData()
{
if (atkValue.String != null)
return MemoryHelper.ReadSeStringNullTerminated(new nint(atkValue.String)).ToString();
return null;
}
private void Logout()
{ {
SquadronState = null; SquadronState = null;
} }
@ -193,7 +202,12 @@ public 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 -= Logout; _clientState.Logout -= ResetCharacterSpecificData;
_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,40 +1,40 @@
using System; using System;
using System.Collections.Generic; 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.Game.Text;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
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.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET; using ImGuiNET;
using LLib;
using LLib.GameUI; 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; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Squadronista.Windows; namespace Squadronista.Windows;
internal sealed class MainWindow : Window, IDisposable internal sealed class MainWindow : LWindow, IDisposable
{ {
private readonly SquadronistaPlugin _plugin; private readonly SquadronistaPlugin _plugin;
private readonly DalamudPluginInterface _pluginInterface;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
private readonly IAddonLifecycle _addonLifecycle; private readonly IAddonLifecycle _addonLifecycle;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
public MainWindow(SquadronistaPlugin plugin, DalamudPluginInterface pluginInterface, IPluginLog pluginLog, public MainWindow(SquadronistaPlugin plugin, IPluginLog pluginLog,
IAddonLifecycle addonLifecycle, IGameGui gameGui) IAddonLifecycle addonLifecycle, IGameGui gameGui)
: base("Squadronista##SquadronistaMainWindow") : base("Squadronista##SquadronistaMainWindow")
{ {
_plugin = plugin; _plugin = plugin;
_pluginInterface = pluginInterface;
_pluginLog = pluginLog; _pluginLog = pluginLog;
_addonLifecycle = addonLifecycle; _addonLifecycle = addonLifecycle;
_gameGui = gameGui; _gameGui = gameGui;
@ -92,26 +92,32 @@ internal sealed class MainWindow : Window, IDisposable
public override unsafe void Draw() public override unsafe void Draw()
{ {
LImGui.AddPatreonIcon(_pluginInterface);
var agentExpedition = AgentGcArmyExpedition.Instance(); var agentExpedition = AgentGcArmyExpedition.Instance();
if (agentExpedition == null || agentExpedition->SelectedRow >= _plugin.AvailableMissions.Count) if (agentExpedition == null || agentExpedition->SelectedRow >= _plugin.AvailableMissions.Count)
{ {
ImGui.Text($"Could not find mission... ({(agentExpedition != null ? agentExpedition->SelectedRow.ToString() : "null")}; {_plugin.AvailableMissions.Count})"); ImGui.Text($"Could not find mission... ({(agentExpedition != null ? agentExpedition->SelectedRow.ToString(CultureInfo.InvariantCulture) : "null")}; {_plugin.AvailableMissions.Count})");
return; return;
} }
var selectedMission = _plugin.AvailableMissions[agentExpedition->SelectedRow]; var selectedMission = _plugin.AvailableMissions[agentExpedition->SelectedRow];
ImGui.Text($"{selectedMission.Name}"); var missionAttributes = FindCurrentAttributeIndex(agentExpedition, selectedMission);
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); var task = state.GetCalculation(selectedMission, missionAttributes);
if (task != null) if (task != null)
{ {
if (task.IsCompletedSuccessfully) if (task.IsCompletedSuccessfully)
@ -126,7 +132,7 @@ internal sealed class MainWindow : Window, IDisposable
{ {
var atkValues = addonMemberList->AtkValues; var atkValues = addonMemberList->AtkValues;
activeMembers = Enumerable.Range(0, (int)atkValues[4].UInt) activeMembers = Enumerable.Range(0, (int)atkValues[4].UInt)
.Where(i => atkValues[5 + i * 15].UInt == 3) .Where(i => ((SquadronMemberUiInfo)atkValues[5 + i * 15].Int).HasFlag(SquadronMemberUiInfo.IsPartOfMission))
.Select(i => atkValues[6 + i * 15].ReadAtkString()) .Select(i => atkValues[6 + i * 15].ReadAtkString())
.Where(x => !string.IsNullOrEmpty(x)) .Where(x => !string.IsNullOrEmpty(x))
.Cast<string>() .Cast<string>()
@ -162,7 +168,7 @@ internal sealed class MainWindow : Window, IDisposable
ImGui.Text($"Trainings needed ({result.Trainings.Count})"); ImGui.Text($"Trainings needed ({result.Trainings.Count})");
ImGui.Indent(); ImGui.Indent();
foreach (var training in result.Trainings) foreach (var training in result.Trainings)
ImGui.Text($"{training.Name}"); ImGui.TextColored(ImGuiColors.DalamudYellow, $"{training.Name}");
ImGui.Unindent(); ImGui.Unindent();
} }
@ -170,21 +176,21 @@ internal sealed class MainWindow : Window, IDisposable
ImGui.Text("Final Stats:"); ImGui.Text("Final Stats:");
ImGui.SameLine(0); ImGui.SameLine(0);
ImGui.TextColored( ImGui.TextColored(
result.PhysicalAbility >= selectedMission.CurrentAttributes.PhysicalAbility result.PhysicalAbility >= missionAttributes!.PhysicalAbility
? ImGuiColors.HealerGreen ? ImGuiColors.HealerGreen
: ImGuiColors.DalamudYellow, $"{result.PhysicalAbility}"); : ImGuiColors.DalamudYellow, $"{result.PhysicalAbility}");
ImGui.SameLine(0); ImGui.SameLine(0);
ImGui.Text("/"); ImGui.Text("/");
ImGui.SameLine(0); ImGui.SameLine(0);
ImGui.TextColored( ImGui.TextColored(
result.MentalAbility >= selectedMission.CurrentAttributes.MentalAbility result.MentalAbility >= missionAttributes.MentalAbility
? ImGuiColors.HealerGreen ? ImGuiColors.HealerGreen
: ImGuiColors.DalamudYellow, $"{result.MentalAbility}"); : ImGuiColors.DalamudYellow, $"{result.MentalAbility}");
ImGui.SameLine(0); ImGui.SameLine(0);
ImGui.Text("/"); ImGui.Text("/");
ImGui.SameLine(0); ImGui.SameLine(0);
ImGui.TextColored( ImGui.TextColored(
result.TacticalAbility >= selectedMission.CurrentAttributes.TacticalAbility result.TacticalAbility >= missionAttributes.TacticalAbility
? ImGuiColors.HealerGreen ? ImGuiColors.HealerGreen
: ImGuiColors.DalamudYellow, $"{result.TacticalAbility}"); : ImGuiColors.DalamudYellow, $"{result.TacticalAbility}");
@ -193,7 +199,7 @@ internal sealed class MainWindow : Window, IDisposable
} }
else else
{ {
ImGui.TextColored(ImGuiColors.DalamudRed, $"No combination of members/trainings can achieve\n{(selectedMission.IsFlaggedMission ? "all" : "2 out of 3")} attributes for {selectedMission.CurrentAttributes.PhysicalAbility} / {selectedMission.CurrentAttributes.MentalAbility} / {selectedMission.CurrentAttributes.TacticalAbility}."); ImGui.TextColored(ImGuiColors.DalamudRed, $"No combination of members/trainings can achieve\n{(selectedMission.IsFlaggedMission ? "all" : "2 out of 3")} attributes for {missionAttributes}.");
ImGui.Text("Level the squadron further and check again."); ImGui.Text("Level the squadron further and check again.");
} }
} }
@ -204,16 +210,13 @@ internal sealed class MainWindow : Window, IDisposable
} }
else else
{ {
if (selectedMission.CurrentAttributes == null) if (missionAttributes == null)
FindCurrentAttributeIndex(agentExpedition, selectedMission);
if (selectedMission.CurrentAttributes == null)
{ {
ImGui.Text("No matching mission found...?"); ImGui.Text("No matching mission found...?");
return; return;
} }
state.SetCalculation(selectedMission, Task.Factory.StartNew(() => state.SetCalculation(selectedMission, missionAttributes, Task.Factory.StartNew(() =>
{ {
var solver = new SquadronSolver(_pluginLog, state, _plugin.Trainings); var solver = new SquadronSolver(_pluginLog, state, _plugin.Trainings);
@ -225,7 +228,7 @@ internal sealed class MainWindow : Window, IDisposable
if (selectedMission.IsFlaggedMission) if (selectedMission.IsFlaggedMission)
{ {
// only relevant when all 3 stats match // only relevant when all 3 stats match
var perfectMatches = solver.SolveFor(selectedMission, 3).ToList(); var perfectMatches = solver.SolveFor(selectedMission, missionAttributes, 3).ToList();
if (perfectMatches.Count > 0) if (perfectMatches.Count > 0)
{ {
results.Results.Add(perfectMatches results.Results.Add(perfectMatches
@ -236,7 +239,7 @@ internal sealed class MainWindow : Window, IDisposable
} }
else else
{ {
var matches = solver.SolveFor(selectedMission, 2) var matches = solver.SolveFor(selectedMission, missionAttributes, 2)
.OrderByDescending(x => x.MatchingAttributes) .OrderByDescending(x => x.MatchingAttributes)
.ThenBy(x => x.Trainings.Count) .ThenBy(x => x.Trainings.Count)
.ThenBy(x => x.Members.Sum(y => y.Level)) .ThenBy(x => x.Members.Sum(y => y.Level))
@ -266,7 +269,7 @@ internal sealed class MainWindow : Window, IDisposable
if (matches.Count > 0) if (matches.Count > 0)
{ {
var suboptimalMatch = matches.First(); var suboptimalMatch = matches.First();
if (suboptimalMatch.Trainings.Count == 0 && perfectMatchesWithTraining.Any()) if (suboptimalMatch.Trainings.Count == 0 && perfectMatchesWithTraining.Count != 0)
results.Results.Insert(results.Results.Count - 1, suboptimalMatch); results.Results.Insert(results.Results.Count - 1, suboptimalMatch);
else else
results.Results.Add(suboptimalMatch); results.Results.Add(suboptimalMatch);
@ -274,12 +277,12 @@ internal sealed class MainWindow : Window, IDisposable
} }
return results; return results;
})); }, default, TaskCreationOptions.LongRunning, TaskScheduler.Default));
} }
} }
} }
private unsafe void FindCurrentAttributeIndex(AgentGcArmyExpedition* agentExpedition, private unsafe Attributes? FindCurrentAttributeIndex(AgentGcArmyExpedition* agentExpedition,
SquadronMission selectedMission) SquadronMission selectedMission)
{ {
AddonGcArmyExpedition* addonExpedition = AddonGcArmyExpedition* addonExpedition =
@ -287,11 +290,11 @@ internal sealed class MainWindow : Window, IDisposable
// should never happen // should never happen
if (addonExpedition == null || !LAddon.IsAddonReady(&addonExpedition->AtkUnitBase)) if (addonExpedition == null || !LAddon.IsAddonReady(&addonExpedition->AtkUnitBase))
return; return null;
AtkComponentBase* requiredAttribComponent = addonExpedition->RequiredAttributesComponentNode; AtkComponentBase* requiredAttribComponent = addonExpedition->RequiredAttributesComponentNode;
if (requiredAttribComponent == null) if (requiredAttribComponent == null)
return; return null;
AtkComponentNode* physicalComponent = GetNodeById<AtkComponentNode>(requiredAttribComponent->UldManager, 2); AtkComponentNode* physicalComponent = GetNodeById<AtkComponentNode>(requiredAttribComponent->UldManager, 2);
AtkComponentNode* mentalComponent = GetNodeById<AtkComponentNode>(requiredAttribComponent->UldManager, 4); AtkComponentNode* mentalComponent = GetNodeById<AtkComponentNode>(requiredAttribComponent->UldManager, 4);
@ -299,7 +302,7 @@ internal sealed class MainWindow : Window, IDisposable
if (physicalComponent == null || mentalComponent == null || tacticalComponent == null) if (physicalComponent == null || mentalComponent == null || tacticalComponent == null)
{ {
_pluginLog.Warning("Could not parse required attribute children"); _pluginLog.Warning("Could not parse required attribute children");
return; return null;
} }
AtkTextNode* physicalText = GetNodeById<AtkTextNode>(physicalComponent->Component->UldManager, 2); AtkTextNode* physicalText = GetNodeById<AtkTextNode>(physicalComponent->Component->UldManager, 2);
@ -308,19 +311,26 @@ internal sealed class MainWindow : Window, IDisposable
if (physicalText == null || mentalText == null || tacticalText == null) if (physicalText == null || mentalText == null || tacticalText == null)
{ {
_pluginLog.Warning("Could not parse required attribute texts"); _pluginLog.Warning("Could not parse required attribute texts");
return; return null;
} }
int physical = int.Parse(physicalText->NodeText.ToString()); int physical = int.Parse(physicalText->NodeText.ToString(), CultureInfo.InvariantCulture);
int mental = int.Parse(mentalText->NodeText.ToString()); int mental = int.Parse(mentalText->NodeText.ToString(), CultureInfo.InvariantCulture);
int tactical = int.Parse(tacticalText->NodeText.ToString()); int tactical = int.Parse(tacticalText->NodeText.ToString(), CultureInfo.InvariantCulture);
selectedMission.CurrentAttributes = new Attributes var newAttributes = new Attributes
{ {
PhysicalAbility = physical, PhysicalAbility = physical,
MentalAbility = mental, MentalAbility = mental,
TacticalAbility = tactical, 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) private static unsafe T* GetNodeById<T>(AtkUldManager uldManager, uint nodeId, NodeType? type = null)
@ -329,8 +339,8 @@ internal sealed class MainWindow : Window, IDisposable
for (var i = 0; i < uldManager.NodeListCount; i++) for (var i = 0; i < uldManager.NodeListCount; i++)
{ {
var n = uldManager.NodeList[i]; var n = uldManager.NodeList[i];
if (n->NodeID != nodeId || type != null && n->Type != type.Value) continue; if (n->NodeId != nodeId || type != null && n->Type != type.Value) continue;
if (!n->IsVisible) continue; if (!n->IsVisible()) continue;
return (T*)n; return (T*)n;
} }

View File

@ -1,15 +1,86 @@
{ {
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net7.0-windows7.0": { "net8.0-windows7.0": {
"DalamudPackager": { "DalamudPackager": {
"type": "Direct", "type": "Direct",
"requested": "[2.1.12, )", "requested": "[2.1.13, )",
"resolved": "2.1.12", "resolved": "2.1.13",
"contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg==" "contentHash": "rMN1omGe8536f4xLMvx9NwfvpAc9YFFfeXJ1t4P4PE6Gu8WCIoFliR1sh07hM+bfODmesk/dvMbji7vNI+B/pQ=="
},
"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": { "llib": {
"type": "Project" "type": "Project",
"dependencies": {
"DalamudPackager": "[2.1.13, )"
}
} }
} }
} }

View File

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