diff --git a/Blish HUD/GameServices/ArcDps/Common/CommonFields.cs b/Blish HUD/GameServices/ArcDps/Common/CommonFields.cs index aa3279c20..ea637b3d3 100644 --- a/Blish HUD/GameServices/ArcDps/Common/CommonFields.cs +++ b/Blish HUD/GameServices/ArcDps/Common/CommonFields.cs @@ -1,9 +1,11 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; namespace Blish_HUD.ArcDps.Common { public class CommonFields { + private bool _enabled = false; /// /// Delegate which will be invoked in and @@ -15,11 +17,9 @@ public class CommonFields { /// Contains every player in the current group or squad. /// Key: Character Name, Value: Account Name /// - public IReadOnlyDictionary PlayersInSquad => _playersInSquad; - - private readonly ConcurrentDictionary _playersInSquad = new ConcurrentDictionary(); - - private bool _enabled; + public IReadOnlyDictionary PlayersInSquad => GameService.ArcDpsV2.Common.PlayersInSquad + .Select(x => new KeyValuePair(x.Key, new Player(x.Value.CharacterName, x.Value.AccountName, x.Value.Profession, x.Value.Elite, x.Value.Self))) + .ToDictionary(x=> x.Key, x => x.Value); /// /// Gets invoked whenever someone joins the squad or group. @@ -35,37 +35,12 @@ public class CommonFields { /// Activates the service. /// public void Activate() { + GameService.ArcDpsV2.Common.PlayerAdded += player => PlayerAdded?.Invoke(new Player(player.CharacterName, player.AccountName, player.Profession, player.Elite, player.Self)); + GameService.ArcDpsV2.Common.PlayerRemoved += player => PlayerRemoved?.Invoke(new Player(player.CharacterName, player.AccountName, player.Profession, player.Elite, player.Self)); + if (_enabled) return; _enabled = true; - GameService.ArcDps.RawCombatEvent += CombatHandler; - } - - private void CombatHandler(object sender, RawCombatEventArgs args) { - if (args.CombatEvent.Ev != null) return; - - /* notify tracking change */ - if (args.CombatEvent.Src.Elite != 0) return; - - /* add */ - if (args.CombatEvent.Src.Profession != 0) { - if (_playersInSquad.ContainsKey(args.CombatEvent.Src.Name)) return; - - string accountName = args.CombatEvent.Dst.Name.StartsWith(":") - ? args.CombatEvent.Dst.Name.Substring(1) - : args.CombatEvent.Dst.Name; - - var player = new Player( - args.CombatEvent.Src.Name, accountName, - args.CombatEvent.Dst.Profession, args.CombatEvent.Dst.Elite, args.CombatEvent.Dst.Self != 0 - ); - - if (_playersInSquad.TryAdd(args.CombatEvent.Src.Name, player)) this.PlayerAdded?.Invoke(player); - } - /* remove */ - else { - if (_playersInSquad.TryRemove(args.CombatEvent.Src.Name, out var player)) this.PlayerRemoved?.Invoke(player); - } } public struct Player { diff --git a/Blish HUD/GameServices/ArcDps/V2/ArcDpsBridgeVersion.cs b/Blish HUD/GameServices/ArcDps/V2/ArcDpsBridgeVersion.cs new file mode 100644 index 000000000..7a2ab0e09 --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/ArcDpsBridgeVersion.cs @@ -0,0 +1,7 @@ +namespace Blish_HUD.GameServices.ArcDps { + public enum ArcDpsBridgeVersion { + V1 = 0, + V2 = 1, + None = 100, + } +} diff --git a/Blish HUD/GameServices/ArcDps/V2/ArcDpsClient.cs b/Blish HUD/GameServices/ArcDps/V2/ArcDpsClient.cs new file mode 100644 index 000000000..782854f88 --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/ArcDpsClient.cs @@ -0,0 +1,224 @@ +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Blish_HUD.GameServices.ArcDps.V2; +using Blish_HUD.GameServices.ArcDps.V2.Processors; + +namespace Blish_HUD.GameServices.ArcDps { + + internal class ArcDpsClient : IArcDpsClient { +#if DEBUG + public static long Counter; +#endif + + private static readonly Logger _logger = Logger.GetLogger(); + private readonly BlockingCollection[] messageQueues; + private readonly Dictionary processors = new Dictionary(); + private readonly ArcDpsBridgeVersion arcDpsBridgeVersion; + private bool isConnected = false; + private NetworkStream networkStream; + private CancellationToken ct; + private bool disposedValue; + + public event EventHandler Error; + + public bool IsConnected => this.isConnected && this.Client.Connected; + + public TcpClient Client { get; } + + public event Action Disconnected; + + public ArcDpsClient(ArcDpsBridgeVersion arcDpsBridgeVersion) { + this.arcDpsBridgeVersion = arcDpsBridgeVersion; + + processors.Add(1, new ImGuiProcessor()); + + if (this.arcDpsBridgeVersion == ArcDpsBridgeVersion.V1) { + processors.Add(2, new LegacyCombatProcessor()); + processors.Add(3, new LegacyCombatProcessor()); + } else { + processors.Add(2, new CombatEventProcessor()); + processors.Add(3, new CombatEventProcessor()); + } + + // hardcoded message queue size. One Collection per message type. This is done just for optimizations + this.messageQueues = new BlockingCollection[4]; + + this.Client = new TcpClient(); + } + + public void RegisterMessageTypeListener(int type, Func listener) + where T : struct { + var processor = (MessageProcessor)this.processors[type]; + if (messageQueues[type] == null) { + messageQueues[type] = new BlockingCollection(); + + try { + Task.Run(() => this.ProcessMessage(processor, messageQueues[type])); + } catch (OperationCanceledException) { + // NOP + } + } + + processor.RegisterListener(listener); + } + + private void ProcessMessage(MessageProcessor processor, BlockingCollection messageQueue) { + while (!ct.IsCancellationRequested) { + ct.ThrowIfCancellationRequested(); + Task.Delay(1).Wait(); + foreach (var item in messageQueue.GetConsumingEnumerable()) { + ct.ThrowIfCancellationRequested(); + processor.Process(item, ct); + ArrayPool.Shared.Return(item); + } + } + + ct.ThrowIfCancellationRequested(); + } + + /// + /// Initializes the client and connects to the arcdps "server" + /// + /// + /// CancellationToken to cancel the whole client + public void Initialize(IPEndPoint endpoint, CancellationToken ct) { + this.ct = ct; + this.Client.Connect(endpoint); + _logger.Info("Connected to arcdps endpoint on: " + endpoint.ToString()); + + this.networkStream = this.Client.GetStream(); + this.isConnected = true; + + try { + if (this.arcDpsBridgeVersion == ArcDpsBridgeVersion.V1) { + Task.Run(async () => await this.LegacyReceive(ct), ct); + } else { + Task.Run(async () => await this.Receive(ct), ct); + } + } catch (OperationCanceledException) { + // NOP + } + } + + public void Disconnect() { + if (isConnected) { + if (this.Client.Connected) { + this.Client.Close(); + this.Client.Dispose(); + _logger.Info("Disconnected from arcdps endpoint"); + } + + this.isConnected = false; + this.Disconnected?.Invoke(); + } + } + + private async Task LegacyReceive(CancellationToken ct) { + _logger.Info($"Start Legacy Receive Task for {this.Client.Client.RemoteEndPoint?.ToString()}"); + try { + var messageHeaderBuffer = new byte[9]; + ArrayPool pool = ArrayPool.Shared; + while (this.Client.Connected) { + ct.ThrowIfCancellationRequested(); + + if (this.Client.Available == 0) { + await Task.Delay(1, ct); + } + + ReadFromStream(this.networkStream, messageHeaderBuffer, 9); + + // In V1 the message type is part of the message and therefor included in message length, so we subtract it here + var messageLength = Unsafe.ReadUnaligned(ref messageHeaderBuffer[0]) - 1; + var messageType = messageHeaderBuffer[8]; + + var messageBuffer = pool.Rent(messageLength); + ReadFromStream(this.networkStream, messageBuffer, messageLength); + + this.messageQueues[messageType]?.Add(messageBuffer); +#if DEBUG + Interlocked.Increment(ref Counter); +#endif + + } + } catch (Exception ex) { + _logger.Error(ex.ToString()); + this.Error?.Invoke(this, SocketError.SocketError); + this.Disconnect(); + } + + _logger.Info($"Legacy Receive Task for {this.Client.Client?.RemoteEndPoint?.ToString()} stopped"); + } + + private async Task Receive(CancellationToken ct) { + _logger.Info($"Start Receive Task for {this.Client.Client.RemoteEndPoint?.ToString()}"); + try { + var messageHeaderBuffer = new byte[5]; + ArrayPool pool = ArrayPool.Shared; + while (this.Client.Connected) { + ct.ThrowIfCancellationRequested(); + + if (this.Client.Available == 0) { + await Task.Delay(1, ct); + } + + ReadFromStream(this.networkStream, messageHeaderBuffer, 5); + + var messageLength = Unsafe.ReadUnaligned(ref messageHeaderBuffer[0]); + var messageType = messageHeaderBuffer[4]; + + var messageBuffer = pool.Rent(messageLength); + ReadFromStream(this.networkStream, messageBuffer, messageLength); + this.messageQueues[messageType]?.Add(messageBuffer); +#if DEBUG + Interlocked.Increment(ref Counter); +#endif + } + } catch (Exception ex) { + _logger.Error(ex.ToString()); + this.Error?.Invoke(this, SocketError.SocketError); + this.Disconnect(); + } + + _logger.Info($"Receive Task for {this.Client.Client?.RemoteEndPoint?.ToString()} stopped"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ReadFromStream(Stream stream, byte[] buffer, int length) { + int bytesRead = 0; + while (bytesRead != length) { + bytesRead += stream.Read(buffer, bytesRead, length - bytesRead); + } + } + + protected virtual void Dispose(bool disposing) { + if (!disposedValue) { + if (disposing) { + Client.Dispose(); + foreach (var item in messageQueues) { + if (item.Count != 0) { + foreach (var message in item) { + ArrayPool.Shared.Return(message); + } + } + } + networkStream.Dispose(); + } + + disposedValue = true; + } + } + + public void Dispose() { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Blish HUD/GameServices/ArcDps/V2/ArcDpsIdCollection.cs b/Blish HUD/GameServices/ArcDps/V2/ArcDpsIdCollection.cs new file mode 100644 index 000000000..6bef01c04 --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/ArcDpsIdCollection.cs @@ -0,0 +1,1223 @@ +using System; + +namespace Blish_HUD.GameServices.ArcDps.V2 { + /// + /// ArcDps Enums and helpful extension methods. + /// + /// + /// Source: + /// (MIT License) + /// + public static class ArcDpsIdCollection { + + public const int ArcDPSPollingRate = 300; + + internal static class GW2Builds { + internal const ulong StartOfLife = ulong.MinValue; + // + internal const ulong HoTRelease = 54485; + internal const ulong November2016NightmareRelease = 69591; + internal const ulong February2017Balance = 72781; + internal const ulong May2017Balance = 76706; + internal const ulong July2017ShatteredObservatoryRelease = 79873; + internal const ulong December2017Balance = 84832; + internal const ulong February2018Balance = 86181; + internal const ulong May2018Balance = 88541; + internal const ulong July2018Balance = 90455; + internal const ulong August2018Balance = 92069; + internal const ulong October2018Balance = 92715; + internal const ulong November2018Rune = 93543; + internal const ulong December2018Balance = 94051; + internal const ulong March2019Balance = 95535; + internal const ulong April2019Balance = 96406; + internal const ulong June2019RaidRewards = 97235; + internal const ulong July2019Balance = 97950; + internal const ulong July2019Balance2 = 98248; + internal const ulong October2019Balance = 99526; + internal const ulong December2019Balance = 100690; + internal const ulong February2020Balance = 102321; + internal const ulong February2020Balance2 = 102389; + internal const ulong July2020Balance = 104844; + internal const ulong September2020SunquaPeakRelease = 106277; + internal const ulong May2021Balance = 115190; + internal const ulong May2021BalanceHotFix = 115728; + internal const ulong June2021Balance = 116210; + internal const ulong EODBeta1 = 118697; + internal const ulong EODBeta2 = 119939; + internal const ulong EODBeta3 = 121168; + internal const ulong EODBeta4 = 122479; + internal const ulong March2022Balance = 126520; + internal const ulong March2022Balance2 = 127285; + internal const ulong May2022Balance = 128773; + internal const ulong June2022Balance = 130910; + internal const ulong June2022BalanceHotFix = 131084; + internal const ulong July2022FractalInstabilitiesRework = 131720; + internal const ulong August2022BalanceHotFix = 132359; + internal const ulong August2022Balance = 133322; + internal const ulong October2022Balance = 135242; + internal const ulong October2022BalanceHotFix = 135930; + internal const ulong November2022Balance = 137943; + internal const ulong February2023Balance = 141374; + internal const ulong May2023Balance = 145038; + internal const ulong May2023BalanceHotFix = 146069; + internal const ulong June2023Balance = 147734; + internal const ulong SOTOBetaAndSilentSurfNM = 147830; + internal const ulong July2023BalanceAndSilentSurfCM = 148697; + internal const ulong SOTOReleaseAndBalance = 150431; + internal const ulong September2023Balance = 151966; + internal const ulong DagdaNMHPChangedAndCMRelease = 153978; + // + internal const ulong EndOfLife = ulong.MaxValue; + } + + internal static class ArcDPSBuilds { + internal const int StartOfLife = int.MinValue; + // + internal const int ProperConfusionDamageSimulation = 20210529; + internal const int ScoringSystemChange = 20210800; // was somewhere around there + internal const int DirectX11Update = 20210923; + internal const int InternalSkillIDsChange = 20220304; + internal const int BuffAttrFlatIncRemoved = 20220308; + internal const int FunctionalIDToGUIDEvents = 20220709; + internal const int NewLogStart = 20221111; + internal const int FunctionalEffect2Events = 20230719; + internal const int BuffExtensionBroken = 20230905; + internal const int BuffExtensionOverstackValueChanged = 20231107; + // + internal const int EndOfLife = int.MaxValue; + } + + public static class WeaponSetIDs { + public const int NoSet = -1; + public const int FirstLandSet = 4; + public const int SecondLandSet = 5; + public const int FirstWaterSet = 0; + public const int SecondWaterSet = 1; + public const int TransformSet = 3; + public const int KitSet = 2; + } + + /// + /// Class containing reward types. + /// + internal static class RewardTypes { + internal const int OldRaidReward1 = 55821; // On each kill + internal const int OldRaidReward2 = 60685; // On each kill + internal const int CurrentRaidReward = 22797; // Once per week + internal const int PostEoDStrikeReward = 29453; + } + + /// + /// Class containing reward IDs. + /// + internal static class RewardIDs { + internal const ulong ShiverpeaksPassChests = 993; // Three chests, once a day + internal const ulong KodansOldAndCurrentChest = 1035; // Old repeatable chest, now only once a day + internal const ulong KodansCurrentChest1 = 1028; // Current, once a day + internal const ulong KodansCurrentChest2 = 1032; // Current, once a day + internal const ulong KodansCurrentRepeatableChest = 1091; // Current, repeatable + internal const ulong FraenirRepeatableChest = 1007; + internal const ulong BoneskinnerRepeatableChest = 1031; + internal const ulong WhisperRepeatableChest = 1052; + } + + // Buff cycle + public enum BuffCycle : byte { + Cycle, // damage happened on tick timer + NotCycle, // damage happened outside tick timer (resistable) + NotCycle_NoResit, // BEFORE MAY 2021: the others were lumped here, now retired + NotCycle_DamageToTargetOnHit, // damage happened to target on hiting target + NotCycle_DamageToSourceOnHit, // damage happened to source on hiting target + NotCycle_DamageToTargetOnStackRemove, // damage happened to target on source losing a stack + Unknown + }; + + internal static BuffCycle GetBuffCycle(byte bt) { + return bt < (byte)BuffCycle.Unknown ? (BuffCycle)bt : BuffCycle.Unknown; + } + + // Breakbar State + + public enum BreakbarState : byte { + Active = 0, + Recover = 1, + Immune = 2, + None = 3, + Unknown + }; + internal static BreakbarState GetBreakbarState(int value) { + return value < (int)BreakbarState.Unknown ? (BreakbarState)value : BreakbarState.Unknown; + } + + // Buff Formula + + // this enum is updated regularly to match the in game enum. The matching between the two is simply cosmetic, for clarity while comparing against an updated skill defs + public enum BuffStackType : byte { + StackingConditionalLoss = 0, // the same thing as Stacking + Queue = 1, + StackingTargetUniqueSrc = 2, // This one clearly behaves like an intensity buff (multiple stack actives, any instance can be extended), always come with a stack limit of 999. It is unclear at this time what differentiate this one from the traditional Stacking type. + Regeneration = 3, + Stacking = 4, + Force = 5, + Unknown, + }; + internal static BuffStackType GetBuffStackType(byte bt) { + return bt < (byte)BuffStackType.Unknown ? (BuffStackType)bt : BuffStackType.Unknown; + } + + public enum BuffAttribute : short { + None = 0, + Power = 1, + Precision = 2, + Toughness = 3, + Vitality = 4, + Ferocity = 5, + Healing = 6, + Condition = 7, + Concentration = 8, + Expertise = 9, + Armor = 10, + Agony = 11, + StatInc = 12, + FlatInc = 13, + PhysInc = 14, + CondInc = 15, + PhysRec = 16, + CondRec = 17, + AttackSpeed = 18, + UnusedSiphonInc_Arc = 19, // Unused due to being auto detected by the solver + SiphonRec = 20, + // + Unknown = short.MaxValue, + // + /*ConditionDurationIncrease = 24, + RetaliationDamageOutput = 25, + CriticalChance = 26, + PowerDamageToHP = 34, + ConditionDamageToHP = 35, + GlancingBlow = 47, + ConditionSkillActivationFormula = 52, + ConditionDamageFormula = 54, + ConditionMovementActivationFormula = 55, + EnduranceRegeneration = 61, + IncomingHealingEffectiveness = 65, + OutgoingHealingEffectivenessFlatInc = 68, + OutgoingHealingEffectivenessConvInc = 70, + RegenerationHealingOutput = 71, + ExperienceFromKills = 76, + GoldFind = 77, + MovementSpeed = 78, + KarmaBonus = 87, + SkillCooldown = 96, + MagicFind = 92, + ExperienceFromAll = 100, + WXP = 112,*/ + // Custom Ids, matched using a very simple pattern detection, see BuffInfoSolver.cs + ConditionDurationInc = -1, + DamageFormulaSquaredLevel = -2, + CriticalChance = -3, + StrikeDamageToHP = -4, + ConditionDamageToHP = -5, + GlancingBlow = -6, + SkillActivationDamageFormula = -7, + DamageFormula = -8, + MovementActivationDamageFormula = -9, + EnduranceRegeneration = -10, + HealingEffectivenessRec = -11, + HealingEffectivenessFlatInc = -12, + HealingEffectivenessConvInc = -13, + HealingOutputFormula = -14, + ExperienceFromKills = -15, + GoldFind = -16, + MovementSpeed = -17, + KarmaBonus = -18, + SkillRechargeSpeedIncrease = -19, + MagicFind = -20, + ExperienceFromAll = -21, + WXP = -22, + SiphonInc = -23, + PhysRec2 = -24, + CondRec2 = -25, + BoonDurationInc = -26, + HealingEffectivenessRec2 = -27, + MovementSpeedStacking = -28, + MovementSpeedStacking2 = -29, + FishingPower = -30, + MaximumHP = -31, + VitalityPercent = -32, + DefensePercent = -33, + } + internal static BuffAttribute GetBuffAttribute(short bt, int evtcVersion) { + BuffAttribute res; + if (evtcVersion >= ArcDPSBuilds.BuffAttrFlatIncRemoved) { + // Enum has shifted by -1 + if (bt <= (byte)BuffAttribute.SiphonRec - 1) { + // only apply +1 shift to enum higher or equal to the one removed + res = bt < (byte)BuffAttribute.FlatInc ? (BuffAttribute)bt : (BuffAttribute)(bt + 1); + } else { + res = BuffAttribute.Unknown; + } + } else { + res = bt <= (byte)BuffAttribute.SiphonRec ? (BuffAttribute)bt : BuffAttribute.Unknown; + } + if (res == BuffAttribute.UnusedSiphonInc_Arc) { + res = BuffAttribute.Unknown; + } + return res; + } + + // Broken + /* + public enum BuffCategory : byte + { + Boon = 0, + Any = 1, + Condition = 2, + Food = 4, + Upgrade = 6, + Boost = 8, + Trait = 11, + Enhancement = 13, + Stance = 16, + Unknown = byte.MaxValue + } + internal static BuffCategory GetBuffCategory(byte bt) + { + return Enum.IsDefined(typeof(BuffCategory), bt) ? (BuffCategory)bt : BuffCategory.Unknown; + }*/ + + // Content local + + public enum ContentLocal : byte { + Effect = 0, + Marker = 1, + Unknown + } + internal static ContentLocal GetContentLocal(byte bt) { + return bt < (byte)ContentLocal.Unknown ? (ContentLocal)bt : ContentLocal.Unknown; + } + + // Custom ids + private const int DummyTarget = -1; + private const int HandOfErosion = -2; + private const int HandOfEruption = -3; + private const int PyreGuardianProtect = -4; + private const int PyreGuardianStab = -5; + private const int PyreGuardianRetal = -6; + private const int QadimLamp = -7; + private const int AiKeeperOfThePeak2 = -8; + private const int MatthiasSacrifice = -9; + private const int BloodstoneFragment = -10; + private const int BloodstoneShardMainFight = -11; + private const int ChargedBloodstone = -12; + private const int PyreGuardianResolution = -13; + private const int CASword = -14; + private const int SubArtsariiv = -15; + private const int DummyMaiTrinStrike = -16; + private const int TheDragonVoidZhaitan = -17; + private const int TheDragonVoidSooWon = -18; + private const int TheDragonVoidKralkatorrik = -19; + private const int TheDragonVoidMordremoth = -20; + private const int TheDragonVoidJormag = -21; + private const int TheDragonVoidPrimordus = -22; + private const int PushableVoidAmalgamate = -23; + private const int DragonBodyVoidAmalgamate = -24; + private const int VentariTablet = -25; + private const int PoisonMushroom = -26; + private const int SpearAggressionRevulsion = -27; + private const int DragonOrb = -28; + private const int ChestOfSouls = -29; + private const int ShackledPrisoner = -30; + private const int DemonicBond = -31; + private const int BloodstoneShardRift = -32; + private const int BloodstoneShardButton = -33; + private const int SiegeChest = -34; + private const int Mine = -35; + private const int Environment = -36; + private const int FerrousBomb = -37; + private const int SanctuaryPrism = -38; + private const int Torch = -39; + private const int BoundIcebroodElemental = -40; + private const int CAChest = -41; + private const int ChestOfDesmina = -42; + public const int NonIdentifiedSpecies = 0; + + // + + public enum TrashID : int { + // Mordremoth + SmotheringShadow = 15640, + Canach = 15501, + Braham = 15778, + Caithe = 15565, + BlightedRytlock = 15999, + //BlightedCanach = 15999, + BlightedBraham = 15553, + BlightedMarjory = 15572, + BlightedCaithe = 15916, + BlightedForgal = 15597, + BlightedSieran = 15979, + //BlightedTybalt = 15597, + //BlightedPaleTree = 15597, + //BlightedTrahearne = 15597, + //BlightedEir = 15597, + Glenna = 15014, + // VG + Seekers = 15426, + RedGuardian = 15433, + BlueGuardian = 15431, + GreenGuardian = 15420, + // Gorse + ChargedSoul = 15434, + EnragedSpirit = 16024, + AngeredSpirit = 16005, + // Sab + Kernan = 15372, + Knuckles = 15404, + Karde = 15430, + BanditSapper = 15423, + BanditThug = 15397, + BanditArsonist = 15421, + // Slothasor + Slubling1 = 16064, + Slubling2 = 16071, + Slubling3 = 16077, + Slubling4 = 16104, + PoisonMushroom = ArcDpsIdCollection.PoisonMushroom, + // Trio + BanditSaboteur = 16117, + Warg = 7481, + VeteranTorturedWarg = 16129, + BanditAssassin = 16067, + BanditAssassin2 = 16113, + BanditSapperTrio = 16074, + BanditDeathsayer = 16076, + BanditDeathsayer2 = 16080, + BanditBrawler = 16066, + BanditBrawler2 = 16119, + BanditBattlemage = 16093, + BanditBattlemage2 = 16100, + BanditCleric = 16101, + BanditCleric2 = 16060, + BanditBombardier = 16138, + BanditSniper = 16065, + NarellaTornado = 16092, + OilSlick = 16096, + InsectSwarm = 16120, + Prisoner1 = 16056, + Prisoner2 = 16103, + // Matthias + Spirit = 16105, + Spirit2 = 16114, + IcePatch = 16139, + Storm = 16108, + Tornado = 16068, + MatthiasSacrificeCrystal = MatthiasSacrifice, + // Escort + MushroomSpikeThrower = 16219, + MushroomKing = 16255, + MushroomCharger = 16224, + WhiteMantleBattleMage1Escort = 16229, + WhiteMantleBattleMage2Escort = 16240, + WhiteMantleBattleCultist1 = 16265, + WhiteMantleBattleCultist2 = 16281, + WhiteMantleBattleKnight1 = 16242, + WhiteMantleBattleKnight2 = 16220, + WhiteMantleBattleCleric1 = 16272, + WhiteMantleBattleCleric2 = 16266, + WhiteMantleBattleSeeker1 = 16288, + WhiteMantleBattleSeeker2 = 16256, + WargBloodhound = 16222, + RadiantMcLeod = 16234, + CrimsonMcLeod = 16241, + Mine = ArcDpsIdCollection.Mine, + // KC + Olson = 16244, + Engul = 16274, + Faerla = 16264, + Caulle = 16282, + Henley = 16236, + Jessica = 16278, + Galletta = 16228, + Ianim = 16248, + KeepConstructCore = 16261, + GreenPhantasm = 16237, + InsidiousProjection = 16227, + UnstableLeyRift = 16277, + RadiantPhantasm = 16259, + CrimsonPhantasm = 16257, + RetrieverProjection = 16249, + // Twisted Castle + HauntingStatue = 16247, + //CastleFountain = 32951, + // Xera + BloodstoneShardMainFight = ArcDpsIdCollection.BloodstoneShardMainFight, + BloodstoneShardRift = ArcDpsIdCollection.BloodstoneShardRift, + BloodstoneShardButton = ArcDpsIdCollection.BloodstoneShardButton, + ChargedBloodstone = ArcDpsIdCollection.ChargedBloodstone, + BloodstoneFragment = ArcDpsIdCollection.BloodstoneFragment, + XerasPhantasm = 16225, + WhiteMantleSeeker1 = 16238, + WhiteMantleSeeker2 = 16283, + WhiteMantleKnight1 = 16251, + WhiteMantleKnight2 = 16287, + WhiteMantleBattleMage1 = 16221, + WhiteMantleBattleMage2 = 16226, + ExquisiteConjunction = 16232, + FakeXera = 16289, + // MO + Jade = 17181, + // Samarog + Guldhem = 17208, + Rigom = 17124, + SpearAggressionRevulsion = ArcDpsIdCollection.SpearAggressionRevulsion, + // Deimos + Saul = 17126, + ShackledPrisoner = ArcDpsIdCollection.ShackledPrisoner, + DemonicBond = ArcDpsIdCollection.DemonicBond, + Thief = 17206, + Gambler = 17335, + GamblerClones = 17161, + GamblerReal = 17355, + Drunkard = 17163, + Oil = 17332, + Tear = 17303, + Greed = 17213, + Pride = 17233, + Hands = 17221, + // SH + TormentedDead = 19422, + SurgingSoul = 19474, + Scythe = 19396, + FleshWurm = 19464, + // River + Enervator = 19863, + HollowedBomber = 19399, + RiverOfSouls = 19829, + SpiritHorde1 = 19461, + SpiritHorde2 = 19400, + SpiritHorde3 = 19692, + // Statues of Darkness + LightThieves = 19658, + MazeMinotaur = 19402, + // Statue of Death + OrbSpider = 19801, + GreenSpirit1 = 19587, + GreenSpirit2 = 19571, + AscalonianPeasant1 = 19810, + AscalonianPeasant2 = 19758, + // Skeletons are the same as Spirit hordes + // Dhuum + Messenger = 19807, + Echo = 19628, + Enforcer = 19681, + Deathling = 19759, + UnderworldReaper = 19831, + DhuumDesmina = 19481, + // CA + ConjuredGreatsword = 21255, + ConjuredShield = 21170, + ConjuredPlayerSword = CASword, + // Qadim + LavaElemental1 = 21236, + LavaElemental2 = 21078, + IcebornHydra = 21163, + GreaterMagmaElemental1 = 21150, + GreaterMagmaElemental2 = 21223, + FireElemental = 21221, + FireImp = 21100, + PyreGuardian = 21050, + PyreGuardianRetal = ArcDpsIdCollection.PyreGuardianRetal, + PyreGuardianResolution = ArcDpsIdCollection.PyreGuardianResolution, + PyreGuardianProtect = ArcDpsIdCollection.PyreGuardianProtect, + PyreGuardianStab = ArcDpsIdCollection.PyreGuardianStab, + ReaperOfFlesh = 21218, + DestroyerTroll = 20944, + IceElemental = 21049, + AncientInvokedHydra = 21285, + ApocalypseBringer = 21073, + WyvernMatriarch = 20997, + WyvernPatriarch = 21183, + QadimLamp = ArcDpsIdCollection.QadimLamp, + AngryZommoros = 20961, + ChillZommoros = 21118, + AssaultCube = 21092, + AwakenedSoldier = 21244, + Basilisk = 21140, + BlackMoa = 20980, + BrandedCharr = 21083, + BrandedDevourer = 21053, + ChakDrone = 21064, + CrazedKarkaHatchling = 21040, + FireImpLamp = 21173, + GhostlyPirateFighter = 21257, + GiantBrawler = 21288, + GiantHunter = 20972, + GoldOoze = 21264, + GrawlBascher = 21145, + GrawlTrapper = 21290, + GuildInitiateModusSceleris = 21161, + IcebroodAtrocity = 16504, + IcebroodKodan = 20975, + IcebroodQuaggan = 21196, + Jotun = 21054, + JungleWurm = 21147, + Karka = 21192, + MinotaurBull = 20969, + ModnirrBerserker = 20951, + MoltenDisaggregator = 21010, + MoltenProtector = 21037, + MoltenReverberant = 20956, + MordremVinetooth = 20940, + Murellow = 21032, + NightmareCourtier = 21261, + OgreHunter = 21116, + PirareSkrittSentry = 21189, + PolarBear = 20968, + Rabbit = 1085, + ReefSkelk = 21024, + RisenKraitDamoss = 21070, + RottingAncientOakheart = 21252, + RottingDestroyer = 21182, + ShadowSkelk = 20966, + SpiritOfExcess = 21095, + TamedWarg = 18184, + TarElemental = 21019, + WindRider = 21164, + // Adina + HandOfErosion = ArcDpsIdCollection.HandOfErosion, + HandOfEruption = ArcDpsIdCollection.HandOfEruption, + // Sabir + ParalyzingWisp = 21955, + VoltaicWisp = 21975, + SmallJumpyTornado = 21961, + SmallKillerTornado = 21957, + BigKillerTornado = 21987, + // Peerless Qadim + PeerlessQadimPylon = 21996, + PeerlessQadimAuraPylon = 21962, + EntropicDistortion = 21973, + EnergyOrb = 21946, + Brandstorm = 21978, + GiantQadimThePeerless = 21953, + DummyPeerlessQadim = 22005, + // Fraenir + IcebroodElemental = 22576, + BoundIcebroodElemental = ArcDpsIdCollection.BoundIcebroodElemental, + // Boneskinner + PrioryExplorer = 22561, + PrioryScholar = 22448, + VigilRecruit = 22389, + VigilTactician = 22420, + AberrantWisp = 22538, + Torch = ArcDpsIdCollection.Torch, + // Whisper of Jormag + WhisperEcho = 22628, + DoppelgangerElementalist = 22627, + DoppelgangerElementalist2 = 22691, + DoppelgangerEngineer = 22625, + DoppelgangerEngineer2 = 22699, + DoppelgangerGuardian = 22608, + DoppelgangerGuardian2 = 22635, + DoppelgangerMesmer = 22683, + DoppelgangerMesmer2 = 22721, + DoppelgangerNecromancer = 22672, + DoppelgangerNecromancer2 = 22713, + DoppelgangerRanger = 22667, + DoppelgangerRanger2 = 22678, + DoppelgangerRevenant = 22610, + DoppelgangerRevenant2 = 22615, + DoppelgangerThief = 22612, + DoppelgangerThief2 = 22656, + DoppelgangerWarrior = 22640, + DoppelgangerWarrior2 = 22717, + // Cold War + PropagandaBallon = 23093, + DominionBladestorm = 23102, + DominionStalker = 22882, + DominionSpy1 = 22833, + DominionSpy2 = 22856, + DominionAxeFiend = 22938, + DominionEffigy = 22897, + FrostLegionCrusher = 23005, + FrostLegionMusketeer = 22870, + BloodLegionBlademaster = 22993, + CharrTank = 22953, + SonsOfSvanirHighShaman = 22283, + // Aetherblade Hideout + MaiTrinStrikeDuringEcho = 23826, + ScarletPhantomNormalBeam = 24404, + ScarletPhantomBreakbar = 23656, + ScarletPhantomHP = 24431, + ScarletPhantomHPCM = 25262, + ScarletPhantomConeWaveNM = 24396, + ScarletPhantomDeathBeamCM = 25284, + ScarletPhantomDeathBeamCM2 = 25287, + FerrousBomb = ArcDpsIdCollection.FerrousBomb, + // Xunlai Jade Junkyard + Ankka = 24634, + KraitsHallucination = 24258, + LichHallucination = 24158, + QuaggansHallucinationNM = 24969, + QuaggansHallucinationCM = 25289, + ReanimatedMalice1 = 24976, + ReanimatedMalice2 = 24171, + ReanimatedSpite = 24348, + ReanimatedHatred = 23673, + ReanimatedAntipathy = 24827, + ZhaitansReach = 23839, + SanctuaryPrism = ArcDpsIdCollection.SanctuaryPrism, + // Kaineng Overlook + TheSniper = 23612, + TheSniperCM = 25259, + TheMechRider = 24660, + TheMechRiderCM = 25271, + TheEnforcer = 24261, + TheEnforcerCM = 25236, + TheRitualist = 23618, + TheRitualistCM = 25242, + TheMindblade = 24254, + TheMindbladeCM = 25280, + SpiritOfPain = 23793, + SpiritOfDestruction = 23961, + // Void Amalgamate + PushableVoidAmalgamate = ArcDpsIdCollection.PushableVoidAmalgamate, + VoidAmalgamate = 24375, + KillableVoidAmalgamate = 23956, + DragonBodyVoidAmalgamate = ArcDpsIdCollection.DragonBodyVoidAmalgamate, + VoidTangler = 25138, + VoidColdsteel = 23945, + VoidAbomination = 23936, + VoidSaltsprayDragon = 23846, + VoidObliterator = 23995, + VoidRotswarmer = 24590, + VoidGiant = 24450, + VoidSkullpiercer = 25177, + VoidTimeCaster = 25025, + VoidBrandbomber = 24783, + VoidBurster = 24464, + VoidWarforged1 = 24129, + VoidWarforged2 = 24855, + VoidStormseer = 24677, + VoidMelter = 24223, + VoidGoliath = 24761, + DragonEnergyOrb = DragonOrb, + // Cosmic Observatory + TheTormented = 26016, + VeteranTheTormented = 25829, + EliteTheTormented = 26000, + ChampionTheTormented = 25623, + TormentedPhantom = 25604, + SoulFeast = 26069, + Zojja = 26011, + // Temple of Febe + EmbodimentOfGluttony = 25677, + EmbodimentOfRage = 25686, + EmbodimentOfDespair = 26034, + EmbodimentOfRegret = 26049, + EmbodimentOfEnvy = 25967, + EmbodimentOfMalice = 25700, + MaliciousShadow = 25747, + // Freezie + FreeziesFrozenHeart = 21328, + IceStormer = 21325, + IceSpiker = 21337, + IcyProtector = 21326, + // Fractals + FractalVindicator = 19684, + FractalAvenger = 15960, + JadeMawTentacle = 16721, + InspectorEllenKiel = 21566, + ChampionRabbit = 11329, + AwakenedAbomination = 21634, + TheMossman = 11277, + // MAMA + Arkk = 16902, + GreenKnight = 16906, + RedKnight = 16974, + BlueKnight = 16899, + TwistedHorror = 17009, + // Siax + VolatileHallucinationSiax = 17002, + EchoOfTheUnclean = 17068, + NightmareHallucinationSiax = 16911, + // Ensolyss + NightmareHallucination1 = 16912, // (exploding after jump and charging in last phase) + NightmareHallucination2 = 17033, // (small adds, last phase) + NightmareAltar = 35791, + // Skorvald + FluxAnomaly1 = 17578, + FluxAnomaly2 = 17929, + FluxAnomaly3 = 17695, + FluxAnomaly4 = 17651, + FluxAnomalyCM1 = 17599, + FluxAnomalyCM2 = 17770, + FluxAnomalyCM3 = 17851, + FluxAnomalyCM4 = 17673, + SolarBloom = 17732, + // Artsariiv + TemporalAnomalyArtsariiv = 17870, + Spark = 17630, + SmallArtsariiv = 17811, // tiny adds + MediumArtsariiv = 17694, // small adds + BigArtsariiv = 17937, // big adds + CloneArtsariiv = SubArtsariiv, // clone adds + // Arkk + TemporalAnomalyArkk = 17720, + Archdiviner = 17893, + FanaticDagger1 = 11281, + FanaticDagger2 = 11282, + FanaticBow = 11288, + EliteBrazenGladiator = 17730, + BLIGHT = 16437, + PLINK = 16325, + DOC = 16657, + CHOP = 16552, + ProjectionArkk = 17613, + // Ai + EnragedWaterSprite = 23270, + TransitionSorrowDemon1 = 23265, + TransitionSorrowDemon2 = 23242, + TransitionSorrowDemon3 = 23279, + TransitionSorrowDemon4 = 23245, + CCSorrowDemon = 23256, + AiDoubtDemon = 23268, + PlayerDoubtDemon = 23246, + FearDemon = 23264, + GuiltDemon = 23252, + // Kanaxai + AspectOfTorment = 25556, + AspectOfLethargy = 25561, + AspectOfExposure = 25562, + AspectOfDeath = 25580, + AspectOfFear = 25563, + LuxonMonkSpirit = 25571, + CaptainThess1 = 25554, + CaptainThess2 = 25557, + // Open world Soo-Won + SooWonTail = 51756, + VoidGiant2 = 24310, + VoidTimeCaster2 = 24586, + VoidBrandstalker = 24951, + VoidColdsteel2 = 23791, + VoidObliterator2 = 24947, + VoidAbomination2 = 23886, + VoidBomber = 24714, + VoidBrandbeast = 23917, + VoidBrandcharger1 = 24936, + VoidBrandcharger2 = 24039, + VoidBrandfang1 = 24912, + VoidBrandfang2 = 24772, + VoidBrandscale1 = 24053, + VoidBrandscale2 = 24426, + VoidColdsteel3 = 24063, + VoidCorpseknitter1 = 24756, + VoidCorpseknitter2 = 24607, + VoidDespoiler1 = 23874, + VoidDespoiler2 = 25179, + VoidFiend1 = 23707, + VoidFiend2 = 24737, + VoidFoulmaw = 24766, + VoidFrostwing = 24780, + VoidGlacier1 = 23753, + VoidGlacier2 = 24235, + VoidInfested1 = 24390, + VoidInfested2 = 24997, + VoidMelter1 = 24497, + VoidMelter2 = 24807, + VoidRimewolf1 = 24698, + VoidRimewolf2 = 23798, + VoidRotspinner1 = 25057, + VoidStorm = 24007, + VoidStormseer2 = 24419, + VoidStormseer3 = 23962, + VoidTangler2 = 23567, + VoidThornheart1 = 24406, + VoidThornheart2 = 23688, + VoidWorm = 23701, + // + Environment = ArcDpsIdCollection.Environment, + // + Unknown = int.MaxValue, + }; + public static TrashID GetTrashID(int id) { + return Enum.IsDefined(typeof(TrashID), id) ? (TrashID)id : TrashID.Unknown; + } + + public enum TargetID : int { + WorldVersusWorld = 1, + Instance = 2, + DummyTarget = ArcDpsIdCollection.DummyTarget, + Mordremoth = 15884, + // Raid + ValeGuardian = 15438, + Gorseval = 15429, + Sabetha = 15375, + Slothasor = 16123, + Berg = 16088, + Zane = 16137, + Narella = 16125, + Matthias = 16115, + McLeodTheSilent = 16253, + KeepConstruct = 16235, + Xera = 16246, + Xera2 = 16286, + Cairn = 17194, + MursaatOverseer = 17172, + Samarog = 17188, + Deimos = 17154, + SoullessHorror = 19767, + Desmina = 19828, + BrokenKing = 19691, + EaterOfSouls = 19536, + EyeOfJudgement = 19651, + EyeOfFate = 19844, + Dhuum = 19450, + ConjuredAmalgamate = 43974, // Gadget + CARightArm = 10142, // Gadget + CALeftArm = 37464, // Gadget + ConjuredAmalgamate_CHINA = 44885, // Gadget + CARightArm_CHINA = 11053, // Gadget + CALeftArm_CHINA = 38375, // Gadget + Nikare = 21105, + Kenut = 21089, + Qadim = 20934, + Freezie = 21333, + Adina = 22006, + Sabir = 21964, + PeerlessQadim = 22000, + // Strike Missions + IcebroodConstruct = 22154, + VoiceOfTheFallen = 22343, + ClawOfTheFallen = 22481, + VoiceAndClaw = 22315, + FraenirOfJormag = 22492, + IcebroodConstructFraenir = 22436, + Boneskinner = 22521, + WhisperOfJormag = 22711, + VariniaStormsounder = 22836, + MaiTrinStrike = 24033, + DummyMaiTrinStrike = ArcDpsIdCollection.DummyMaiTrinStrike, + EchoOfScarletBriarNM = 24768, + EchoOfScarletBriarCM = 25247, + Ankka = 23957, + MinisterLi = 24485, + MinisterLiCM = 24266, + GadgetTheDragonVoid1 = 43488, + GadgetTheDragonVoid2 = 1378, + VoidAmalgamate1 = 24375, + TheDragonVoidZhaitan = ArcDpsIdCollection.TheDragonVoidZhaitan, + TheDragonVoidJormag = ArcDpsIdCollection.TheDragonVoidJormag, + TheDragonVoidKralkatorrik = ArcDpsIdCollection.TheDragonVoidKralkatorrik, + TheDragonVoidSooWon = ArcDpsIdCollection.TheDragonVoidSooWon, + TheDragonVoidPrimordus = ArcDpsIdCollection.TheDragonVoidPrimordus, + TheDragonVoidMordremoth = ArcDpsIdCollection.TheDragonVoidMordremoth, + PrototypeVermilion = 25413, + PrototypeArsenite = 25415, + PrototypeIndigo = 25419, + PrototypeVermilionCM = 25414, + PrototypeArseniteCM = 25416, + PrototypeIndigoCM = 25423, + Dagda = 25705, + Cerus = 25989, + //VoidAmalgamate = + // Fract + MAMA = 17021, + Siax = 17028, + Ensolyss = 16948, + Skorvald = 17632, + Artsariiv = 17949, + Arkk = 17759, + MaiTrinFract = 19697, + ShadowMinotaur = 20682, + BroodQueen = 20742, + TheVoice = 20497, + AiKeeperOfThePeak = 23254, + AiKeeperOfThePeak2 = ArcDpsIdCollection.AiKeeperOfThePeak2, + KanaxaiScytheOfHouseAurkusNM = 25572, + KanaxaiScytheOfHouseAurkusCM = 25577, + // Golems + MassiveGolem10M = 16169, + MassiveGolem4M = 16202, + MassiveGolem1M = 16178, + VitalGolem = 16198, + AvgGolem = 16177, + StdGolem = 16199, + LGolem = 19676, + MedGolem = 19645, + ConditionGolem = 16174, + PowerGolem = 16176, + // Open world + SooWonOW = 35552, + // + Unknown = int.MaxValue, + }; + public static TargetID GetTargetID(int id) { + return Enum.IsDefined(typeof(TargetID), id) ? (TargetID)id : TargetID.Unknown; + } + + public enum ChestID : int { + ChestOfDesmina = ArcDpsIdCollection.ChestOfDesmina, + ChestOfSouls = ArcDpsIdCollection.ChestOfSouls, + SiegeChest = ArcDpsIdCollection.SiegeChest, + CAChest = ArcDpsIdCollection.CAChest, + // + None = int.MaxValue, + }; + public static ChestID GetChestID(int id) { + return Enum.IsDefined(typeof(ChestID), id) ? (ChestID)id : ChestID.None; + } + + public enum MinionID : int { + // Racial Summons + HoundOfBalthazar = 6394, + SnowWurm = 6445, + DruidSpirit = 6475, + SylvanHound = 6476, + IronLegionSoldier = 6509, + IronLegionMarksman = 6510, + BloodLegionSoldier = 10106, + BloodLegionMarksman = 10107, + AshLegionSoldier = 10108, + AshLegionMarksman = 10109, + STAD007 = 10145, + STA7012 = 10146, + // GW2 Digital Deluxe + MistfireWolf = 9801, + // Rune Summons + RuneJaggedHorror = 21314, + RuneRockDog = 8836, + RuneMarkIGolem = 8837, + RuneTropicalBird = 8838, + // Consumables with summons + Ember = 1454, + HawkeyeGriffon = 5614, + SousChef = 10076, + SunspearParagonSupport = 19643, + RavenSpiritShadow = 22309, + // Mesmer Phantasmas + IllusionarySwordsman = 6487, + IllusionaryBerserker = 6535, + IllusionaryDisenchanter = 6621, + IllusionaryRogue = 9444, + IllusionaryDefender = 9445, + IllusionaryMage = 5750, + IllusionaryDuelist = 5758, + IllusionaryWarlock = 6449, + IllusionaryWarden = 7981, + IllusionaryMariner = 9052, + IllusionaryWhaler = 9057, + IllusionaryAvenger = 15188, + // Mesmer Clones + // - Single Weapon + CloneSword = 8108, + CloneScepter = 8109, + CloneAxe = 18894, + CloneGreatsword = 8110, + CloneStaff = 8111, + CloneTrident = 9058, + CloneSpear = 6479, + CloneDownstate = 10542, + CloneDagger = 25569, + CloneUnknown = 8107, // Possibly -> https://wiki.guildwars2.com/wiki/Clone_(Snowball_Mayhem) + // - Sword + Offhand + CloneSwordTorch = 15090, + CloneSwordFocus = 15114, + CloneSwordSword = 15233, + CloneSwordShield = 15199, + CloneSwordPistol = 15181, + // - Sword 3 + Offhand + CloneIllusionaryLeap = 8106, + CloneIllusionaryLeapFocus = 15084, + CloneIllusionaryLeapShield = 15131, + CloneIllusionaryLeapSword = 15117, + CloneIllusionaryLeapPistol = 15003, + CloneIllusionaryLeapTorch = 15032, + // - Scepter + Offhand + CloneScepterTorch = 15044, + CloneScepterShield = 15156, + CloneScepterPistol = 15196, + CloneScepterFocus = 15240, + CloneScepterSword = 15249, + // - Axe + Offhand + CloneAxeTorch = 18922, + CloneAxePistol = 18939, + CloneAxeSword = 19134, + CloneAxeFocus = 19257, + CloneAxeShield = 25576, + // - Dagger + Offhand + CloneDaggerShield = 25570, + CloneDaggerPistol = 25573, + CloneDaggerFocus = 25575, + CloneDaggerTorch = 25578, + CloneDaggerSword = 25582, + // Necromancer Minions + BloodFiend = 1104, + BoneFiend = 1458, + FleshGolem = 1792, + ShadowFiend = 5673, + FleshWurm = 6002, + BoneMinion = 1192, + UnstableHorror = 18802, + ShamblingHorror = 15314, + // Ranger Spirits + StoneSpirit = 6370, + SunSpirit = 6330, + FrostSpirit = 6369, + StormSpirit = 6371, + WaterSpirit = 12778, + SpiritOfNatureRenewal = 6649, + // Ranger Pets + JuvenileJungleStalker = 3827, + JuvenileKrytanDrakehound = 4425, + JuvenileBrownBear = 4426, + JuvenileCarrionDevourer = 5581, + JuvenileSalamanderDrake = 5582, + JuvenileAlpineWolf = 6043, + JuvenileSnowLeopard = 6044, + JuvenileRaven = 6045, + JuvenileJaguar = 6849, + JuvenileMarshDrake = 6850, + JuvenileBlueMoa = 6883, + JuvenilePinkMoa = 6884, + JuvenileRedMoa = 6885, + JuvenileWhiteMoa = 6886, + JuvenileBlackMoa = 6887, + JuvenileRiverDrake = 6888, + JuvenileIceDrake = 6889, + JuvenileMurellow = 6898, + JuvenileShark = 6968, + JuvenileFernHound = 7336, + JuvenilePolarBear = 7926, + JuvenileBlackBear = 7927, + JuvenileArctodus = 7928, + JuvenileLynx = 7932, + JuvenileWhiptailDevourer = 7948, + JuvenileLashtailDevourer = 7949, + JuvenileWolf = 7975, + JuvenileHyena = 7976, + JuvenileOwl = 8002, + JuvenileEagle = 8003, + JuvenileWhiteRaven = 8004, + JuvenileCaveSpider = 8005, + JuvenileJungleSpider = 8006, + JuvenileForestSpider = 8007, + JuvenileBlackWidowSpider = 8008, + JuvenileBoar = 8013, + JuvenileWarthog = 8014, + JuvenileSiamoth = 8015, + JuvenilePig = 8016, + JuvenileArmorFish = 8035, + JuvenileBlueJellyfish = 8041, + JuvenileRedJellyfish = 8042, + JuvenileRainbowJellyfish = 9458, + JuvenileHawk = 10022, + JuvenileReefDrake = 11491, + JuvenileTiger = 15380, + JuvenileFireWywern = 15399, + JuvenileSmokescale = 15402, + JuvenileBristleback = 15418, + JuvenileEletricWywern = 15436, + JuvenileJacaranda = 18119, + JuvenileFangedIboga = 18688, + JuvenileCheetah = 19005, + JuvenileRockGazelle = 19104, + JuvenileSandLion = 19166, + JuvenileWallow = 24203, + JuvenileWhiteTiger = 24298, + JuvenileSiegeTurtle = 24796, + JuvenilePhoenix = 25131, + JuvenileAetherHunter = 25652, + // Guardian Weapon Summons + BowOfTruth = 6383, + HammerOfWisdom = 5791, + ShieldOfTheAvenger = 6382, + SwordOfJustice = 6381, + // Thief + Thief1 = 7580, + Thief2 = 7581, + Thief3 = 10090, + Thief4 = 10091, + Thief5 = 10092, + Thief6 = 10093, + Thief7 = 10094, + Thief8 = 10095, + Thief9 = 10098, + Thief10 = 10099, + Thief11 = 10102, + Thief12 = 10103, + Thief13 = 18049, + Thief14 = 18419, + Thief15 = 18492, + Thief16 = 18853, + Thief17 = 18871, + Thief18 = 18947, + Thief19 = 19069, + Thief20 = 19087, + Thief21 = 19244, + Thief22 = 19258, + Daredevil1 = 17970, + Daredevil2 = 18161, + Daredevil3 = 18369, + Daredevil4 = 18420, + Daredevil5 = 18502, + Daredevil6 = 18600, + Daredevil7 = 18723, + Daredevil8 = 18742, + Daredevil9 = 19197, + Daredevil10 = 19242, + Deadeye1 = 18023, + Deadeye2 = 18053, + Deadeye3 = 18224, + Deadeye4 = 18249, + Deadeye5 = 18264, + Deadeye6 = 18565, + Deadeye7 = 18710, + Deadeye8 = 18812, + Deadeye9 = 18870, + Deadeye10 = 18902, + Specter1 = 25210, + Specter2 = 25211, + Specter3 = 25212, + Specter4 = 25220, + Specter5 = 25221, + Specter6 = 25223, + Specter7 = 25227, + Specter8 = 25231, + Specter9 = 25232, + Specter10 = 25234, + // Elementalist Summons + LesserAirElemental = 8711, + LesserEarthElemental = 8712, + LesserFireElemental = 8713, + LesserIceElemental = 8714, + AirElemental = 6522, + EarthElemental = 6523, + FireElemental = 6524, + IceElemental = 6525, + // Scrapper Gyros + SneakGyro = 15012, + ShredderGyro = 15046, + BulwarkGyro = 15134, + PurgeGyro = 15135, + MedicGyro = 15208, + BlastGyro = 15330, + FunctionGyro = 15336, + // Revenant Summons + ViskIcerazor = 18524, + KusDarkrazor = 18594, + JasRazorclaw = 18791, + EraBreakrazor = 18806, + OfelaSoulcleave = 19002, + VentariTablet = ArcDpsIdCollection.VentariTablet, + // Mechanist + JadeMech = 23549, + // + Unknown, + } + + public static MinionID GetMinionID(int id) { + return Enum.IsDefined(typeof(MinionID), id) ? (MinionID)id : MinionID.Unknown; + } + } +} diff --git a/Blish HUD/GameServices/ArcDps/V2/CombatEventProcessor.cs b/Blish HUD/GameServices/ArcDps/V2/CombatEventProcessor.cs new file mode 100644 index 000000000..47a6e4b0f --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/CombatEventProcessor.cs @@ -0,0 +1,24 @@ +using Blish_HUD.GameServices.ArcDps.V2.Extensions; +using Blish_HUD.GameServices.ArcDps.V2.Models; +using Blish_HUD.GameServices.ArcDps.V2.Processors; +using SharpDX; +using System; +using System.IO; + +namespace Blish_HUD.GameServices.ArcDps.V2 { + internal class CombatEventProcessor : MessageProcessor { + internal override bool TryInternalProcess(byte[] message, out CombatCallback result) { + try { + using var memoryStream = new MemoryStream(message); + using var binaryReader = new BincodeBinaryReader(memoryStream); + result = binaryReader.ParseCombatCallback(); + return true; + + } catch (Exception) { + result = default; + return false; + } + + } + } +} diff --git a/Blish HUD/GameServices/ArcDps/V2/CommonFields.cs b/Blish HUD/GameServices/ArcDps/V2/CommonFields.cs new file mode 100644 index 000000000..dc793da18 --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/CommonFields.cs @@ -0,0 +1,113 @@ +using Blish_HUD.GameServices.ArcDps.V2.Models; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Blish_HUD.GameServices.ArcDps.V2 { + + public class CommonFields { + + /// + /// Delegate which will be invoked in and + /// + /// + public delegate void PresentPlayersChange(Player player); + + /// + /// Contains every player in the current group or squad. + /// Key: Character Name, Value: Account Name + /// + public IReadOnlyDictionary PlayersInSquad => _playersInSquad; + + private readonly ConcurrentDictionary _playersInSquad = new ConcurrentDictionary(); + + private bool _enabled; + + /// + /// Gets invoked whenever someone joins the squad or group. + /// + public event PresentPlayersChange PlayerAdded; + + /// + /// Gets invoked whenever someone leaves the squad or group. + /// + public event PresentPlayersChange PlayerRemoved; + + /// + /// Activates the service. + /// + public void Activate() { + if (_enabled) return; + + _enabled = true; + GameService.ArcDpsV2.RegisterMessageType(0, CombatHandler); + } + + private Task CombatHandler(CombatCallback combatEvent, CancellationToken ct) { + /* notify tracking change */ + if (combatEvent.Source.Elite != 0) return Task.CompletedTask; + + /* add */ + if (combatEvent.Source.Profession != 0) { + if (_playersInSquad.ContainsKey(combatEvent.Source.Name)) return Task.CompletedTask; + + string accountName = combatEvent.Destination.Name.StartsWith(":") + ? combatEvent.Destination.Name.Substring(1) + : combatEvent.Destination.Name; + + var player = new Player( + combatEvent.Source.Name, accountName, + combatEvent.Destination.Profession, combatEvent.Destination.Elite, combatEvent.Destination.Self != 0 + ); + + if (_playersInSquad.TryAdd(combatEvent.Source.Name, player)) this.PlayerAdded?.Invoke(player); + } + /* remove */ + else { + if (_playersInSquad.TryRemove(combatEvent.Source.Name, out var player)) this.PlayerRemoved?.Invoke(player); + } + + return Task.CompletedTask; + } + + public struct Player { + + public Player(string characterName, string accountName, uint profession, uint elite, bool self) { + this.CharacterName = characterName; + this.AccountName = accountName; + this.Profession = profession; + this.Elite = elite; + this.Self = self; + } + + /// + /// The current character name. + /// + public string CharacterName { get; } + + /// + /// The account name. + /// + public string AccountName { get; } + + /// + /// The core profession. + /// + public uint Profession { get; } + + /// + /// The elite if any is used. + /// + public uint Elite { get; } + + /// + /// if this player agent belongs to the account currently logged in on the local Guild Wars 2 instance. Otherwise . + /// + public bool Self { get; } + + } + + } + +} \ No newline at end of file diff --git a/Blish HUD/GameServices/ArcDps/V2/Extensions/BincodeBinaryReaderExtensions.cs b/Blish HUD/GameServices/ArcDps/V2/Extensions/BincodeBinaryReaderExtensions.cs new file mode 100644 index 000000000..1ee91fd48 --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Extensions/BincodeBinaryReaderExtensions.cs @@ -0,0 +1,99 @@ +using Blish_HUD.GameServices.ArcDps.Models.UnofficialExtras; +using Blish_HUD.GameServices.ArcDps.V2.Models; +using Blish_HUD.GameServices.ArcDps.V2.Processors; + +namespace Blish_HUD.GameServices.ArcDps.V2.Extensions { + public static class BincodeBinaryReaderExtensions { + public static CombatCallback ParseCombatCallback(this BincodeBinaryReader reader) { + var result = default(CombatCallback); + result.Event = reader.ParseCombatEvent(); + result.Source = reader.ParseAgent(); + result.Destination = reader.ParseAgent(); + result.SkillName = reader.Convert.ParseString(); + result.Id = reader.Convert.ParseULong(); + result.Revision = reader.Convert.ParseULong(); + + return result; + } + + public static CombatEvent ParseCombatEvent(this BincodeBinaryReader reader) { + var result = default(CombatEvent); + result.Time = reader.Convert.ParseULong(); + result.SourceAgent = reader.Convert.ParseUSize(); + result.DestinationAgent = reader.Convert.ParseUSize(); + result.Value = reader.Convert.ParseInt(); + result.BuffDamage = reader.Convert.ParseInt(); + result.OverstackValue = reader.Convert.ParseUInt(); + result.SkillId = reader.Convert.ParseUInt(); + result.SourceInstanceId = reader.Convert.ParseUShort(); + result.DestinationInstanceId = reader.Convert.ParseUShort(); + result.SourceMasterInstanceId = reader.Convert.ParseUShort(); + result.DestinationMasterInstanceId = reader.Convert.ParseUShort(); + result.Iff = ParseEnum(reader.Convert.ParseByte(), (int)Affinity.Unknown, Affinity.Unknown); + result.Buff = reader.Convert.ParseBool(); + result.Result = reader.Convert.ParseByte(); + result.IsActivation = ParseEnum(reader.Convert.ParseByte(), (int)Activation.Unknown, Activation.Unknown); + result.IsBuffRemoved = ParseEnum(reader.Convert.ParseByte(), (int)BuffRemove.Unknown, BuffRemove.Unknown); + result.IsNinety = reader.Convert.ParseBool(); + result.IsFifty = reader.Convert.ParseBool(); + result.IsMoving = reader.Convert.ParseBool(); + result.IsStateChanged = ParseEnum(reader.Convert.ParseByte(), (int)StateChange.Unknown, StateChange.Unknown); + result.IsFlanking = reader.Convert.ParseBool(); + result.IsShiels = reader.Convert.ParseBool(); + result.IsOffCycle = reader.Convert.ParseBool(); + result.Pad61 = reader.Convert.ParseByte(); + result.Pad62 = reader.Convert.ParseByte(); + result.Pad63 = reader.Convert.ParseByte(); + result.Pad64 = reader.Convert.ParseByte(); + return result; + } + + public static Agent ParseAgent(this BincodeBinaryReader reader) { + var result = default(Agent); + result.Name = reader.Convert.ParseString(); + result.Id = reader.Convert.ParseUSize(); + result.Profession = reader.Convert.ParseUInt(); + result.Elite = reader.Convert.ParseUInt(); + result.Self = reader.Convert.ParseUInt(); + result.Team = reader.Convert.ParseUShort(); + return result; + } + + public static UserInfo ParseUserInfo(this BincodeBinaryReader reader) { + var result = default(UserInfo); + result.AccountName = reader.Convert.ParseString(); + result.JoinTime = reader.Convert.ParseULong(); + result.Role = ParseEnum((byte)reader.Convert.ParseUInt(), (int)UserRole.None, UserRole.None); + result.Subgroup = reader.Convert.ParseByte(); + result.ReadyStatus = reader.Convert.ParseBool(); + result._unused1 = reader.Convert.ParseByte(); + result._unused2 = reader.Convert.ParseUInt(); + return result; + } + + public static ChatMessageInfo ParseChatMessageInfo(BincodeBinaryReader reader) { + var result = default(ChatMessageInfo); + result.ChannelId = reader.Convert.ParseUInt(); + result.ChannelType = ParseEnum((byte)reader.Convert.ParseUInt(), (int)ChannelType.Invalid, ChannelType.Invalid); + result.Subgroup = reader.Convert.ParseByte(); + result.IsBroadcast = reader.Convert.ParseBool(); + result._unused1 = reader.Convert.ParseByte(); + result.TimeStamp = reader.Convert.ParseString(); + result.AccountName = reader.Convert.ParseString(); + result.CharacterName = reader.Convert.ParseString(); + result.Text = reader.Convert.ParseString(); + return result; + } + + // This is used to not make an expensive reflection typeof/GetType and wasting precious time + private static T ParseEnum(byte enumByteValue, int maxValue, T unknown) + where T : System.Enum { + if (enumByteValue > maxValue) { + return unknown; + } + + return (T)(object)enumByteValue; + } + } + +} diff --git a/Blish HUD/GameServices/ArcDps/V2/IArcDpsClient.cs b/Blish HUD/GameServices/ArcDps/V2/IArcDpsClient.cs new file mode 100644 index 000000000..0198504ce --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/IArcDpsClient.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Blish_HUD.GameServices.ArcDps.V2 { + internal interface IArcDpsClient : IDisposable { + TcpClient Client { get; } + + event EventHandler Error; + + void Disconnect(); + + void Initialize(IPEndPoint endpoint, CancellationToken ct); + + void RegisterMessageTypeListener(int type, Func listener) where T : struct; + } +} diff --git a/Blish HUD/GameServices/ArcDps/V2/Models/Activation.cs b/Blish HUD/GameServices/ArcDps/V2/Models/Activation.cs new file mode 100644 index 000000000..3fa7ee28f --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Models/Activation.cs @@ -0,0 +1,11 @@ +namespace Blish_HUD.GameServices.ArcDps.V2.Models { + public enum Activation : byte { + None = 0, + Normal = 1, + Quickness = 2, + CancelFire = 3, + CancelCancel = 4, + Reset = 5, + Unknown = 6, + } +} \ No newline at end of file diff --git a/Blish HUD/GameServices/ArcDps/V2/Models/Affinity.cs b/Blish HUD/GameServices/ArcDps/V2/Models/Affinity.cs new file mode 100644 index 000000000..308abc6ee --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Models/Affinity.cs @@ -0,0 +1,7 @@ +namespace Blish_HUD.GameServices.ArcDps.V2.Models { + public enum Affinity : byte { + Friend = 0, + Foe = 1, + Unknown = 2, + } +} \ No newline at end of file diff --git a/Blish HUD/GameServices/ArcDps/V2/Models/Agent.cs b/Blish HUD/GameServices/ArcDps/V2/Models/Agent.cs new file mode 100644 index 000000000..01effcca8 --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Models/Agent.cs @@ -0,0 +1,49 @@ +namespace Blish_HUD.GameServices.ArcDps.V2.Models { + /// + /// An Agent. Could be anything that has behaviour in-game, for example a player or an NPC + /// + public struct Agent { + + /// + /// The name of the agent. + /// + /// + /// Can be null. + /// + public string Name { get; set; } + + /// + /// Agent unique identifier. + /// + /// + /// Not unique between sessions. + /// + public ulong Id { get; set; } + + /// + /// Profession id at time of event. + /// + /// + /// Meaning differs per event type. Eg. "Species ID" for non-gadgets. See evtc notes for details. + /// + public uint Profession { get; set; } + + /// + /// Elite specialization id at time of event. + /// + /// + /// Meaning differs per event type. See evtc notes for details. + /// + public uint Elite { get; set; } + + /// + /// One if this agent belongs to the account currently logged in on the local Guild Wars 2 instance. Zero otherwise. + /// + public uint Self { get; set; } + + /// + /// Team unique identifier. + /// + public ushort Team { get; set; } + } +} \ No newline at end of file diff --git a/Blish HUD/GameServices/ArcDps/V2/Models/BuffRemove.cs b/Blish HUD/GameServices/ArcDps/V2/Models/BuffRemove.cs new file mode 100644 index 000000000..2940e9577 --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Models/BuffRemove.cs @@ -0,0 +1,9 @@ +namespace Blish_HUD.GameServices.ArcDps.V2.Models { + public enum BuffRemove : byte { + None = 0, + All = 1, + Single = 2, + Manual = 3, + Unknown = 4, + } +} \ No newline at end of file diff --git a/Blish HUD/GameServices/ArcDps/V2/Models/CombatCallback.cs b/Blish HUD/GameServices/ArcDps/V2/Models/CombatCallback.cs new file mode 100644 index 000000000..e0c0809f4 --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Models/CombatCallback.cs @@ -0,0 +1,39 @@ +namespace Blish_HUD.GameServices.ArcDps.V2.Models { + /// + /// A combat event, like arcdps exposes it to its plugins + /// + public struct CombatCallback { + /// + /// The event data. + /// + public CombatEvent Event { get; set; } + + /// + /// The agent or entity that caused this event. + /// + public Agent Source { get; set; } + + /// + /// The agent or entity that this event is happening to. + /// + public Agent Destination { get; set; } + + /// + /// The relevant skill name. + /// + public string SkillName { get; set; } + + /// + /// Unique identifier of this event. + /// + public ulong Id { get; set; } + + /// + /// Format of the data structure. Static unless ArcDps changes the format. + /// + /// + /// Used to distinguish different versions of the format. + /// + public ulong Revision { get; set; } + } +} \ No newline at end of file diff --git a/Blish HUD/GameServices/ArcDps/V2/Models/CombatEvent.cs b/Blish HUD/GameServices/ArcDps/V2/Models/CombatEvent.cs new file mode 100644 index 000000000..2590fcc8e --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Models/CombatEvent.cs @@ -0,0 +1,179 @@ +namespace Blish_HUD.GameServices.ArcDps.V2.Models { + /// + /// Infos and data about the combat event. + /// + /// + /// For more information see the arcdps plugin documentation. + /// + public struct CombatEvent { + + /// + /// Time when the event was registered. + /// + /// + /// System specific time since boot, not the actual time in a known format. + /// + public ulong Time { get; set; } + + /// + /// Map instance agent id that caused the event. + /// + /// + /// Aka. entity id in-game. + /// + public ulong SourceAgent { get; set; } + + /// + /// Map instance agent id that this event happened to. + /// + /// + /// Aka. entity id in-game. + /// + public ulong DestinationAgent { get; set; } + + /// + /// An event-specific value. + /// + /// + /// Meaning differs per event-type. Eg. estimated physical hit damage. See evtc notes for details. + /// + public int Value { get; set; } + + /// + /// Estimated buff damage. Zero on application event. + /// + public int BuffDamage { get; set; } + + /// + /// Estimated overwritten stack duration for buff application. + /// + public uint OverstackValue { get; set; } + + /// + /// Skill id of relevant skill. + /// + /// + /// Can be zero. + /// + public uint SkillId { get; set; } + + /// + /// Map instance agent id as it appears in-game at time of event. + /// + public ushort SourceInstanceId { get; set; } + + /// + /// Map instance agent id as it appears in-game at time of event. + /// + public ushort DestinationInstanceId { get; set; } + + /// + /// If SourceAgent has a master (eg. minion, pet), this field will be equal to the agent id of the master. Otherwise zero. + /// + public ushort SourceMasterInstanceId { get; set; } + + /// + /// If DstAgent has a master (eg. minion, pet), this field will be equal to the agent id of the master. Otherwise zero. + /// + public ushort DestinationMasterInstanceId { get; set; } + + /// + /// Current affinity of SourceAgent and DestinationAgent. + /// + /// + /// Friend = 0, foe = 1, unknown = 2. + /// + public Affinity Iff { get; set; } + + /// + /// if buff was applied, removed or damaging. Otherwise . + /// + public bool Buff { get; set; } + + /// + /// or . + /// + /// + /// See evtc notes for details. + /// + public byte Result { get; set; } + + /// + /// The event is bound to the usage or cancellation of a skill. . + /// + /// + /// The type of /> + /// + public Activation IsActivation { get; set; } + + /// + /// . + /// + /// + /// For strips and cleanses: SourceAgent = relevant, DestinationAgent = caused it. + /// + public BuffRemove IsBuffRemoved { get; set; } + + /// + /// if SourceAgent is above 90% health. Otherwise . + /// + public bool IsNinety { get; set; } + + /// + /// if DestinationAgent is below 50% health. Otherwise . + /// + public bool IsFifty { get; set; } + + /// + /// if SourceAgent is moving at time of event. Otherwise . + /// + public bool IsMoving { get; set; } + + /// + /// Type of that occured. + /// + /// + /// SourceAgent is now alive, dead, downed and other ambiguous stuff eg. when SourceAgent is Self, DestinationAgent is a reward id and Value is a reward type such as a wiggly box. + /// + public StateChange IsStateChanged { get; set; } + + /// + /// if SourceAgent is flanking DestinationAgent at time of event. Otherwise . + /// + public bool IsFlanking { get; set; } + + /// + /// if all or part of damage was VS. barrier or shield. Otherwise . + /// + public bool IsShiels { get; set; } + + /// + /// if no buff damage happened during tick. Otherwise . + /// + public bool IsOffCycle { get; set; } + + /// + /// Buff instance id of buff applied. Non-zero if no buff damage happened during tick. Otherwise zero. + /// + public byte Pad61 { get; set; } + + /// + /// Buff instance id of buff applied. + /// + public byte Pad62 { get; set; } + + /// + /// Buff instance id of buff applied. + /// + public byte Pad63 { get; set; } + + /// + /// Buff instance id of buff applied. + /// + /// + /// Used for internal tracking (garbage). + /// + public byte Pad64 { get; set; } + } + +} \ No newline at end of file diff --git a/Blish HUD/GameServices/ArcDps/V2/Models/ConditionResult.cs b/Blish HUD/GameServices/ArcDps/V2/Models/ConditionResult.cs new file mode 100644 index 000000000..84a77b0ec --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Models/ConditionResult.cs @@ -0,0 +1,10 @@ +namespace Blish_HUD.GameServices.ArcDps.V2.Models { + public enum ConditionResult : byte { + ExpectedToHit = 0, + InvulnerableByBuff = 1, + InvulnerableByPlayerSkill1 = 2, + InvulnerableByPlayerSkill2 = 3, + InvulnerableByPlayerSkill3 = 4, + Unknown = 5, + } +} \ No newline at end of file diff --git a/Blish HUD/GameServices/ArcDps/V2/Models/ImgUiCallback.cs b/Blish HUD/GameServices/ArcDps/V2/Models/ImgUiCallback.cs new file mode 100644 index 000000000..aeadb4c10 --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Models/ImgUiCallback.cs @@ -0,0 +1,5 @@ +namespace Blish_HUD.GameServices.ArcDps.V2.Models { + public struct ImGuiCallback { + public uint NotCharacterSelectOrLoading { get; set; } + } +} \ No newline at end of file diff --git a/Blish HUD/GameServices/ArcDps/V2/Models/PhysicalResult.cs b/Blish HUD/GameServices/ArcDps/V2/Models/PhysicalResult.cs new file mode 100644 index 000000000..f87ec362d --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Models/PhysicalResult.cs @@ -0,0 +1,17 @@ +namespace Blish_HUD.GameServices.ArcDps.V2.Models { + public enum PhysicalResult : byte { + Normal = 0, + Critical = 1, + Glance = 2, + Blocked = 3, + Evaded = 4, + Interrupted = 5, + Absorbed = 6, + Blinded = 7, + KillingBlow = 8, + Downed = 9, + BreakbarDamage = 10, + Activation = 11, + Unknown = 12, + } +} \ No newline at end of file diff --git a/Blish HUD/GameServices/ArcDps/V2/Models/StateChange.cs b/Blish HUD/GameServices/ArcDps/V2/Models/StateChange.cs new file mode 100644 index 000000000..c7af1269e --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Models/StateChange.cs @@ -0,0 +1,57 @@ +namespace Blish_HUD.GameServices.ArcDps.V2.Models { + public enum StateChange : byte { + None = 0, + EnterCombat = 1, + ExitCombat = 2, + ChangeUp = 3, + ChangeDead = 4, + ChangeDown = 5, + Spawn = 6, + Despawn = 7, + HealthUpdate = 8, + LogStart = 9, + LogEnd = 10, + WeaponSwap = 11, + MaxHealthUpdate = 12, + PointOfView = 13, + Language = 14, + GWBuild = 15, + ShardId = 16, + Reward = 17, + BuffInitial = 18, + Position = 19, + Velocity = 20, + Rotation = 21, + TeamChange = 22, + AttackTarget = 23, + Targetable = 24, + MapID = 25, + ReplInfo = 26, + StackActive = 27, + StackReset = 28, + Guild = 29, + BuffInfo = 30, + BuffFormula = 31, + SkillInfo = 32, + SkillTiming = 33, + BreakbarState = 34, + BreakbarPercent = 35, + Error = 36, + Tag = 37, + BarrierUpdate = 38, + StatReset = 39, + Extension = 40, + APIDelayed = 41, + InstanceStart = 42, + TickRate = 43, + Last90BeforeDown = 44, + Effect_45 = 45, + EffectIDToGUID = 46, + LogStartNPCUpdate = 47, + Idle = 48, + ExtensionCombat = 49, + FractalScale = 50, + Effect_51 = 51, + Unknown = 38, + } +} \ No newline at end of file diff --git a/Blish HUD/GameServices/ArcDps/V2/Models/Unofficial Extras/ChannelType.cs b/Blish HUD/GameServices/ArcDps/V2/Models/Unofficial Extras/ChannelType.cs new file mode 100644 index 000000000..068b84c19 --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Models/Unofficial Extras/ChannelType.cs @@ -0,0 +1,8 @@ +namespace Blish_HUD.GameServices.ArcDps.Models.UnofficialExtras { + public enum ChannelType { + Party = 0, + Squad = 1, + _Reserved = 2, + Invalid = 3, + } +} diff --git a/Blish HUD/GameServices/ArcDps/V2/Models/Unofficial Extras/ChatMessageInfo.cs b/Blish HUD/GameServices/ArcDps/V2/Models/Unofficial Extras/ChatMessageInfo.cs new file mode 100644 index 000000000..fb8afc7eb --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Models/Unofficial Extras/ChatMessageInfo.cs @@ -0,0 +1,21 @@ +namespace Blish_HUD.GameServices.ArcDps.Models.UnofficialExtras { + public struct ChatMessageInfo { + public uint ChannelId { get; set; } + + public ChannelType ChannelType { get; set; } + + public byte Subgroup { get; set; } + + public bool IsBroadcast { get; set; } + + public byte _unused1 { get; set; } + + public string TimeStamp { get; set; } + + public string AccountName { get; set; } + + public string CharacterName { get; set; } + + public string Text { get; set; } + } +} diff --git a/Blish HUD/GameServices/ArcDps/V2/Models/Unofficial Extras/Language.cs b/Blish HUD/GameServices/ArcDps/V2/Models/Unofficial Extras/Language.cs new file mode 100644 index 000000000..af983aa63 --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Models/Unofficial Extras/Language.cs @@ -0,0 +1,9 @@ +namespace Blish_HUD.GameServices.ArcDps.Models.UnofficialExtras { + public enum Language { + English = 0, + French = 2, + German = 3, + Spanish = 4, + Chinese = 5, + } +} diff --git a/Blish HUD/GameServices/ArcDps/V2/Models/Unofficial Extras/UserInfo.cs b/Blish HUD/GameServices/ArcDps/V2/Models/Unofficial Extras/UserInfo.cs new file mode 100644 index 000000000..75f376595 --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Models/Unofficial Extras/UserInfo.cs @@ -0,0 +1,17 @@ +namespace Blish_HUD.GameServices.ArcDps.Models.UnofficialExtras { + public struct UserInfo { + public string AccountName { get; set; } + + public ulong JoinTime { get; set; } + + public UserRole Role { get; set; } + + public byte Subgroup { get; set; } + + public bool ReadyStatus { get; set; } + + public byte _unused1 { get; set; } + + public uint _unused2 { get; set; } + } +} diff --git a/Blish HUD/GameServices/ArcDps/V2/Models/Unofficial Extras/UserRole.cs b/Blish HUD/GameServices/ArcDps/V2/Models/Unofficial Extras/UserRole.cs new file mode 100644 index 000000000..5eec2238c --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Models/Unofficial Extras/UserRole.cs @@ -0,0 +1,10 @@ +namespace Blish_HUD.GameServices.ArcDps.Models.UnofficialExtras { + public enum UserRole { + SquadLeader = 0, + Lieutenant = 1, + Member = 2, + Invited = 3, + Applied = 4, + None = 5, + } +} diff --git a/Blish HUD/GameServices/ArcDps/V2/Processors/BincodeSerializer.cs b/Blish HUD/GameServices/ArcDps/V2/Processors/BincodeSerializer.cs new file mode 100644 index 000000000..5e1e9af9d --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Processors/BincodeSerializer.cs @@ -0,0 +1,236 @@ +using Gw2Sharp.ChatLinks.Internal; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blish_HUD.GameServices.ArcDps.V2.Processors { + public static class BincodeSerializer { + public static class FloatConverter { + public static class Float32Converter { + public static float Convert(BinaryReader reader) { + return reader.ReadSingle(); + } + } + + public static class Float64Converter { + public static double Convert(BinaryReader reader) { + return reader.ReadDouble(); + } + } + } + + public static class IntConverter { + public static bool UseVarint { get; set; } = true; + + public class VarintEncoding { + public static readonly VarintEncoding Instance = new VarintEncoding(); + + public ulong ConvertUnsigned(BinaryReader reader) { + var firstByte = reader.ReadByte(); + + if (firstByte < 251) { + return firstByte; + } else if (firstByte == 251) { + return reader.ReadUInt16(); + } else if (firstByte == 252) { + return reader.ReadUInt32(); + } else if (firstByte == 253) { + return reader.ReadUInt64(); + } else { + throw new InvalidOperationException("Varint Encoding size was Int128"); + } + } + + public long Convert(BinaryReader reader) { + var unsigned = ConvertUnsigned(reader); + return UnZigZag(unsigned); + } + + private long UnZigZag(ulong unsigned) { + if (unsigned == 0) { + return 0; + } else if (unsigned % 2 == 0) { + return (long)(unsigned / 2); + } else { + return (long)((unsigned + 1) / 2) * -1; + } + } + } + + public static class Int8Converter { + public static sbyte Convert(BinaryReader reader) { + if (UseVarint) { + return (sbyte)VarintEncoding.Instance.Convert(reader); + } + return reader.ReadSByte(); + } + + public static byte ConvertUnsigned(BinaryReader reader) { + if (UseVarint) { + return (byte)VarintEncoding.Instance.ConvertUnsigned(reader); + } + return reader.ReadByte(); + } + } + + public static class Int16Converter { + public static short Convert(BinaryReader reader) { + if (UseVarint) { + return (short)VarintEncoding.Instance.Convert(reader); + } + return reader.ReadInt16(); + } + + public static ushort ConvertUnsigned(BinaryReader reader) { + if (UseVarint) { + return (ushort)VarintEncoding.Instance.ConvertUnsigned(reader); + } + return reader.ReadUInt16(); + } + } + + public static class Int32Converter { + public static int Convert(BinaryReader reader) { + if (UseVarint) { + return (int)VarintEncoding.Instance.Convert(reader); + } + return reader.ReadInt32(); + } + + public static uint ConvertUnsigned(BinaryReader reader) { + if (UseVarint) { + return (uint)VarintEncoding.Instance.ConvertUnsigned(reader); + } + return reader.ReadUInt32(); + } + } + + public static class Int64Converter { + public static long Convert(BinaryReader reader) { + if (UseVarint) { + return VarintEncoding.Instance.Convert(reader); + } + return reader.ReadInt64(); + } + + public static ulong ConvertUnsigned(BinaryReader reader) { + if (UseVarint) { + return VarintEncoding.Instance.ConvertUnsigned(reader); + } + return reader.ReadUInt64(); + } + } + + public static class ISizeConverter { + public static long Convert(BinaryReader reader) { + if (UseVarint) { + return VarintEncoding.Instance.Convert(reader); + } + return reader.ReadInt64(); + } + } + + public static class USizeConverter { + public static ulong Convert(BinaryReader reader) { + if (UseVarint) { + return VarintEncoding.Instance.ConvertUnsigned(reader); + } + return reader.ReadUInt64(); + } + } + } + + public static class BoolConverter { + public static bool Convert(BinaryReader reader) { + return reader.ReadBoolean(); + } + } + + public static class CollectionConverter { + public static class ArrayConverter { + // TODO: Maybe make this more performant in code generation and generate the specific count of + public static IEnumerable Convert(BinaryReader binaryReader, Func converter, int size) { + for (var i = 0; i < size; i++) { + yield return converter(binaryReader); + } + } + } + + public static class StringConverter { + public static string Convert(BinaryReader reader) { + var size = IntConverter.USizeConverter.Convert(reader); + return Encoding.UTF8.GetString(reader.ReadBytes((int)size)); + } + } + + public static class VariableLengthConverter { + public static IEnumerable Convert(BinaryReader reader, Func converter) { + var size = (int)IntConverter.USizeConverter.Convert(reader); + + for (var i = 0; i < size; i++) { + yield return converter(reader); + } + } + } + } + } + + public class BincodeBinaryReader : BinaryReader { + public Converter Convert { get; set; } + + public BincodeBinaryReader(Stream input) : base(input) { + this.Convert = new Converter(this); + } + + public BincodeBinaryReader(Stream input, Encoding encoding) : base(input, encoding) { + this.Convert = new Converter(this); + } + + public BincodeBinaryReader(Stream input, Encoding encoding, bool leaveOpen) : base(input, encoding, leaveOpen) { + this.Convert = new Converter(this); + } + + public class Converter { + private readonly BinaryReader reader; + + internal Converter(BinaryReader reader) { + this.reader = reader; + } + + public float ParseFloat() => BincodeSerializer.FloatConverter.Float32Converter.Convert(reader); + + public double ParseDouble() => BincodeSerializer.FloatConverter.Float64Converter.Convert(reader); + + public sbyte ParseSByte() => BincodeSerializer.IntConverter.Int8Converter.Convert(reader); + + public byte ParseByte() => BincodeSerializer.IntConverter.Int8Converter.ConvertUnsigned(reader); + + public short ParseShort() => BincodeSerializer.IntConverter.Int16Converter.Convert(reader); + + public ushort ParseUShort() => BincodeSerializer.IntConverter.Int16Converter.ConvertUnsigned(reader); + + public int ParseInt() => BincodeSerializer.IntConverter.Int32Converter.Convert(reader); + + public uint ParseUInt() => BincodeSerializer.IntConverter.Int32Converter.ConvertUnsigned(reader); + + public long ParseLong() => BincodeSerializer.IntConverter.Int64Converter.Convert(reader); + + public ulong ParseULong() => BincodeSerializer.IntConverter.Int64Converter.ConvertUnsigned(reader); + + public string ParseString() => BincodeSerializer.CollectionConverter.StringConverter.Convert(reader); + + public T[] ParseArray(int size, Func creationFunc) => BincodeSerializer.CollectionConverter.ArrayConverter.Convert(reader, creationFunc, size).ToArray(); + + public List ParseList(Func creationFunc) => BincodeSerializer.CollectionConverter.VariableLengthConverter.Convert(reader, creationFunc).ToList(); + + public long ParseISize() => BincodeSerializer.IntConverter.ISizeConverter.Convert(reader); + + public ulong ParseUSize() => BincodeSerializer.IntConverter.USizeConverter.Convert(reader); + + public bool ParseBool() => BincodeSerializer.BoolConverter.Convert(reader); + } + } +} diff --git a/Blish HUD/GameServices/ArcDps/V2/Processors/ImGuiProcessor.cs b/Blish HUD/GameServices/ArcDps/V2/Processors/ImGuiProcessor.cs new file mode 100644 index 000000000..794edb0bc --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Processors/ImGuiProcessor.cs @@ -0,0 +1,22 @@ +using Blish_HUD.GameServices.ArcDps.V2.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blish_HUD.GameServices.ArcDps.V2.Processors { + internal class ImGuiProcessor : MessageProcessor { + internal override bool TryInternalProcess(byte[] message, out ImGuiCallback result) { + + try { + result = new ImGuiCallback() { NotCharacterSelectOrLoading = message[0] }; + + return true; + } catch (Exception) { + result = default; + return false; + } + } + } +} diff --git a/Blish HUD/GameServices/ArcDps/V2/Processors/LegacyCombatProcessor.cs b/Blish HUD/GameServices/ArcDps/V2/Processors/LegacyCombatProcessor.cs new file mode 100644 index 000000000..23225e9e4 --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Processors/LegacyCombatProcessor.cs @@ -0,0 +1,202 @@ +using Blish_HUD.GameServices.ArcDps.V2.Models; +using System; +using System.Text; + +namespace Blish_HUD.GameServices.ArcDps.V2.Processors { + internal class LegacyCombatProcessor : MessageProcessor { + + internal override bool TryInternalProcess(byte[] message, out CombatCallback result) { + try { + result = ProcessCombat(message); + return true; + + } catch (Exception) { + result = default; + return false; + } + } + + public static CombatCallback ProcessCombat(byte[] data) { + CombatEvent ev = default; + Agent src = default; + Agent dst = default; + string skillName = null; + int offset = 1; + + if ((byte)(data[0] & (byte)CombatMessageFlags.Ev) == (byte)CombatMessageFlags.Ev) (ev, offset) = ParseEv(data, offset); + + if ((byte)(data[0] & (byte)CombatMessageFlags.Src) == (byte)CombatMessageFlags.Src) (src, offset) = ParseAg(data, offset); + + if ((byte)(data[0] & (byte)CombatMessageFlags.Dst) == (byte)CombatMessageFlags.Dst) (dst, offset) = ParseAg(data, offset); + + if ((byte)(data[0] & (byte)CombatMessageFlags.SkillName) == (byte)CombatMessageFlags.SkillName) (skillName, offset) = ParseString(data, offset); + + ulong id = BitConverter.ToUInt64(data, offset); + ulong revision = BitConverter.ToUInt64(data, offset + 8); + + return new CombatCallback() { + Event = ev, + Source = src, + Destination = dst, + SkillName = skillName, + Id = id, + Revision = revision, + }; + } + + private static (CombatEvent, int) ParseEv(byte[] data, int offset) { + ulong time; + ulong srcAgent; + ulong dstAgent; + int value; + int buffDmg; + uint overStackValue; + uint skillId; + ushort srcInstId; + ushort dstInstId; + ushort srcMasterInstId; + ushort dstMasterInstId; + byte iff; + bool buff; + byte result; + byte isActivation; + byte isBuffRemove; + bool isNinety; + bool isFifty; + bool isMoving; + byte isStateChange; + bool isFlanking; + bool isShields; + bool isOffCycle; + byte pad61; + byte pad62; + byte pad63; + byte pad64; + (time, offset) = U64(data, offset); + (srcAgent, offset) = U64(data, offset); + (dstAgent, offset) = U64(data, offset); + (value, offset) = I32(data, offset); + (buffDmg, offset) = I32(data, offset); + (overStackValue, offset) = U32(data, offset); + (skillId, offset) = U32(data, offset); + (srcInstId, offset) = U16(data, offset); + (dstInstId, offset) = U16(data, offset); + (srcMasterInstId, offset) = U16(data, offset); + (dstMasterInstId, offset) = U16(data, offset); + (iff, offset) = U8(data, offset); + (buff, offset) = B(data, offset); + (result, offset) = U8(data, offset); + (isActivation, offset) = U8(data, offset); + (isBuffRemove, offset) = U8(data, offset); + (isNinety, offset) = B(data, offset); + (isFifty, offset) = B(data, offset); + (isMoving, offset) = B(data, offset); + (isStateChange, offset) = U8(data, offset); + (isFlanking, offset) = B(data, offset); + (isShields, offset) = B(data, offset); + (isOffCycle, offset) = B(data, offset); + (pad61, offset) = U8(data, offset); + (pad62, offset) = U8(data, offset); + (pad63, offset) = U8(data, offset); + (pad64, offset) = U8(data, offset); + + var ev = new CombatEvent() { + Time = time, + SourceAgent = srcAgent, + DestinationAgent = dstAgent, + Value = value, + BuffDamage = buffDmg, + OverstackValue = overStackValue, + SkillId = skillId, + SourceInstanceId = srcInstId, + DestinationInstanceId = dstInstId, + SourceMasterInstanceId = srcMasterInstId, + DestinationMasterInstanceId = dstMasterInstId, + Iff = (Affinity)iff, + Buff = buff, + Result = result, + IsActivation = (Activation)isActivation, + IsBuffRemoved = (BuffRemove)isBuffRemove, + IsNinety = isNinety, + IsFifty = isFifty, + IsMoving = isMoving, + IsStateChanged = (StateChange)isStateChange, + IsFlanking = isFlanking, + IsShiels = isShields, + IsOffCycle = isOffCycle, + Pad61 = pad61, + Pad62 = pad62, + Pad63 = pad63, + Pad64 = pad64, + }; + + return (ev, offset); + } + + private static (Agent, int) ParseAg(byte[] data, int offset) { + string name; + ulong id; + uint profession; + uint elite; + uint self; + ushort team; + (name, offset) = ParseString(data, offset); + (id, offset) = U64(data, offset); + (profession, offset) = U32(data, offset); + (elite, offset) = U32(data, offset); + (self, offset) = U32(data, offset); + (team, offset) = U16(data, offset); + + var ag = new Agent() { + Name = name, + Id = id, + Profession = profession, + Elite = elite, + Self = self, + Team = team, + }; + + return (ag, offset); + } + + private static (string, int) ParseString(byte[] data, int offset) { + ulong length; + (length, offset) = U64(data, offset); + string str = Encoding.UTF8.GetString(data, offset, (int)length); + return (str, offset + (int)length); + } + + private static (ulong, int) U64(byte[] data, int offset) { + return (BitConverter.ToUInt64(data, offset), offset + 8); + } + + private static (uint, int) U32(byte[] data, int offset) { + return (BitConverter.ToUInt32(data, offset), offset + 4); + } + + private static (int, int) I32(byte[] data, int offset) { + return (BitConverter.ToInt32(data, offset), offset + 4); + } + + private static (ushort, int) U16(byte[] data, int offset) { + return (BitConverter.ToUInt16(data, offset), offset + 2); + } + + private static (byte, int) U8(byte[] data, int offset) { + return (data[offset], offset + 1); + } + + private static (bool, int) B(byte[] data, int offset) { + return (data[offset] != 0, offset + 1); + } + + private enum CombatMessageFlags { + + Ev = 0x01, + Src = 0x02, + Dst = 0x04, + SkillName = 0x08 + + } + } +} diff --git a/Blish HUD/GameServices/ArcDps/V2/Processors/MessageProcessor.cs b/Blish HUD/GameServices/ArcDps/V2/Processors/MessageProcessor.cs new file mode 100644 index 000000000..83b89c115 --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Processors/MessageProcessor.cs @@ -0,0 +1,8 @@ +using System.Threading; + +namespace Blish_HUD.GameServices.ArcDps.V2.Processors { + internal abstract class MessageProcessor { + + public abstract void Process(byte[] message, CancellationToken ct); + } +} diff --git a/Blish HUD/GameServices/ArcDps/V2/Processors/MessageProcessor{T}.cs b/Blish HUD/GameServices/ArcDps/V2/Processors/MessageProcessor{T}.cs new file mode 100644 index 000000000..bd4ed9774 --- /dev/null +++ b/Blish HUD/GameServices/ArcDps/V2/Processors/MessageProcessor{T}.cs @@ -0,0 +1,33 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Blish_HUD.GameServices.ArcDps.V2.Processors { + internal abstract class MessageProcessor : MessageProcessor + where T : struct { + private readonly List> listeners = new List>(); + + public override void Process(byte[] message, CancellationToken ct) { + if (listeners.Count > 0 && TryInternalProcess(message, out var parsedMessage)) { + Task.Run(async () => await SendToListener(parsedMessage, ct)); + } + + } + + private async Task SendToListener(T Message, CancellationToken ct) { + foreach (var listener in listeners) { + ct.ThrowIfCancellationRequested(); + await listener.Invoke(Message, ct); + } + } + + internal abstract bool TryInternalProcess(byte[] message, out T result); + + public void RegisterListener(Func listener) { + listeners.Add(listener); + } + + } +} diff --git a/Blish HUD/GameServices/ArcDpsService.cs b/Blish HUD/GameServices/ArcDpsService.cs index ccdd3646f..64546765c 100644 --- a/Blish HUD/GameServices/ArcDpsService.cs +++ b/Blish HUD/GameServices/ArcDpsService.cs @@ -7,79 +7,75 @@ using Blish_HUD.ArcDps; using Blish_HUD.ArcDps.Common; using Blish_HUD.ArcDps.Models; +using Blish_HUD.GameServices.ArcDps.V2.Models; using Microsoft.Xna.Framework; namespace Blish_HUD { + [Obsolete("This class only wraps the V2 service, please use that one instead")] public class ArcDpsService : GameService { private static readonly Logger Logger = Logger.GetLogger(); - private static readonly object WatchLock = new object(); - #if DEBUG + private readonly ConcurrentDictionary>> _subscriptions = + new ConcurrentDictionary>>(); + +#if DEBUG public static long Counter; - #endif +#endif /// /// Triggered upon error of the underlaying socket listener. /// + [Obsolete("This class only wraps the V2 service, please use that one instead")] public event EventHandler Error; /// /// Provides common fields that multiple modules might want to track /// + [Obsolete("This class only wraps the V2 service, please use that one instead")] public CommonFields Common { get; private set; } /// /// Indicates if arcdps updated in the last second (it should every in-game frame) /// - public bool RenderPresent { get; private set; } + [Obsolete("This class only wraps the V2 service, please use that one instead")] + public bool RenderPresent => GameService.ArcDpsV2.RenderPresent; /// /// Indicates if the socket listener for the arcdps service is running and arcdps sent an update in the last second. /// - public bool Running => this._server?.Running ?? false && this.RenderPresent; + [Obsolete("This class only wraps the V2 service, please use that one instead")] + public bool Running => GameService.ArcDpsV2.Running; /// /// Indicates if arcdps currently draws its HUD (not in character select, cut scenes or loading screens) /// - public bool HudIsActive { - get { - lock (WatchLock) { - return _hudIsActive; - } - } - private set { - lock (WatchLock) { - _stopwatch.Restart(); - _hudIsActive = value; - } - } - } + [Obsolete("This class only wraps the V2 service, please use that one instead")] + public bool HudIsActive => GameService.ArcDpsV2.HudIsActive; + /// /// The timespan after which ArcDPS is treated as not responding. /// private readonly TimeSpan _leeway = TimeSpan.FromMilliseconds(1000); - private readonly ConcurrentDictionary>> _subscriptions = - new ConcurrentDictionary>>(); - - private bool _hudIsActive; - - /// - /// The underlaying connected to the ArcDPS BlishHUD Bridge. - /// - private SocketListener _server; - private Stopwatch _stopwatch; - private bool _subscribed; + [Obsolete("This class only wraps the V2 service, please use that one instead")] public void SubscribeToCombatEventId(Action func, params uint[] skillIds) { + if (!_subscribed) { - this.RawCombatEvent += DispatchSkillSubscriptions; - _subscribed = true; + GameService.ArcDpsV2.RegisterMessageType(2, async (combatEvent, ct) => { + DispatchSkillSubscriptions(combatEvent, RawCombatEventArgs.CombatEventType.Area); + await System.Threading.Tasks.Task.CompletedTask; + }); + GameService.ArcDpsV2.RegisterMessageType(3, async (combatEvent, ct) => { + DispatchSkillSubscriptions(combatEvent, RawCombatEventArgs.CombatEventType.Local); + await System.Threading.Tasks.Task.CompletedTask; + }); + _subscribed = true; } foreach (uint skillId in skillIds) { @@ -89,13 +85,13 @@ public void SubscribeToCombatEventId(Action func, pa } } - private void DispatchSkillSubscriptions(object sender, RawCombatEventArgs eventHandler) { - if (eventHandler.CombatEvent.Ev == null) return; - - uint skillId = eventHandler.CombatEvent.Ev.SkillId; + private void DispatchSkillSubscriptions(CombatCallback combatEvent, RawCombatEventArgs.CombatEventType combatEventType) { + uint skillId = combatEvent.Event.SkillId; if (!_subscriptions.ContainsKey(skillId)) return; - foreach (Action action in _subscriptions[skillId]) action(sender, eventHandler); + foreach (Action action in _subscriptions[skillId]) { + action(this, ConvertFrom(combatEvent, combatEventType)); + } } /// @@ -109,58 +105,41 @@ private void DispatchSkillSubscriptions(object sender, RawCombatEventArgs eventH /// /// Holds unprocessed combat data /// + [Obsolete("This class only wraps the V2 service, please use that one instead")] public event EventHandler RawCombatEvent; protected override void Initialize() { - this.Common = new CommonFields(); - _stopwatch = new Stopwatch(); - _server = new SocketListener(200_000); - _server.ReceivedMessage += MessageHandler; - _server.OnSocketError += SocketErrorHandler; - #if DEBUG + GameService.ArcDpsV2.Error += Error; + + this.Common = new CommonFields(); + _stopwatch = new Stopwatch(); +#if DEBUG this.RawCombatEvent += (a, b) => { Interlocked.Increment(ref Counter); }; - #endif +#endif + + GameService.ArcDpsV2.RegisterMessageType(2, async (combatEvent, ct) => { + var rawCombat = ConvertFrom(combatEvent, RawCombatEventArgs.CombatEventType.Area); + this.RawCombatEvent?.Invoke(this, rawCombat); + await System.Threading.Tasks.Task.CompletedTask; + }); + + GameService.ArcDpsV2.RegisterMessageType(3, async (combatEvent, ct) => { + var rawCombat = ConvertFrom(combatEvent, RawCombatEventArgs.CombatEventType.Local); + this.RawCombatEvent?.Invoke(this, rawCombat); + await System.Threading.Tasks.Task.CompletedTask; + }); } protected override void Load() { - Gw2Mumble.Info.ProcessIdChanged += Start; _stopwatch.Start(); - } - - /// - /// Starts the socket listener for the arc dps bridge. - /// - private void Start(object sender, ValueEventArgs value) { - this.Start(value.Value); - } - - /// - /// Starts the socket listener for the arc dps bridge. - /// - private void Start(uint processId) { - if (this.Loaded) { - _server.Start(new IPEndPoint(IPAddress.Loopback, GetPort(processId))); - } - } - - private static int GetPort(uint processId) { - ushort pid; - - unchecked { - pid = (ushort) processId; - } - - return pid | (1 << 14) | (1 << 15); + this.SubscribeToCombatEventId((source, combatEvent) => { + System.Diagnostics.Debug.WriteLine(""); + }, + 43916); } protected override void Unload() { - Gw2Mumble.Info.ProcessIdChanged -= Start; - _server.ReceivedMessage -= MessageHandler; - _server.OnSocketError -= SocketErrorHandler; - _stopwatch.Stop(); - _server.Stop(); - this.RenderPresent = false; } protected override void Update(GameTime gameTime) { @@ -169,49 +148,67 @@ protected override void Update(GameTime gameTime) { lock (WatchLock) { elapsed = _stopwatch.Elapsed; } - - this.RenderPresent = elapsed < _leeway; } - private void MessageHandler(object sender, MessageData data) { - switch (data.Message[0]) { - case (byte) MessageType.ImGui: - this.HudIsActive = data.Message[1] != 0; - break; - case (byte) MessageType.CombatArea: - this.ProcessCombat(data.Message, RawCombatEventArgs.CombatEventType.Area); - break; - case (byte) MessageType.CombatLocal: - this.ProcessCombat(data.Message, RawCombatEventArgs.CombatEventType.Local); - break; + private static RawCombatEventArgs ConvertFrom(CombatCallback combatEvent, RawCombatEventArgs.CombatEventType combatEventType) { + + Ev ev = null; + + if (combatEvent.Event.Time != default) { + ev = new Ev( + combatEvent.Event.Time, + combatEvent.Event.SourceAgent, + combatEvent.Event.DestinationAgent, + combatEvent.Event.Value, + combatEvent.Event.BuffDamage, + combatEvent.Event.OverstackValue, + combatEvent.Event.SkillId, + combatEvent.Event.SourceInstanceId, + combatEvent.Event.DestinationInstanceId, + combatEvent.Event.SourceMasterInstanceId, + combatEvent.Event.DestinationMasterInstanceId, + (ArcDpsEnums.IFF)(int)combatEvent.Event.Iff, + combatEvent.Event.Buff, + combatEvent.Event.Result, + (ArcDpsEnums.Activation)(int)combatEvent.Event.IsActivation, + (ArcDpsEnums.BuffRemove)(int)combatEvent.Event.IsBuffRemoved, + combatEvent.Event.IsNinety, + combatEvent.Event.IsFifty, + combatEvent.Event.IsMoving, + (ArcDpsEnums.StateChange)(int)combatEvent.Event.IsStateChanged, + combatEvent.Event.IsFlanking, + combatEvent.Event.IsShiels, + combatEvent.Event.IsOffCycle, + combatEvent.Event.Pad61, + combatEvent.Event.Pad62, + combatEvent.Event.Pad63, + combatEvent.Event.Pad64); } - } - private void SocketErrorHandler(object sender, SocketError socketError) { - // Socketlistener stops by itself. - Logger.Error("Encountered socket error: {0}", socketError.ToString()); - - this.Error?.Invoke(this, socketError); - } - - private void ProcessCombat(byte[] data, RawCombatEventArgs.CombatEventType eventType) { - CombatEvent message = CombatParser.ProcessCombat(data); - - this.OnRawCombatEvent(new RawCombatEventArgs(message, eventType)); - } - - private void OnRawCombatEvent(RawCombatEventArgs e) { - this.RawCombatEvent?.Invoke(this, e); + var source = new Ag( + combatEvent.Source.Name, + combatEvent.Source.Id, + combatEvent.Source.Profession, + combatEvent.Source.Elite, + combatEvent.Source.Self, + combatEvent.Source.Team); + + var destination = new Ag( + combatEvent.Destination.Name, + combatEvent.Destination.Id, + combatEvent.Destination.Profession, + combatEvent.Destination.Elite, + combatEvent.Destination.Self, + combatEvent.Destination.Team); + + return new RawCombatEventArgs(new ArcDps.Models.CombatEvent( + ev, + source, + destination, + combatEvent.SkillName, + combatEvent.Id, + combatEvent.Revision), + combatEventType); } - - private enum MessageType { - - ImGui = 1, - CombatArea = 2, - CombatLocal = 3 - - } - } - } \ No newline at end of file diff --git a/Blish HUD/GameServices/ArcDpsServiceV2.cs b/Blish HUD/GameServices/ArcDpsServiceV2.cs new file mode 100644 index 000000000..2aa18152b --- /dev/null +++ b/Blish HUD/GameServices/ArcDpsServiceV2.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Blish_HUD.ArcDps; +using Blish_HUD.GameServices.ArcDps; +using Blish_HUD.GameServices.ArcDps.V2; +using Blish_HUD.GameServices.ArcDps.V2.Models; +using Microsoft.Xna.Framework; + +namespace Blish_HUD { + + public class ArcDpsServiceV2 : GameService { + private static readonly Logger Logger = Logger.GetLogger(); + + /// + /// The timespan after which ArcDPS is treated as not responding. + /// + private readonly TimeSpan _leeway = TimeSpan.FromMilliseconds(1000); + private readonly CancellationTokenSource _arcDpsClientCancellationTokenSource = new CancellationTokenSource(); + private readonly List _registerListeners = new List(); + private IArcDpsClient _arcDpsClient; + private bool _hudIsActive; + private Stopwatch _stopwatch; + private bool _subscribed; + +#if DEBUG + public static long Counter => ArcDpsClient.Counter; +#endif + + /// + /// Triggered upon error of the underlaying socket listener. + /// + public event EventHandler Error; + + /// + /// Provides common fields that multiple modules might want to track + /// + public CommonFields Common { get; private set; } + + /// + /// Indicates if arcdps updated in the last second (it should every in-game frame) + /// + public bool RenderPresent { get; private set; } + + /// + /// Indicates if the socket listener for the arcdps service is running and arcdps sent an update in the last second. + /// + public bool Running => (this._arcDpsClient?.Client.Connected ?? false) && this.RenderPresent; + + /// + /// Indicates if arcdps currently draws its HUD (not in character select, cut scenes or loading screens) + /// + public bool HudIsActive { + get { + lock (_stopwatch) { + return _hudIsActive; + } + } + private set { + lock (_stopwatch) { + _stopwatch.Restart(); + _hudIsActive = value; + } + } + } + + public void RegisterMessageType(int type, Func listener) + where T : struct { + Action action = () => _arcDpsClient.RegisterMessageTypeListener(type, listener); + _registerListeners.Add(action); + if (_arcDpsClient != null) { + action(); + } + } + + protected override void Initialize() { + this.Common = new CommonFields(); + _stopwatch = new Stopwatch(); + } + + protected override void Load() { + Gw2Mumble.Info.ProcessIdChanged += Start; + _stopwatch.Start(); + } + + /// + /// Starts the socket listener for the arc dps bridge. + /// + private void Start(object sender, ValueEventArgs value) { + this.Start(value.Value); + } + + /// + /// Starts the socket listener for the arc dps bridge. + /// + private void Start(uint processId) { + if (this.Loaded) { + var version = GetVersion(processId); + if (version == ArcDpsBridgeVersion.None) { + return; + } + + if (_arcDpsClient != null) { + _arcDpsClientCancellationTokenSource.Cancel(); + _arcDpsClient.Dispose(); + _arcDpsClient = null; + } + + _arcDpsClient = new ArcDpsClient(version); + + foreach (var item in _registerListeners) { + item(); + } + + _arcDpsClient.Error += SocketErrorHandler; + _arcDpsClient.Initialize(new IPEndPoint(IPAddress.Loopback, GetPort(processId, version)), _arcDpsClientCancellationTokenSource.Token); + + RegisterMessageType(1, async (imGuiCallback, ct) => { + this.HudIsActive = imGuiCallback.NotCharacterSelectOrLoading != 0; + }); + } + } + + private static int GetPort(uint processId, ArcDpsBridgeVersion version) { + ushort pid; + + unchecked { + pid = (ushort)processId; + } + + // +1 for V2 and +0 for V1 + var port = pid | (1 << 14) | (1 << 15); + if (version == ArcDpsBridgeVersion.V2) { + port++; + } + + return port; + } + + protected override void Unload() { + Gw2Mumble.Info.ProcessIdChanged -= Start; + _arcDpsClientCancellationTokenSource.Cancel(); + _arcDpsClientCancellationTokenSource.Dispose(); + _stopwatch.Stop(); + _arcDpsClient.Disconnect(); + _arcDpsClient.Error -= SocketErrorHandler; + this.RenderPresent = false; + } + + protected override void Update(GameTime gameTime) { + TimeSpan elapsed; + + lock (_stopwatch) { + elapsed = _stopwatch.Elapsed; + } + + this.RenderPresent = elapsed < _leeway; + } + + private void SocketErrorHandler(object sender, SocketError socketError) { + // Socketlistener stops by itself. + Logger.Error("Encountered socket error: {0}", socketError.ToString()); + + this.Error?.Invoke(this, socketError); + } + + private ArcDpsBridgeVersion GetVersion(uint processId) { + try { + var port = GetPort(processId, ArcDpsBridgeVersion.V2); + var client = new TcpClient(); + client.Connect(new IPEndPoint(IPAddress.Loopback, port)); + client.Dispose(); + return ArcDpsBridgeVersion.V2; + } catch (Exception) { + } + + try { + var port = GetPort(processId, ArcDpsBridgeVersion.V1); + var client = new TcpClient(); + client.Connect(new IPEndPoint(IPAddress.Loopback, port)); + client.Dispose(); + return ArcDpsBridgeVersion.V1; + } catch (Exception) { + } + + return ArcDpsBridgeVersion.None; + } + } + +} \ No newline at end of file diff --git a/Blish HUD/GameServices/GameService.cs b/Blish HUD/GameServices/GameService.cs index 60517976b..0e40d4e8c 100644 --- a/Blish HUD/GameServices/GameService.cs +++ b/Blish HUD/GameServices/GameService.cs @@ -17,6 +17,7 @@ public abstract class GameService { Graphics = new GraphicsService(), Overlay = new OverlayService(), GameIntegration = new GameIntegrationService(), + ArcDpsV2 = new ArcDpsServiceV2(), // This needs to be initialized bf the V1 ArcDps = new ArcDpsService(), Contexts = new ContextsService(), Module = new ModuleService() @@ -93,6 +94,7 @@ internal void DoUpdate(GameTime gameTime) { public static readonly InputService Input; public static readonly GameIntegrationService GameIntegration; public static readonly ArcDpsService ArcDps; + public static readonly ArcDpsServiceV2 ArcDpsV2; public static readonly ContextsService Contexts; public static readonly ModuleService Module;