Initial testing

This commit is contained in:
Liza 2023-10-09 19:29:35 +02:00
commit 8e3d039ba4
Signed by: liza
GPG Key ID: 7199F8D727D55F67
17 changed files with 891 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/.idea
*.user

16
Squadronista.sln Normal file
View File

@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squadronista", "Squadronista\Squadronista.csproj", "{71E7BF47-88EE-48F4-8FBE-F89F20F34F38}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{71E7BF47-88EE-48F4-8FBE-F89F20F34F38}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

3
Squadronista/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/dist
/obj
/bin

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<Target Name="PackagePlugin" AfterTargets="Build" Condition="'$(Configuration)' == 'Debug'">
<DalamudPackager
ProjectDir="$(ProjectDir)"
OutputPath="$(OutputPath)"
AssemblyName="$(AssemblyName)"
MakeZip="false"
VersionComponents="2"/>
</Target>
<Target Name="PackagePlugin" AfterTargets="Build" Condition="'$(Configuration)' == 'Release'">
<DalamudPackager
ProjectDir="$(ProjectDir)"
OutputPath="$(OutputPath)"
AssemblyName="$(AssemblyName)"
MakeZip="true"
VersionComponents="2"
Exclude="Squadronista.deps.json"/>
</Target>
</Project>

View File

@ -0,0 +1,107 @@
using System;
namespace Squadronista.Solver;
/// <summary>
/// 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).
/// </summary>
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 BonusAttributes ApplyTraining(Training training)
{
if (PhysicalAbility + MentalAbility + TacticalAbility == Cap)
{
int physicalGained = training.CappedPhysicalGained;
int mentalGained = training.CappedMentalGained;
int tacticalGained = training.CappedTacticalGained;
int newPhysicalAbility = PhysicalAbility + physicalGained;
int newMentalAbility = MentalAbility + mentalGained;
int newTacticalAbility = TacticalAbility + tacticalGained;
Fix(ref newPhysicalAbility, physicalGained,
ref newMentalAbility, mentalGained,
ref newTacticalAbility, tacticalGained);
Fix(ref newMentalAbility, mentalGained,
ref newPhysicalAbility, physicalGained,
ref newTacticalAbility, tacticalGained);
Fix(ref newTacticalAbility, tacticalGained,
ref newPhysicalAbility, physicalGained,
ref newMentalAbility, mentalGained);
return new BonusAttributes
{
PhysicalAbility = newPhysicalAbility,
MentalAbility = newMentalAbility,
TacticalAbility = newTacticalAbility,
Cap = Cap
};
}
else
{
return new BonusAttributes
{
PhysicalAbility = PhysicalAbility + training.PhysicalGained,
MentalAbility = MentalAbility + training.MentalGained,
TacticalAbility = TacticalAbility + training.TacticalGained,
Cap = Cap + training.PhysicalGained + training.MentalGained + training.TacticalGained,
};
}
}
private void Fix(ref int mainStat, int mainGained, ref int otherStatA, int otherGainedA, ref int otherStatB, int otherGainedB)
{
if (mainStat >= 0 || mainGained > 0)
return;
// if both stats should gain, this training is just invalid
if (otherGainedA > 0 && otherGainedB > 0)
return;
if (otherGainedA > 0)
otherStatB += mainStat;
if (otherGainedB > 0)
otherStatA += mainStat;
mainStat = 0;
}
public bool Equals(BonusAttributes? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return PhysicalAbility == other.PhysicalAbility && MentalAbility == other.MentalAbility && TacticalAbility == other.TacticalAbility && Cap == other.Cap;
}
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((BonusAttributes)obj);
}
public override int GetHashCode()
{
return HashCode.Combine(PhysicalAbility, MentalAbility, TacticalAbility, Cap);
}
public static bool operator ==(BonusAttributes? left, BonusAttributes? right)
{
return Equals(left, right);
}
public static bool operator !=(BonusAttributes? left, BonusAttributes? right)
{
return !Equals(left, right);
}
}

View File

@ -0,0 +1,13 @@
namespace Squadronista.Solver;
internal enum Race
{
Hyur = 1,
Elezen,
Lalafell,
Miqote,
Roegadyn,
AuRa,
// Hrotgar,
// Viera
}

