diff --git a/Squadronista/Solver/Attributes.cs b/Squadronista/Solver/Attributes.cs index 2977695..999d8f9 100644 --- a/Squadronista/Solver/Attributes.cs +++ b/Squadronista/Solver/Attributes.cs @@ -37,4 +37,9 @@ internal class Attributes : IEquatable { return !Equals(left, right); } + + public override string ToString() + { + return $"{PhysicalAbility} / {MentalAbility} / {TacticalAbility}"; + } } diff --git a/Squadronista/Solver/SquadronMember.cs b/Squadronista/Solver/SquadronMember.cs index 774d6ae..8b52a4f 100644 --- a/Squadronista/Solver/SquadronMember.cs +++ b/Squadronista/Solver/SquadronMember.cs @@ -10,12 +10,12 @@ internal sealed class SquadronMember : IEquatable { 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 required uint ClassJob { get; init; } + + public required Race Race { get; init; } + public required uint EnlistmentTimestamp { get; init; } + public uint Experience { get; init; } public int PhysicalAbility => GrowthParams[Level].PhysicalAbility; public int MentalAbility => GrowthParams[Level].MentalAbility; public int TacticalAbility => GrowthParams[Level].TacticalAbility; @@ -34,6 +34,7 @@ internal sealed class SquadronMember : IEquatable { growthAsList.Add((growth.Physical[i], growth.Mental[i], growth.Tactical[i])); } + growthAsList.Add((growth.Unknown123, growth.Unknown184, growth.Unknown245)); GrowthParams = growthAsList; return this; @@ -43,7 +44,8 @@ internal sealed class SquadronMember : IEquatable { 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; + 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) @@ -65,4 +67,11 @@ internal sealed class SquadronMember : IEquatable { return !Equals(left, right); } + + public Attributes ToAttributes() => new() + { + PhysicalAbility = PhysicalAbility, + MentalAbility = MentalAbility, + TacticalAbility = TacticalAbility, + }; } diff --git a/Squadronista/Solver/SquadronState.cs b/Squadronista/Solver/SquadronState.cs index b0ed1fe..a416f8b 100644 --- a/Squadronista/Solver/SquadronState.cs +++ b/Squadronista/Solver/SquadronState.cs @@ -7,9 +7,9 @@ internal sealed class SquadronState { private readonly Dictionary> _calculationResults = new(); - public required byte Rank { get; init; } public required IReadOnlyList Members { get; init; } public required BonusAttributes Bonus { get; set; } + public required uint CurrentTraining { get; set; } public Task? GetCalculation(SquadronMission mission) => _calculationResults.TryGetValue(mission, out var task) ? task : null; diff --git a/Squadronista/Squadronista.csproj b/Squadronista/Squadronista.csproj index 062bbaa..c5d6672 100644 --- a/Squadronista/Squadronista.csproj +++ b/Squadronista/Squadronista.csproj @@ -1,7 +1,7 @@ net7.0-windows - 1.1 + 2.0 11.0 enable true diff --git a/Squadronista/SquadronistaPlugin.cs b/Squadronista/SquadronistaPlugin.cs index 91c0745..a4377be 100644 --- a/Squadronista/SquadronistaPlugin.cs +++ b/Squadronista/SquadronistaPlugin.cs @@ -1,15 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; +using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; 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.Game; using FFXIVClientStructs.FFXIV.Client.UI; -using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Excel.GeneratedSheets; using Squadronista.Solver; using Squadronista.Windows; @@ -26,12 +24,11 @@ public class SquadronistaPlugin : IDalamudPlugin private readonly IPluginLog _pluginLog; private readonly IDataManager _dataManager; private readonly IAddonLifecycle _addonLifecycle; - private readonly IReadOnlyDictionary _classNamesToId; private readonly IReadOnlyList _allMissions; private readonly MainWindow _mainWindow; public SquadronistaPlugin(DalamudPluginInterface pluginInterface, IClientState clientState, IPluginLog pluginLog, - IDataManager dataManager, IAddonLifecycle addonLifecycle, IGameGui gameGui, IFramework framework) + IDataManager dataManager, IAddonLifecycle addonLifecycle, IGameGui gameGui) { _pluginInterface = pluginInterface; _clientState = clientState; @@ -39,12 +36,6 @@ public class SquadronistaPlugin : IDalamudPlugin _dataManager = dataManager; _addonLifecycle = addonLifecycle; - _classNamesToId = dataManager.GetExcelSheet()! - .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()! .Where(x => x.RowId > 0) .Select(x => new SquadronMission @@ -98,68 +89,81 @@ public class SquadronistaPlugin : IDalamudPlugin private unsafe void UpdateSquadronState(AddonEvent type, AddonArgs args) { _pluginLog.Information("Updating squadron state from member list"); - - AtkUnitBase* addon = (AtkUnitBase*)args.Addon; - if (addon->AtkValuesCount != 133) + var gcArmyManager = GcArmyManager.Instance(); + if (gcArmyManager == null) { - _pluginLog.Error("Unexpected AddonGcArmyMemberList atkvalues count"); + _pluginLog.Warning("No GcArmyManager"); ResetCharacterSpecificData(); return; } - var atkValues = addon->AtkValues; - uint memberCount = atkValues[4].UInt; - - // can't do any missions like this... - if (memberCount < 4) + if (gcArmyManager->Data == null) { + _pluginLog.Warning("No GcArmyManager->Data"); ResetCharacterSpecificData(); return; } - IReadOnlyList members = Enumerable.Range(0, (int)memberCount) - .Select(i => new SquadronMember + if (gcArmyManager->GetMemberCount() < 4) + { + _pluginLog.Warning($"Not enough squadron members to send on missions, only got {gcArmyManager->GetMemberCount()} members"); + ResetCharacterSpecificData(); + return; + } + + IReadOnlyList members = Enumerable.Range(0, (int)gcArmyManager->GetMemberCount()) + .Select(i => { - 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)) + var member = gcArmyManager->GetMember((uint)i); + if (member == null) + return null; + + return new SquadronMember + { + Name = _dataManager.GetExcelSheet()!.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() + .OrderBy(x => x.EnlistmentTimestamp) .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(); var bonus = new BonusAttributes { - PhysicalAbility = attributes[0], - MentalAbility = attributes[1], - TacticalAbility = attributes[2], - Cap = attributes.Sum() + PhysicalAbility = gcArmyManager->Data->BonusPhysical, + MentalAbility = gcArmyManager->Data->BonusMental, + TacticalAbility = gcArmyManager->Data->BonusTactical, + Cap = gcArmyManager->Data->BonusPhysical + gcArmyManager->Data->BonusMental + gcArmyManager->Data->BonusTactical, }; if (SquadronState != null && members.SequenceEqual(SquadronState.Members) && - rank == SquadronState.Rank && bonus == SquadronState.Bonus) { // nothing changed... + _pluginLog.Verbose("Not updating SquadronState, not changed"); return; } SquadronState = new SquadronState { Members = members, - Rank = rank, Bonus = bonus, + CurrentTraining = ((ExtendedGcArmyData*)gcArmyManager->Data)->CurrentTraining, }; + _pluginLog.Verbose( + $"Bonus stats: {bonus} (Cap: {bonus.Cap})"); 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) @@ -184,13 +188,6 @@ public class SquadronistaPlugin : IDalamudPlugin .ToList(); } - private unsafe string? ReadAtkString(AtkValue atkValue) - { - if (atkValue.String != null) - return MemoryHelper.ReadSeStringNullTerminated(new nint(atkValue.String)).ToString(); - return null; - } - private void ResetCharacterSpecificData() { SquadronState = null; @@ -207,4 +204,9 @@ public class SquadronistaPlugin : IDalamudPlugin _clientState.Logout -= ResetCharacterSpecificData; _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; } + + [StructLayout(LayoutKind.Explicit, Size = 0xB18)] + private struct ExtendedGcArmyData { + [FieldOffset(0x284)] public ushort CurrentTraining; + } } diff --git a/Squadronista/Windows/MainWindow.cs b/Squadronista/Windows/MainWindow.cs index 289261d..effaa1f 100644 --- a/Squadronista/Windows/MainWindow.cs +++ b/Squadronista/Windows/MainWindow.cs @@ -97,13 +97,20 @@ internal sealed class MainWindow : LImGui.LWindow, IDisposable } var selectedMission = _plugin.AvailableMissions[agentExpedition->SelectedRow]; - ImGui.Text($"{selectedMission.Name}"); + if (selectedMission.CurrentAttributes != null) + ImGui.Text($"{selectedMission.Name} ({selectedMission.CurrentAttributes})"); + else + ImGui.Text($"{selectedMission.Name}"); var state = _plugin.SquadronState; if (state == null) { 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 { var task = state.GetCalculation(selectedMission); @@ -188,7 +195,7 @@ internal sealed class MainWindow : LImGui.LWindow, IDisposable } 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 {selectedMission.CurrentAttributes}."); ImGui.Text("Level the squadron further and check again."); } } @@ -319,7 +326,7 @@ internal sealed class MainWindow : LImGui.LWindow, IDisposable if (selectedMission.PossibleAttributes.Contains(newAttributes)) selectedMission.CurrentAttributes = newAttributes; else - _pluginLog.Warning($"Wrong attributes for {selectedMission.Name}: {physical} / {mental} / {tactical}"); + _pluginLog.Warning($"Wrong attributes for {selectedMission.Name}: {newAttributes}"); } private static unsafe T* GetNodeById(AtkUldManager uldManager, uint nodeId, NodeType? type = null)