diff --git a/BossMod/ActionQueue/ActionDefinition.cs b/BossMod/ActionQueue/ActionDefinition.cs index 77154c58c1..5da6a08a03 100644 --- a/BossMod/ActionQueue/ActionDefinition.cs +++ b/BossMod/ActionQueue/ActionDefinition.cs @@ -222,6 +222,27 @@ private ActionDefinitions() // bozja actions for (var i = BozjaHolsterID.None + 1; i < BozjaHolsterID.Count; ++i) RegisterBozja(i); + + // pomanders + for (var i = PomanderID.Safety; i < PomanderID.Count; ++i) + { + var pid = new ActionID(ActionType.Pomander, (uint)i); + _definitions[pid] = new(pid) + { + InstantAnimLock = 2.1f, + AllowedTargets = ActionTargets.Self + }; + } + + for (var i = 1u; i <= 3; i++) + { + var mid = new ActionID(ActionType.Magicite, i); + _definitions[mid] = new(mid) + { + InstantAnimLock = 2.1f, + AllowedTargets = ActionTargets.Self + }; + } } public void Dispose() diff --git a/BossMod/Data/ActionID.cs b/BossMod/Data/ActionID.cs index c8faac2f72..1655f46cf0 100644 --- a/BossMod/Data/ActionID.cs +++ b/BossMod/Data/ActionID.cs @@ -25,6 +25,8 @@ public enum ActionType : byte // below are custom additions, these aren't proper actions from game's point of view, but it makes sense for us to treat them as such BozjaHolsterSlot0 = 0xE0, // id = BozjaHolsterID, use from holster to replace duty action 0 BozjaHolsterSlot1 = 0xE1, // id = BozjaHolsterID, use from holster to replace duty action 1 + Pomander = 0xE2, // id = PomanderID + Magicite = 0xE3, // id = slot (1-3) } public enum Positional { Any, Flank, Rear, Front } diff --git a/BossMod/Data/DeepDungeonState.cs b/BossMod/Data/DeepDungeonState.cs new file mode 100644 index 0000000000..6dbc9fb2d4 --- /dev/null +++ b/BossMod/Data/DeepDungeonState.cs @@ -0,0 +1,180 @@ +using static FFXIVClientStructs.FFXIV.Client.Game.InstanceContent.InstanceContentDeepDungeon; + +namespace BossMod; + +public sealed class DeepDungeonState +{ + public DungeonProgress Progress; + public byte DungeonId; + public RoomFlags[] MapData = new RoomFlags[25]; + public PartyMember[] Party = new PartyMember[4]; + public Item[] Items = new Item[16]; + public Chest[] Chests = new Chest[16]; + public byte[] Magicite = new byte[3]; + + public enum DungeonType : byte + { + None = 0, + POTD = 1, + HOH = 2, + EO = 3 + } + + public DungeonType Type => (DungeonType)DungeonId; + + public record struct DungeonProgress(byte Floor, byte Tileset, byte WeaponLevel, byte ArmorLevel, byte SyncedGearLevel, byte HoardCount, byte ReturnProgress, byte PassageProgress) + { + public readonly bool IsBossFloor => Floor % 10 == 0; + } + public record struct PartyMember(ulong EntityId, byte Room); + public record struct Item(byte Count, byte Flags) + { + public readonly bool Usable => (Flags & (1 << 0)) != 0; + public readonly bool Active => (Flags & (1 << 1)) != 0; + } + public record struct Chest(byte Type, byte Room); + + public Item GetItem(PomanderID pid) => GetSlotForPomander(pid) is var s && s >= 0 ? Items[s] : default; + + public int GetSlotForPomander(PomanderID pid) => Service.LuminaRow(DungeonId)!.Value.PomanderSlot.ToList().FindIndex(p => p.RowId == (uint)pid); + public PomanderID GetPomanderForSlot(int slot) + { + var slots = Service.LuminaRow(DungeonId)!.Value.PomanderSlot; + return slot >= 0 && slot < slots.Count ? (PomanderID)slots[slot].RowId : PomanderID.None; + } + + public bool ReturnActive => Progress.ReturnProgress >= 11; + public bool PassageActive => Progress.PassageProgress >= 11; + public byte Floor => Progress.Floor; + + public IEnumerable CompareToInitial() + { + if (Progress != default || DungeonId != 0) + yield return new OpProgressChange(DungeonId, Progress); + + if (MapData.Any(m => m > 0)) + yield return new OpMapDataChange(MapData); + + if (Party.Any(p => p != default)) + yield return new OpPartyStateChange(Party); + + if (Items.Any(i => i != default)) + yield return new OpItemsChange(Items); + + if (Chests.Any(c => c != default)) + yield return new OpChestsChange(Chests); + + if (Magicite.Any(c => c > 0)) + yield return new OpMagiciteChange(Magicite); + } + + public Event ProgressChanged = new(); + public sealed record class OpProgressChange(byte DungeonId, DungeonProgress Value) : WorldState.Operation + { + protected override void Exec(WorldState ws) + { + ws.DeepDungeon.DungeonId = DungeonId; + ws.DeepDungeon.Progress = Value; + ws.DeepDungeon.ProgressChanged.Fire(this); + } + public override void Write(ReplayRecorder.Output output) + { + output.EmitFourCC("DDPG"u8) + .Emit(DungeonId) + .Emit(Value.Floor) + .Emit(Value.Tileset) + .Emit(Value.WeaponLevel) + .Emit(Value.ArmorLevel) + .Emit(Value.SyncedGearLevel) + .Emit(Value.HoardCount) + .Emit(Value.ReturnProgress) + .Emit(Value.PassageProgress); + } + } + + public Event MapDataChanged = new(); + public sealed record class OpMapDataChange(RoomFlags[] Value) : WorldState.Operation + { + public readonly RoomFlags[] Value = Value; + + protected override void Exec(WorldState ws) + { + ws.DeepDungeon.MapData = Value; + ws.DeepDungeon.MapDataChanged.Fire(this); + } + public override void Write(ReplayRecorder.Output output) + { + output.EmitFourCC("DDMP"u8).Emit(Array.ConvertAll(Value, r => (byte)r)); + } + } + + public Event PartyStateChanged = new(); + public sealed record class OpPartyStateChange(PartyMember[] Value) : WorldState.Operation + { + public readonly PartyMember[] Value = Value; + + protected override void Exec(WorldState ws) + { + ws.DeepDungeon.Party = Value; + ws.DeepDungeon.PartyStateChanged.Fire(this); + } + public override void Write(ReplayRecorder.Output output) + { + output.EmitFourCC("DDPT"u8); + foreach (var member in Value) + output.EmitActor(member.EntityId).Emit(member.Room); + } + } + + public Event ItemsChanged = new(); + public sealed record class OpItemsChange(Item[] Value) : WorldState.Operation + { + public readonly Item[] Value = Value; + + protected override void Exec(WorldState ws) + { + ws.DeepDungeon.Items = Value; + ws.DeepDungeon.ItemsChanged.Fire(this); + } + public override void Write(ReplayRecorder.Output output) + { + output.EmitFourCC("DDIT"u8); + foreach (var item in Value) + output.Emit(item.Count).Emit(item.Flags, "X"); + } + } + + public Event ChestsChanged = new(); + public sealed record class OpChestsChange(Chest[] Value) : WorldState.Operation + { + public readonly Chest[] Value = Value; + + protected override void Exec(WorldState ws) + { + ws.DeepDungeon.Chests = Value; + ws.DeepDungeon.ChestsChanged.Fire(this); + } + public override void Write(ReplayRecorder.Output output) + { + output.EmitFourCC("DDCT"u8); + foreach (var chest in Value) + output.Emit(chest.Type).Emit(chest.Room); + } + } + + public Event MagiciteChanged = new(); + public sealed record class OpMagiciteChange(byte[] Value) : WorldState.Operation + { + public readonly byte[] Value = Value; + + protected override void Exec(WorldState ws) + { + ws.DeepDungeon.Magicite = Value; + ws.DeepDungeon.MagiciteChanged.Fire(this); + } + public override void Write(ReplayRecorder.Output output) + { + output.EmitFourCC("DDMG"u8).Emit(Value); + } + } +} diff --git a/BossMod/Data/PomanderID.cs b/BossMod/Data/PomanderID.cs new file mode 100644 index 0000000000..3bbc827320 --- /dev/null +++ b/BossMod/Data/PomanderID.cs @@ -0,0 +1,48 @@ +namespace BossMod; + +public enum PomanderID : uint +{ + None, + + // Pomanders - PotD/HoH + Safety, + Sight, + Strength, + Steel, + Affluence, + Flight, + Alteration, + Purity, + Fortune, + Witching, + Serenity, + Rage, // palace only + Lust, // palace only + Intuition, + Raising, + Resolution, // palace only + Frailty, // HoH only + Concealment, // HoH only + Petrification, // HoH only + + // Protomanders - EO + ProtoLethargy, + ProtoStorms, + ProtoDread, + ProtoSafety, + ProtoSight, + ProtoStrength, + ProtoSteel, + ProtoAffluence, + ProtoFlight, + ProtoAlteration, + ProtoPurity, + ProtoFortune, + ProtoWitching, + ProtoSerenity, + ProtoIntuition, + ProtoRaising, + + Count +} + diff --git a/BossMod/Data/WorldState.cs b/BossMod/Data/WorldState.cs index e03690d9af..c527daed9d 100644 --- a/BossMod/Data/WorldState.cs +++ b/BossMod/Data/WorldState.cs @@ -16,6 +16,7 @@ public sealed class WorldState public readonly ActorState Actors = new(); public readonly PartyState Party; public readonly ClientState Client = new(); + public readonly DeepDungeonState DeepDungeon = new(); public readonly NetworkState Network = new(); public DateTime CurrentTime => Frame.Timestamp; @@ -69,6 +70,8 @@ public IEnumerable CompareToInitial() yield return o; foreach (var o in Network.CompareToInitial()) yield return o; + foreach (var o in DeepDungeon.CompareToInitial()) + yield return o; } // implementation of operations diff --git a/BossMod/Framework/WorldStateGameSync.cs b/BossMod/Framework/WorldStateGameSync.cs index 0d466e6059..46b52d3333 100644 --- a/BossMod/Framework/WorldStateGameSync.cs +++ b/BossMod/Framework/WorldStateGameSync.cs @@ -4,6 +4,7 @@ using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Control; +using FFXIVClientStructs.FFXIV.Client.Game.Event; using FFXIVClientStructs.FFXIV.Client.Game.Fate; using FFXIVClientStructs.FFXIV.Client.Game.Group; using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent; @@ -182,6 +183,7 @@ public unsafe void Update(TimeSpan prevFramePerf) UpdateActors(); UpdateParty(); UpdateClient(); + UpdateDeepDungeon(); } private unsafe void UpdateWaymarks() @@ -663,6 +665,84 @@ private unsafe void UpdateClient() _ws.Execute(new ClientState.OpFocusTargetChange(focusTargetId)); } + private unsafe void UpdateDeepDungeon() + { + var ddold = _ws.DeepDungeon; + var ddnew = GetDeepDungeonState(); + + if (ddold.DungeonId != ddnew.DungeonId || ddold.Progress != ddnew.Progress) + _ws.Execute(new DeepDungeonState.OpProgressChange(ddnew.DungeonId, ddnew.Progress)); + if (!MemoryExtensions.SequenceEqual(ddold.MapData, ddnew.MapData)) + _ws.Execute(new DeepDungeonState.OpMapDataChange(ddnew.MapData)); + if (!MemoryExtensions.SequenceEqual(ddold.Party, ddnew.Party)) + _ws.Execute(new DeepDungeonState.OpPartyStateChange(ddnew.Party)); + if (!MemoryExtensions.SequenceEqual(ddold.Items, ddnew.Items)) + _ws.Execute(new DeepDungeonState.OpItemsChange(ddnew.Items)); + if (!MemoryExtensions.SequenceEqual(ddold.Chests, ddnew.Chests)) + _ws.Execute(new DeepDungeonState.OpChestsChange(ddnew.Chests)); + if (!MemoryExtensions.SequenceEqual(ddold.Magicite, ddnew.Magicite)) + _ws.Execute(new DeepDungeonState.OpMagiciteChange(ddnew.Magicite)); + } + + private unsafe DeepDungeonState GetDeepDungeonState() + { + var dd = EventFramework.Instance()->GetInstanceContentDeepDungeon(); + if (dd == null) + return new(); + + var progress = new DeepDungeonState.DungeonProgress + { + Floor = dd->Floor, + WeaponLevel = dd->WeaponLevel, + ArmorLevel = dd->ArmorLevel, + + SyncedGearLevel = dd->SyncedGearLevel, + HoardCount = dd->HoardCount, + + ReturnProgress = dd->ReturnProgress, + PassageProgress = dd->PassageProgress, + + Tileset = dd->ActiveLayoutIndex + }; + + var state = new DeepDungeonState + { + Progress = progress, + Magicite = dd->Magicite.ToArray(), + DungeonId = dd->DeepDungeonId + }; + + dd->MapData.CopyTo(state.MapData); + + var ddParty = dd->Party; + for (var i = 0; i < 4; i++) + { + ref var pinfo = ref state.Party[i]; + pinfo.EntityId = (uint)SanitizedObjectID(ddParty[i].EntityId); + pinfo.Room = SanitizeRoom(ddParty[i].RoomIndex); + } + + var ddItem = dd->Items; + for (var i = 0; i < ddItem.Length; i++) + { + ref var pitem = ref state.Items[i]; + pitem.Count = ddItem[i].Count; + pitem.Flags = ddItem[i].Flags; + } + + var ddChest = dd->Chests; + for (var i = 0; i < ddChest.Length; i++) + { + ref var pchest = ref state.Chests[i]; + pchest.Type = ddChest[i].ChestType; + pchest.Room = SanitizeRoom(ddChest[i].RoomIndex); + } + + return state; + } + + private byte SanitizeRoom(sbyte room) => room < 0 ? (byte)0 : (byte)room; + private ulong SanitizedObjectID(ulong raw) => raw != InvalidEntityId ? raw : 0; private void DispatchActorEvents(ulong instanceID) diff --git a/BossMod/Replay/ReplayParserLog.cs b/BossMod/Replay/ReplayParserLog.cs index 48895d881b..a8b63ca187 100644 --- a/BossMod/Replay/ReplayParserLog.cs +++ b/BossMod/Replay/ReplayParserLog.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent; +using System.Globalization; using System.IO; using System.IO.Compression; using System.Threading; @@ -351,6 +352,12 @@ private ReplayParserLog(Input input, ReplayBuilder builder) [new("CLAF"u8)] = ParseClientActiveFate, [new("CPET"u8)] = ParseClientActivePet, [new("CLFT"u8)] = ParseClientFocusTarget, + [new("DDPG"u8)] = ParseDeepDungeonProgress, + [new("DDMP"u8)] = ParseDeepDungeonMap, + [new("DDPT"u8)] = ParseDeepDungeonParty, + [new("DDIT"u8)] = ParseDeepDungeonItems, + [new("DDCT"u8)] = ParseDeepDungeonChests, + [new("DDMG"u8)] = ParseDeepDungeonMagicite, [new("IPCI"u8)] = ParseNetworkIDScramble, [new("IPCS"u8)] = ParseNetworkServerIPC, }; @@ -698,6 +705,44 @@ private ClientState.OpClassJobLevelsChange ParseClientClassJobLevels() private ClientState.OpActivePetChange ParseClientActivePet() => new(new(_input.ReadULong(true), _input.ReadByte(false), _input.ReadByte(false))); private ClientState.OpFocusTargetChange ParseClientFocusTarget() => new(_input.ReadULong(true)); + private DeepDungeonState.OpProgressChange ParseDeepDungeonProgress() => new(_input.ReadByte(false), new DeepDungeonState.DungeonProgress(_input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false), _input.ReadByte(false))); + private DeepDungeonState.OpMapDataChange ParseDeepDungeonMap() => new(Array.ConvertAll(_input.ReadBytes(), b => (InstanceContentDeepDungeon.RoomFlags)b)); + private DeepDungeonState.OpPartyStateChange ParseDeepDungeonParty() + { + var pt = new DeepDungeonState.PartyMember[4]; + for (var i = 0; i < pt.Length; i++) + { + ref var p = ref pt[i]; + p.EntityId = _input.ReadActorID(); + p.Room = _input.ReadByte(false); + } + return new(pt); + } + private DeepDungeonState.OpItemsChange ParseDeepDungeonItems() + { + var it = new DeepDungeonState.Item[16]; + for (var i = 0; i < it.Length; i++) + { + ref var item = ref it[i]; + item.Count = _input.ReadByte(false); + item.Flags = _input.ReadByte(true); + } + return new(it); + } + private DeepDungeonState.OpChestsChange ParseDeepDungeonChests() + { + var ct = new DeepDungeonState.Chest[16]; + for (var i = 0; i < ct.Length; i++) + { + ref var chest = ref ct[i]; + chest.Type = _input.ReadByte(false); + chest.Room = _input.ReadByte(false); + } + return new(ct); + } + + private DeepDungeonState.OpMagiciteChange ParseDeepDungeonMagicite() => new(_input.ReadBytes()); + private NetworkState.OpIDScramble ParseNetworkIDScramble() => new(_input.ReadUInt(false)); private NetworkState.OpServerIPC ParseNetworkServerIPC() => new(new((Network.ServerIPC.PacketID)_input.ReadInt(), _input.ReadUShort(false), _input.ReadUInt(false), _input.ReadUInt(true), new(_input.ReadLong()), _input.ReadBytes()));