View File

@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets;
namespace Squadronista.Solver;
internal sealed class SquadronMember
{
public required string Name { get; init; }
public required int Level { get; init; }
public required uint ClassJob { get; init; }
// TODO
public required Race Race { get; init; }
// TODO
public int Experience { get; init; }
public int PhysicalAbility => GrowthParams[Level].PhysicalAbility;
public int MentalAbility => GrowthParams[Level].MentalAbility;
public int TacticalAbility => GrowthParams[Level].TacticalAbility;
public IReadOnlyList<(byte PhysicalAbility, byte MentalAbility, byte TacticalAbility)> GrowthParams
{
get;
private set;
} = new List<(byte PhysicalAbility, byte MentalAbility, byte TacticalAbility)>().AsReadOnly();
public SquadronMember InitializeFrom(IDataManager dataManager)
{
List<(byte, byte, byte)> growthAsList = new();
var growth = dataManager.GetExcelSheet<GcArmyMemberGrow>()!.Single(x => x.ClassJob.Row == ClassJob);
for (int i = 0; i <= 59; ++i)
{
growthAsList.Add((growth.Physical[i], growth.Mental[i], growth.Tactical[i]));
}
growthAsList.Add((growth.Unknown123, growth.Unknown184, growth.Unknown245));
GrowthParams = growthAsList;
return this;
}
}

View File

@ -0,0 +1,208 @@
using System.Collections.Generic;
using System.Linq;
using Dalamud.Plugin.Services;
namespace Squadronista.Solver;
internal sealed class SquadronSolver
{
private readonly IPluginLog _pluginLog;
private readonly SquadronState _state;
private readonly IReadOnlyList<Training> _trainings;
private readonly IReadOnlyList<List<SquadronMember>> _memberCombinations;
private readonly IReadOnlyList<BonusAttributes> _allBonusCombinations;
private readonly Dictionary<BonusAttributes, IReadOnlyList<Training>> _calculatedTrainings = new();
private List<BonusAttributes> _newTrainings;
private int _calculatedTrainingSteps;
public SquadronSolver(IPluginLog pluginLog, SquadronState state, IReadOnlyList<Training> trainings)
{
_pluginLog = pluginLog;
_state = state;
_trainings = trainings;
_memberCombinations = MakeCombinations(_state.Members, 4)
.DistinctBy(x => string.Join("|", x.Select(y => y.Name).Order()))
.ToList()
.AsReadOnly();
foreach (var combo in _memberCombinations)
pluginLog.Verbose($"Squadron member combination: {string.Join(" ", combo.Select(x => x.Name))} → {combo.Sum(x => x.PhysicalAbility)} / {combo.Sum(x => x.MentalAbility)} / {combo.Sum(x => x.TacticalAbility)}");
_allBonusCombinations = CalculateAllBonuses()
.Where(x => x != _state.Bonus)
.ToList()
.AsReadOnly();
_calculatedTrainings[_state.Bonus] = new List<Training>();
_newTrainings = _calculatedTrainings.Keys.ToList();
}
public IEnumerable<CalculationResult> SolveFor(SquadronMission mission)
{
int minPhysical = mission.PhysicalAbility;
int minMental = mission.MentalAbility;
int minTactical = mission.TacticalAbility;
bool foundWithoutTraining = false;
List<CalculationResult> intermediates = CalculateForAllMemberCombinations(mission.Level, _state.Bonus);
foreach (var x in intermediates)
{
//_pluginLog.Information($"{string.Join(" ", x.Members.Select(y => y.Name))} → {x.PhysicalAbility} / {x.MentalAbility} / {x.TacticalAbility}");
if (x.PhysicalAbility >= minPhysical && x.MentalAbility >= minMental && x.TacticalAbility >= minTactical)
{
x.TrainingsCalculated = true;
yield return x.WithMission(mission);
foundWithoutTraining = true;
}
}
if (!foundWithoutTraining)
{
foreach (var bonus in _allBonusCombinations)
{
intermediates = CalculateForAllMemberCombinations(mission.Level, bonus);
foreach (var x in intermediates)
{
if (x.PhysicalAbility >= minPhysical && x.MentalAbility >= minMental &&
x.TacticalAbility >= minTactical)
{
CalculateTrainingsForBonus(x);
if (x.TrainingsCalculated)
yield return x.WithMission(mission);
}
}
}
}
}
private List<CalculationResult> CalculateForAllMemberCombinations(int requiredLevel, BonusAttributes bonus)
{
return _memberCombinations
.Where(x => x.Any(y => y.Level >= requiredLevel))
.Select(x => new CalculationResult(x, bonus))
.ToList();
}
private List<List<T>> MakeCombinations<T>(IReadOnlyList<T> entries, int count)
{
if (count == 0)
return new List<List<T>>();
if (count == 1)
return entries.Select(x => new List<T> { x }).ToList();
return entries.SelectMany(x =>
{
var combos = MakeCombinations(entries.Except(new[] { x }).ToList(), count - 1);
combos.ForEach(c => c.Insert(0, x));
return combos;
}).ToList();
}
private IEnumerable<BonusAttributes> CalculateAllBonuses()
{
for (int physical = 0; physical <= _state.Bonus.Cap; physical += 20)
{
for (int mental = 0; mental <= _state.Bonus.Cap - physical; mental += 20)
{
yield return new BonusAttributes
{
PhysicalAbility = physical,
MentalAbility = mental,
TacticalAbility = _state.Bonus.Cap - physical - mental,
Cap = _state.Bonus.Cap
};
}
}
}
private void CalculateTrainingsForBonus(CalculationResult result)
{
if (result.Trainings.Count > 0)
return;
int currentPhysical = _state.Bonus.PhysicalAbility;
int currentMental = _state.Bonus.MentalAbility;
int currentTactical = _state.Bonus.TacticalAbility;
if (_calculatedTrainings.TryGetValue(result.Bonus, out var calculatedTraining))
{
_pluginLog.Information($"Found existing steps: {string.Join(", ", calculatedTraining.Select(x => x.Name))}");
result.Trainings = calculatedTraining;
result.TrainingsCalculated = true;
return;
}
_pluginLog.Verbose($"Trying to find steps from {currentPhysical} to {result.Bonus.PhysicalAbility}, {currentMental} to {result.Bonus.MentalAbility} and {currentTactical} to {result.Bonus.TacticalAbility}");
while (_calculatedTrainingSteps < 10)
{
_calculatedTrainingSteps++;
_pluginLog.Debug($"Calculating training step {_calculatedTrainingSteps} from currently {_calculatedTrainings.Count} training combinations");
List<BonusAttributes> newerTrainings = new();
foreach (var bonus in _newTrainings)
{
_pluginLog.Verbose($" From {bonus.PhysicalAbility} / {bonus.MentalAbility} / {bonus.TacticalAbility}");
foreach (var training in _trainings)
{
var newBonus = bonus.ApplyTraining(training);
if (newBonus.PhysicalAbility < 0 || newBonus.MentalAbility < 0 || newBonus.TacticalAbility < 0)
continue;
if (_calculatedTrainings.ContainsKey(newBonus))
continue;
_pluginLog.Verbose($" → {newBonus.PhysicalAbility} / {newBonus.MentalAbility} / {newBonus.TacticalAbility} with {training.Name} (c = {newBonus.Cap})");
_calculatedTrainings[newBonus] = _calculatedTrainings[bonus].Concat(new[] { training }).ToList().AsReadOnly();
newerTrainings.Add(newBonus);
}
}
_newTrainings = newerTrainings;
_pluginLog.Verbose($"Finished calculating, we now have {_calculatedTrainings.Count} training combinations");
if (newerTrainings.Count == 0)
break;
if (_calculatedTrainings.TryGetValue(result.Bonus, out calculatedTraining))
{
_pluginLog.Verbose($"Found steps to reach {result.PhysicalAbility} / {result.MentalAbility} / {result.TacticalAbility}: {string.Join(", ", calculatedTraining.Select(x => x.Name))}");
result.Trainings = calculatedTraining;
result.TrainingsCalculated = true;
return;
}
}
}
public sealed class CalculationResult
{
public SquadronMission? Mission { get; private set; }
public int PhysicalAbility { get; }
public int MentalAbility { get; }
public int TacticalAbility { get; }
public List<SquadronMember> Members { get; }
public BonusAttributes Bonus { get; }
public IReadOnlyList<Training> Trainings { get; set; } = new List<Training>().AsReadOnly();
public bool TrainingsCalculated { get; set; }
public CalculationResult(List<SquadronMember> members, BonusAttributes bonus)
{
PhysicalAbility = members.Sum(x => x.PhysicalAbility) + bonus.PhysicalAbility;
MentalAbility = members.Sum(x => x.MentalAbility) + bonus.MentalAbility;
TacticalAbility = members.Sum(x => x.TacticalAbility) + bonus.TacticalAbility;
Members = members;
Bonus = bonus;
}
public CalculationResult WithMission(SquadronMission mission)
{
Mission = mission;
return this;
}
}
}

View File

@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Squadronista.Solver;
internal sealed class SquadronState
{
public required byte Rank { get; init; }
public required IReadOnlyList<SquadronMember> Members { get; init; }
public required BonusAttributes Bonus { get; set; }
public Task<SquadronSolver.CalculationResult?>? CalculationTask { get; set; }
}

View File

@ -0,0 +1,25 @@
namespace Squadronista.Solver;
public class Training
{
public required uint RowId { get; init; }
public required string Name { get; init; }
public required int PhysicalGained { get; init; }
public required int MentalGained { get; init; }
public required int TacticalGained { get; init; }
public int CappedPhysicalGained => CalculateCapped(PhysicalGained, MentalGained, TacticalGained);
public int CappedMentalGained => CalculateCapped(MentalGained, PhysicalGained, TacticalGained);
public int CappedTacticalGained => CalculateCapped(TacticalGained, PhysicalGained, MentalGained);
private int CalculateCapped(int mainStat, int otherA, int otherB)
{
if (mainStat > 0)
return mainStat;
if (otherA == 40 || otherB == 40)
return -20;
return -40;
}
}

View File

@ -0,0 +1,12 @@
namespace Squadronista;
internal sealed class SquadronMission
{
public required int Id { get; init; }
public required string Name { get; init; }
public required byte Level { get; init; }
public required bool IsFlaggedMission { get; init; }
public int PhysicalAbility { get; init; }
public int MentalAbility { get; init; }
public int TacticalAbility { get; init; }
}

View File

@ -0,0 +1,60 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<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>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DebugType>portable</DebugType>
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<DebugType>portable</DebugType>
</PropertyGroup>
<PropertyGroup>
<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>
<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>

View File

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

View File

@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Interface.Windowing;
using Dalamud.Memory;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Excel.GeneratedSheets;
using Squadronista.Solver;
using Squadronista.Windows;
using Race = Squadronista.Solver.Race;
namespace Squadronista;
public class SquadronistaPlugin : IDalamudPlugin
{
private readonly WindowSystem _windowSystem = new(nameof(SquadronistaPlugin));
private readonly DalamudPluginInterface _pluginInterface;
private readonly IClientState _clientState;
private readonly IPluginLog _pluginLog;
private readonly IDataManager _dataManager;
private readonly IAddonLifecycle _addonLifecycle;
private readonly IReadOnlyDictionary<string, uint> _classNamesToId;
private readonly IReadOnlyList<SquadronMission> _allMissions;
private readonly MainWindow _mainWindow;
public SquadronistaPlugin(DalamudPluginInterface pluginInterface, IClientState clientState, IPluginLog pluginLog,
IDataManager dataManager, IAddonLifecycle addonLifecycle)
{
_pluginInterface = pluginInterface;
_clientState = clientState;
_pluginLog = pluginLog;
_dataManager = dataManager;
_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>()!
.Where(x => x.RowId > 0)
.Select(x => new SquadronMission
{
Id = (int)x.RowId,
Name = x.Name.ToString(),
Level = x.RequiredLevel,
// 13 and 14 seems to be a duplicate
IsFlaggedMission = x.RowId is 7 or 14 or 15 or 34,
// not sure why this is structured the way it is
// 'Supply Wagon Escort', for example, has the following physical values:
// 210, 125, 305, 305, 210, 115
// and the UI shows 115 as required
PhysicalAbility = x.RequiredPhysical.Last(),
MentalAbility = x.RequiredMental.Last(),
TacticalAbility = x.RequiredTactical.Last(),
})
.ToList()
.AsReadOnly();
Trainings = dataManager.GetExcelSheet<GcArmyTraining>()!
.Where(x => x.RowId > 0 && x.RowId != 7)
.Select(x => new Training
{
RowId = x.RowId,
Name = x.Name.ToString(),
PhysicalGained = x.PhysicalBonus,
MentalGained = x.MentalBonus,
TacticalGained = x.TacticalBonus
})
.ToList()
.AsReadOnly();
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_clientState.Logout += Logout;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GcArmyMemberList", UpdateSquadronState);
_addonLifecycle.RegisterListener(AddonEvent.PostRefresh, "GcArmyExpedition", UpdateExpeditionState);
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GcArmyExpedition", UpdateExpeditionState);
_mainWindow = new MainWindow(this, pluginLog, addonLifecycle);
_windowSystem.AddWindow(_mainWindow);
}
internal SquadronState? SquadronState { get; set; }
internal List<SquadronMission> AvailableMissions { get; set; } = new();
internal IReadOnlyList<Training> Trainings { get; set; }
private unsafe void UpdateSquadronState(AddonEvent type, AddonArgs args)
{
AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
if (addon->AtkValuesCount != 133)
{
_pluginLog.Error("Unexpected AddonGcArmyMemberList atkvalues count");
return;
}
var atkValues = addon->AtkValues;
uint memberCount = atkValues[4].UInt;
// can't do any missions like this...
if (memberCount < 4)
return;
IReadOnlyList<SquadronMember> members = Enumerable.Range(0, (int)memberCount)
.Select(i => new SquadronMember
{
Name = ReadAtkString(atkValues[6 + 15 * i])!,
Level = atkValues[10 + 15 * i].Int,
ClassJob = _classNamesToId[ReadAtkString(atkValues[7 + 15 * i])!.ToLower()],
Race = Race.Lalafell, // TODO
Experience = 0, // TODO
}.InitializeFrom(_dataManager))
.ToList()
.AsReadOnly();
byte rank = byte.Parse(ReadAtkString(atkValues[0])!.Split(":")[1].Trim());
int[] attributes = ReadAtkString(atkValues[1])!.Split(":")[1].Trim()
.Split("/")
.Select(int.Parse)
.ToArray();
SquadronState = new SquadronState
{
Members = members,
Rank = rank,
Bonus = new BonusAttributes
{
PhysicalAbility = attributes[0],
MentalAbility = attributes[1],
TacticalAbility = attributes[2],
Cap = attributes.Sum()
},
};
foreach (var member in members)
_pluginLog.Information($"MM → {member.Name}, {member.ClassJob}, Lv{member.Level}");
}
private unsafe void UpdateExpeditionState(AddonEvent type, AddonArgs args)
{
AvailableMissions.Clear();
AddonGcArmyExpedition* addonExpedition = (AddonGcArmyExpedition*)args.Addon;
if (addonExpedition->AtkUnitBase.AtkValuesCount != 216)
{
_pluginLog.Error("Unexpected AddonGcArmyExpedition atkvalues count");
return;
}
var atkValues = addonExpedition->AtkUnitBase.AtkValues;
int missionCount = atkValues[6].Int;
_pluginLog.Information($"Missions → {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))
.ToList();
}
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;
}
public void Dispose()
{
_mainWindow.Dispose();
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GcArmyExpedition", UpdateExpeditionState);
_addonLifecycle.UnregisterListener(AddonEvent.PostRefresh, "GcArmyExpedition", UpdateExpeditionState);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GcArmyMemberList", UpdateSquadronState);
_clientState.Logout -= Logout;
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
}
}

View File

@ -0,0 +1,153 @@
using System;
using System.Linq;
using System.Numerics;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using Squadronista.Solver;
using Task = System.Threading.Tasks.Task;
namespace Squadronista.Windows;
internal sealed class MainWindow : Window, IDisposable
{
private readonly SquadronistaPlugin _plugin;
private readonly IPluginLog _pluginLog;
private readonly IAddonLifecycle _addonLifecycle;
public MainWindow(SquadronistaPlugin plugin, IPluginLog pluginLog, IAddonLifecycle addonLifecycle)
: base("Squadronista##SquadronistaMainWindow")
{
_plugin = plugin;
_pluginLog = pluginLog;
_addonLifecycle = addonLifecycle;
Position = new Vector2(100, 100);
PositionCondition = ImGuiCond.Always;
Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse
;
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GcArmyExpedition", ExpeditionPostSetup);
_addonLifecycle.RegisterListener(AddonEvent.PreFinalize, "GcArmyExpedition", ExpeditionPreFinalize);
_addonLifecycle.RegisterListener(AddonEvent.PostUpdate, "GcArmyExpedition", ExpeditionPostUpdate);
}
private void ExpeditionPostSetup(AddonEvent type, AddonArgs args)
{
IsOpen = true;
}
private void ExpeditionPreFinalize(AddonEvent type, AddonArgs args)
{
IsOpen = false;
}
private unsafe void ExpeditionPostUpdate(AddonEvent type, AddonArgs args)
{
AtkUnitBase* addon = (AtkUnitBase*)args.Addon;
short x = 0, y = 0;
addon->GetPosition(&x, &y);
short width = 0, height = 0;
addon->GetSize(&width, &height, true);
x += width;
if ((short)Position!.Value.X != x || (short)Position!.Value.Y != y)
Position = new Vector2(x, y);
}
public override void Draw()
{
var flaggedMission = _plugin.AvailableMissions.FirstOrDefault(x => x.IsFlaggedMission);
if (flaggedMission == null)
{
ImGui.Text("No flagged mission available.");
return;
}
ImGui.Text($"{flaggedMission.Name}");
ImGui.Indent();
var state = _plugin.SquadronState;
if (state == null)
{
ImGui.TextColored(ImGuiColors.DalamudYellow, "Open Squadron Member list to continue.");
}
else
{
var task = state.CalculationTask;
if (task != null)
{
if (task.IsCompletedSuccessfully)
{
SquadronSolver.CalculationResult? result = task.Result;
if (result != null)
{
if (result.Mission?.Id != flaggedMission.Id)
{
state.CalculationTask = null;
return;
}
ImGui.Text("Squadron Members");
ImGui.Indent();
foreach (var member in result.Members)
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.Text($"{training.Name}");
ImGui.Unindent();
}
ImGui.Spacing();
ImGui.TextColored(result.Trainings.Count == 0 ? ImGuiColors.HealerGreen : ImGuiColors.DalamudYellow, $"Final Stats: {result.PhysicalAbility} / {result.MentalAbility} / {result.TacticalAbility}");
}
else
{
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.");
}
}
else if (task.IsCanceled || task.IsFaulted)
ImGui.Text($"{task.Exception}");
else
ImGui.Text("Calculating...");
}
else
{
state.CalculationTask = Task.Factory.StartNew(() =>
{
var solver = new SquadronSolver(_pluginLog, state, _plugin.Trainings);
var allSolutions = solver.SolveFor(flaggedMission).ToList();
if (allSolutions.Count == 0)
return null;
int shortestTrainings = allSolutions.Min(x => x.Trainings.Count);
return allSolutions
.Where(x => x.Trainings.Count == shortestTrainings)
.OrderBy(x => x.Members.Sum(y => y.Level))
.First();
});
}
}
ImGui.Unindent();
}
public void Dispose()
{
_addonLifecycle.UnregisterListener(AddonEvent.PreFinalize, "GcArmyExpedition", ExpeditionPreFinalize);
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GcArmyExpedition", ExpeditionPostSetup);
}
}

View File

@ -0,0 +1,13 @@
{
"version": 1,
"dependencies": {
"net7.0-windows7.0": {
"DalamudPackager": {
"type": "Direct",
"requested": "[2.1.12, )",
"resolved": "2.1.12",
"contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg=="
}
}
}
}

7
global.json Normal file
View File

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