diff --git a/ArcdpsLogManager/ArcdpsLogManager.csproj b/ArcdpsLogManager/ArcdpsLogManager.csproj
index 5ab2f937..99043e00 100644
--- a/ArcdpsLogManager/ArcdpsLogManager.csproj
+++ b/ArcdpsLogManager/ArcdpsLogManager.csproj
@@ -17,12 +17,12 @@
Each new log data update causes a revision increase.
See LogDataUpdater for the updates.
-->
- 1.7.1.2
+ 1.9.0.1
-
+
diff --git a/ArcdpsLogManager/CHANGELOG.md b/ArcdpsLogManager/CHANGELOG.md
index 52fbd35a..a7612ef3 100644
--- a/ArcdpsLogManager/CHANGELOG.md
+++ b/ArcdpsLogManager/CHANGELOG.md
@@ -2,10 +2,23 @@
This is the full changelog of the arcdps Log Manager.
-## Log Manager v1.7.1
+## Log Manager v1.9 (unreleased)
+#### New features
+s Added support for Silent Surf CM
+- Added fractal scale as a column to the log list; right-click to hide/show (in logs since 2023-07-16)
+- Added fractal scale to the log detail panel (in logs since 2023-07-16)
+- *EVTC Inspector*: Added rudimentary support for the reworked effect events (since 2023-07-16).
+#### Changes
+- The game data collecting tab (requires Show -> Debug data to be enabled) now scans multiple files at once. This
+ provides a nice performance benefit on SSDs, but may result in slowdowns with HDDs (not tested on a HDD).
+#### Fixes
+- Fixed API error caused by adding a workaround for a GW2 API internal error when searching for unknown guilds.
+
+## Log Manager v1.8
#### New features
- Added support for Old Lion's Court CM
- Added rudimentary support for map (full instance) logs – they are correctly identified, have proper map names, and a filtering category.
+- *EVTC Inspector*: Added skill and buff info from logs to Processed skills tab (no skill timings and buff formulas yet)
## Log Manager v1.7
diff --git a/ArcdpsLogManager/Configuration/StoredSettings.cs b/ArcdpsLogManager/Configuration/StoredSettings.cs
index eb59308c..a140138e 100644
--- a/ArcdpsLogManager/Configuration/StoredSettings.cs
+++ b/ArcdpsLogManager/Configuration/StoredSettings.cs
@@ -23,6 +23,6 @@ public class StoredSettings
public string DpsReportDomain { get; set; } = DpsReportUploader.DefaultDomain.Domain;
public bool DpsReportUploadDetailedWvw { get; set; } = false;
public int? MinimumLogDurationSeconds { get; set; } = null;
- public List HiddenLogListColumns { get; set; } = new List() {"Character", "Map ID", "Game Version", "arcdps Version", "Instabilities"};
+ public List HiddenLogListColumns { get; set; } = new List() {"Character", "Map ID", "Game Version", "arcdps Version", "Instabilities", "Scale"};
public List IgnoredUpdateVersions { get; set; } = new List();
}
\ No newline at end of file
diff --git a/ArcdpsLogManager/Controls/LogDetailPanel.cs b/ArcdpsLogManager/Controls/LogDetailPanel.cs
index 5828e20d..d06aabf0 100644
--- a/ArcdpsLogManager/Controls/LogDetailPanel.cs
+++ b/ArcdpsLogManager/Controls/LogDetailPanel.cs
@@ -69,6 +69,16 @@ public LogData LogData
EncounterMode.Emboldened5 => "Emboldened 5",
_ => throw new ArgumentOutOfRangeException()
};
+
+ if (logData.LogExtras.FractalExtras?.FractalScale != null)
+ {
+ if (modeLabel.Text != "")
+ {
+ modeLabel.Text += ", ";
+ }
+ modeLabel.Text += $"Scale {logData.LogExtras.FractalExtras.FractalScale}";
+ }
+
modeLabel.Visible = !String.IsNullOrWhiteSpace(modeLabel.Text);
string result;
diff --git a/ArcdpsLogManager/Controls/LogEncounterFilterTree.cs b/ArcdpsLogManager/Controls/LogEncounterFilterTree.cs
index e2e824c9..cab619b9 100644
--- a/ArcdpsLogManager/Controls/LogEncounterFilterTree.cs
+++ b/ArcdpsLogManager/Controls/LogEncounterFilterTree.cs
@@ -259,6 +259,7 @@ private Image GetCategoryIcon(EncounterCategory category)
EncounterCategory.RaidWing5 => imageProvider.GetRaidWingIcon(),
EncounterCategory.RaidWing6 => imageProvider.GetRaidWingIcon(),
EncounterCategory.RaidWing7 => imageProvider.GetRaidWingIcon(),
+ EncounterCategory.Map => imageProvider.GetTinyInstanceIcon(),
_ => null
};
}
diff --git a/ArcdpsLogManager/ImageProvider.cs b/ArcdpsLogManager/ImageProvider.cs
index b9ea76f9..8bba29a4 100644
--- a/ArcdpsLogManager/ImageProvider.cs
+++ b/ArcdpsLogManager/ImageProvider.cs
@@ -64,6 +64,7 @@ public class ImageProvider
private Lazy TinyIconFestival { get; } = new Lazy(Resources.GetTinyIconFestival);
private Lazy TinyIconIcebroodSaga { get; } = new Lazy(Resources.GetTinyIconIcebroodSaga);
private Lazy TinyIconEndOfDragons { get; } = new Lazy(Resources.GetTinyIconEndOfDragons);
+ private Lazy TinyIconInstance { get; } = new Lazy(Resources.GetTinyIconInstance);
// RAIDS
private Lazy GenericRaidWing { get; } = new Lazy(Resources.GetGenericRaidWingIcon);
@@ -188,6 +189,7 @@ public class ImageProvider
public Image GetTinyFestivalIcon() => TinyIconFestival.Value;
public Image GetTinyIcebroodSagaIcon() => TinyIconIcebroodSaga.Value;
public Image GetTinyEndOfDragonsIcon() => TinyIconEndOfDragons.Value;
+ public Image GetTinyInstanceIcon() => TinyIconInstance.Value;
public Image GetTinyProfessionIcon(Profession profession)
{
diff --git a/ArcdpsLogManager/Images/ArenaNet/storyline_32px.png b/ArcdpsLogManager/Images/ArenaNet/storyline_32px.png
new file mode 100644
index 00000000..755b0b04
Binary files /dev/null and b/ArcdpsLogManager/Images/ArenaNet/storyline_32px.png differ
diff --git a/ArcdpsLogManager/Logs/Extras/FractalExtras.cs b/ArcdpsLogManager/Logs/Extras/FractalExtras.cs
index fe8686a0..f4a752a9 100644
--- a/ArcdpsLogManager/Logs/Extras/FractalExtras.cs
+++ b/ArcdpsLogManager/Logs/Extras/FractalExtras.cs
@@ -8,4 +8,7 @@ public class FractalExtras
{
[JsonProperty]
public List MistlockInstabilities { get; set; } = new List();
+
+ [JsonProperty]
+ public int? FractalScale { get; set; } = null;
}
\ No newline at end of file
diff --git a/ArcdpsLogManager/Logs/Filters/Groups/CategoryLogGroup.cs b/ArcdpsLogManager/Logs/Filters/Groups/CategoryLogGroup.cs
index 25a6ac46..2f593742 100644
--- a/ArcdpsLogManager/Logs/Filters/Groups/CategoryLogGroup.cs
+++ b/ArcdpsLogManager/Logs/Filters/Groups/CategoryLogGroup.cs
@@ -27,6 +27,7 @@ public class CategoryLogGroup : LogGroup
{EncounterCategory.SpecialForcesTrainingArea, "Special Forces Training Area"},
{EncounterCategory.Other, "Uncategorized (PvE)"},
{EncounterCategory.Festival, "Festivals"},
+ {EncounterCategory.Map, "Instance logs"},
// All World vs. World names are defined within the WorldVersusWorldLogGroup.
};
diff --git a/ArcdpsLogManager/Logs/LogData.cs b/ArcdpsLogManager/Logs/LogData.cs
index 0645d5cd..293e6de3 100644
--- a/ArcdpsLogManager/Logs/LogData.cs
+++ b/ArcdpsLogManager/Logs/LogData.cs
@@ -295,11 +295,14 @@ public void ProcessLog(LogAnalytics logAnalytics)
}
LogExtras = new LogExtras();
- if (Encounter.GetEncounterCategory() == EncounterCategory.Fractal)
+
+ var mistlockInstabilities = logAnalytics.FractalInstabilityDetector.GetInstabilities(log).ToList();
+ if (Encounter.GetEncounterCategory() == EncounterCategory.Fractal || mistlockInstabilities.Count > 0 || log.FractalScale != null)
{
LogExtras.FractalExtras = new FractalExtras
{
- MistlockInstabilities = logAnalytics.FractalInstabilityDetector.GetInstabilities(log).ToList()
+ MistlockInstabilities = mistlockInstabilities,
+ FractalScale = log.FractalScale,
};
}
diff --git a/ArcdpsLogManager/Logs/Updates/LogDataUpdater.cs b/ArcdpsLogManager/Logs/Updates/LogDataUpdater.cs
index e0edb544..a8dfe8bb 100644
--- a/ArcdpsLogManager/Logs/Updates/LogDataUpdater.cs
+++ b/ArcdpsLogManager/Logs/Updates/LogDataUpdater.cs
@@ -33,24 +33,24 @@ public class LogDataUpdater
&& string.Compare(log.EvtcVersion, "EVTC20200609", StringComparison.OrdinalIgnoreCase) >= 0,
"Commander tag identification is now available."),
new LogUpdate(log => log.ParsingVersion < new Version(1, 0, 0, 0)
- && log.EncounterResult == EncounterResult.Failure,
+ && log.EncounterResult == EncounterResult.Failure,
"Add health percentage for failed logs"),
new LogUpdate(log => log.ParsingVersion < new Version(1, 0, 0, 0)
- && log.Encounter == Encounter.TwinLargos
- && log.EncounterResult == EncounterResult.Unknown,
+ && log.Encounter == Encounter.TwinLargos
+ && log.EncounterResult == EncounterResult.Unknown,
"Twin Largos logs had Unknown results if Kenut did not appear in the log."),
new LogUpdate(log => log.ParsingVersion < new Version(1, 0, 0, 0),
"Durations are significantly more accurate."),
new LogUpdate(log => log.ParsingVersion < new Version(1, 0, 0, 1)
- && log.Encounter == Encounter.Other,
+ && log.Encounter == Encounter.Other,
"Support for Ai, Keeper of the Peak"),
new LogUpdate(log => log.ParsingVersion < new Version(1, 0, 3, 1)
- && log.Encounter == Encounter.KeepConstruct,
+ && log.Encounter == Encounter.KeepConstruct,
"Adds Keep Construct CM detection."),
new LogUpdate(log => log.ParsingVersion < new Version(1, 0, 3, 2)
- && log.Encounter == Encounter.Xera
- && log.EncounterResult == EncounterResult.Failure
- && log.HealthPercentage > 0.998,
+ && log.Encounter == Encounter.Xera
+ && log.EncounterResult == EncounterResult.Failure
+ && log.HealthPercentage > 0.998,
"Twisted Castle logs may have been incorrectly identified as Xera."),
#pragma warning disable 618
new LogUpdate(log => log.ParsingVersion < new Version(1, 0, 3, 3)
@@ -83,53 +83,66 @@ x.Profession is Profession.Thief or Profession.Engineer or Profession.Ranger
&& log.Encounter.GetEncounterCategory() == EncounterCategory.Fractal,
"Adds Mistlock Instabilities for fractal log details, filters, and log list column."),
new LogUpdate(log => log.ParsingVersion < new Version(1, 4, 0, 1)
- && log.Encounter == Encounter.Other,
+ && log.Encounter == Encounter.Other,
"Add support for End of Dragons strike missions."),
new LogUpdate(log => log.ParsingVersion < new Version(1, 5, 0, 0)
- && log.Encounter == Encounter.AetherbladeHideout
- && log.GameBuild >= 127931,
+ && log.Encounter == Encounter.AetherbladeHideout
+ && log.GameBuild >= 127931,
"Add CM detection for Aetherblade Hideout."),
new LogUpdate(log => log.ParsingVersion < new Version(1, 5, 0, 1)
- && log.Encounter == Encounter.XunlaiJadeJunkyard
- && log.GameBuild >= 128773,
+ && log.Encounter == Encounter.XunlaiJadeJunkyard
+ && log.GameBuild >= 128773,
"Add CM detection for Xunlai Jade Junkyard."),
new LogUpdate(log => log.ParsingVersion < new Version(1, 5, 1, 1)
- && log.Encounter == Encounter.Other
- && log.MapId == MapIds.XunlaiJadeJunkyard
- && log.GameBuild >= 129355,
+ && log.Encounter == Encounter.Other
+ && log.MapId == MapIds.XunlaiJadeJunkyard
+ && log.GameBuild >= 129355,
"Add CM detection for Kaineng Overlook."),
new LogUpdate(log => log.ParsingVersion < new Version(1, 6, 0, 0)
- // Some raid enemies can be manually added and they would be categorized as Other.
- && (log.Encounter.IsRaid() || (log.MapId != null && MapIds.IsRaidMap(log.MapId.Value)))
- && log.GameBuild >= 130910,
+ // Some raid enemies can be manually added and they would be categorized as Other.
+ && (log.Encounter.IsRaid() || (log.MapId != null && MapIds.IsRaidMap(log.MapId.Value)))
+ && log.GameBuild >= 130910,
"Add Emboldened (easy) mode detection for raids."),
new LogUpdate(log => log.ParsingVersion < new Version(1, 6, 0, 1)
- // Some raid enemies can be manually added and they would be categorized as Other.
- && log.Encounter == Encounter.HarvestTemple
- && log.GameBuild >= 130910,
+ // Some raid enemies can be manually added and they would be categorized as Other.
+ && log.Encounter == Encounter.HarvestTemple
+ && log.GameBuild >= 130910,
"Add CM detection for Harvest Temple."),
new LogUpdate(log => log.ParsingVersion < new Version(1, 6, 0, 2)
- && log.Encounter == Encounter.AiKeeperOfThePeakDayAndNight
- && log.ParsingVersion >= new Version(1, 6, 0, 0),
+ && log.Encounter == Encounter.AiKeeperOfThePeakDayAndNight
+ && log.ParsingVersion >= new Version(1, 6, 0, 0),
"Fixes Ai logs always being categorized as both phases"),
new LogUpdate(log => log.ParsingVersion < new Version(1, 6, 0, 3)
- && log.Encounter == Encounter.HarvestTemple,
+ && log.Encounter == Encounter.HarvestTemple,
"Slightly improved health percentage display for Harvest Temple (16.66% per phase)"),
new LogUpdate(log => log.ParsingVersion < new Version(1, 7, 0, 0)
- && log.Encounter == Encounter.Other
- && log.MapId == MapIds.OldLionsCourt,
+ && log.Encounter == Encounter.Other
+ && log.MapId == MapIds.OldLionsCourt,
"Add support for Old Lion's Court"),
new LogUpdate(log => log.ParsingVersion < new Version(1, 7, 1, 0)
- && log.Encounter == Encounter.Other
- && log.MapId == MapIds.OldLionsCourt,
+ && log.Encounter == Encounter.Other
+ && log.MapId == MapIds.OldLionsCourt,
"Add support for Old Lion's Court CM"),
new LogUpdate(log => log.ParsingVersion < new Version(1, 7, 1, 1)
- && log.Encounter == Encounter.Other,
+ && log.Encounter == Encounter.Other,
"Add basic support for map logs"),
new LogUpdate(log => log.ParsingVersion < new Version(1, 7, 1, 2)
- && log.ParsingVersion >= new Version(1, 7, 1, 0)
- && log.Encounter == Encounter.OldLionsCourt,
+ && log.ParsingVersion >= new Version(1, 7, 1, 0)
+ && log.Encounter == Encounter.OldLionsCourt,
"Fix CM detection for Old Lion's Court"),
+ new LogUpdate(log => log.ParsingVersion < new Version(1, 8, 1, 0)
+ && log.Encounter == Encounter.Other
+ && log.MapId == MapIds.HarvestTemple,
+ "Add support for Harvest Temple logs triggered by Void Melters"),
+ new LogUpdate(log => log.ParsingVersion < new Version(1, 9, 0, 0)
+ && log.Encounter == Encounter.Other
+ && log.MapId == MapIds.SilentSurf,
+ "Add support for Silent Surf CM"),
+ new LogUpdate(log => log.ParsingVersion < new Version(1, 9, 0, 1)
+ // There may be fractal encounters that are added manually and are not supported, so we also go through other logs.
+ && (log.Encounter == Encounter.Other || log.Encounter.GetEncounterCategory() == EncounterCategory.Fractal)
+ && string.Compare(log.EvtcVersion, "EVTC20230716", StringComparison.OrdinalIgnoreCase) >= 0,
+ "Add support for fractal scale."),
// When adding a new update, you need to increase the revision (last value) of the version in the .csproj file
// unless the version changes more significantly, in that case it can be reset to 0.
};
diff --git a/ArcdpsLogManager/ManagerForm.cs b/ArcdpsLogManager/ManagerForm.cs
index e852ada5..02117e8c 100644
--- a/ArcdpsLogManager/ManagerForm.cs
+++ b/ArcdpsLogManager/ManagerForm.cs
@@ -494,6 +494,7 @@ private TabControl ConstructMainTabControl()
// Statistics
var statistics = new StatisticsSection(ImageProvider, LogNameProvider);
FilteredLogsUpdated += (sender, args) => statistics.UpdateDataFromLogs(logsFiltered.ToList());
+ LogNameProvider.MapNamesUpdated += (_, _) => statistics.UpdateDataFromLogs(logsFiltered.ToList());
// Game data collecting
var gameDataCollecting = new GameDataCollecting(logList, LogCache, ApiData, LogDataProcessor, UploadProcessor, ImageProvider, LogNameProvider);
diff --git a/ArcdpsLogManager/Processing/ApiProcessor.cs b/ArcdpsLogManager/Processing/ApiProcessor.cs
index f684001b..f86b6e28 100644
--- a/ArcdpsLogManager/Processing/ApiProcessor.cs
+++ b/ArcdpsLogManager/Processing/ApiProcessor.cs
@@ -50,6 +50,12 @@ protected override async Task Process(string item, CancellationToken cancellatio
notFound = true;
retry = false;
}
+ catch (ServerErrorException)
+ {
+ // Currently a bug in the gw2 api. It is throwing "unknown error" for not existent guilds
+ notFound = true;
+ retry = false;
+ }
} while (retry);
if (guild != null)
diff --git a/ArcdpsLogManager/Resources.cs b/ArcdpsLogManager/Resources.cs
index 45fe7741..b509f2bf 100644
--- a/ArcdpsLogManager/Resources.cs
+++ b/ArcdpsLogManager/Resources.cs
@@ -78,6 +78,7 @@ public static Icon GetProgramIcon()
public static Image GetTinyIconFestival() => GetImage("ArenaNet.festivals_32px.png");
public static Image GetTinyIconIcebroodSaga() => GetImage("ArenaNet.icebrood_saga_32px.png");
public static Image GetTinyIconEndOfDragons() => GetImage("ArenaNet.end_of_dragons_32px.png");
+ public static Image GetTinyIconInstance() => GetImage("ArenaNet.storyline_32px.png");
// FRACTAL INSTABILITIES
public static Image GetInstabilityImage(string iconName)
diff --git a/ArcdpsLogManager/Sections/GameDataGathering.cs b/ArcdpsLogManager/Sections/GameDataGathering.cs
index 96d4f7eb..a641b389 100644
--- a/ArcdpsLogManager/Sections/GameDataGathering.cs
+++ b/ArcdpsLogManager/Sections/GameDataGathering.cs
@@ -14,6 +14,7 @@
using GW2Scratch.EVTCAnalytics.Model;
using GW2Scratch.EVTCAnalytics.Model.Agents;
using GW2Scratch.EVTCAnalytics.Processing;
+using System.Collections.Concurrent;
namespace GW2Scratch.ArcdpsLogManager.Sections
{
@@ -357,12 +358,12 @@ private void GatherData(LogList logList, ProgressBar progressBar, Label progress
{
return Task.Run(() =>
{
- var species = new Dictionary>>();
- var skills = new Dictionary>>();
+ var species = new ConcurrentDictionary>>();
+ var skills = new ConcurrentDictionary>>();
int done = 0;
int failed = 0;
- foreach (var log in logs)
+ Parallel.ForEach(logs, log =>
{
cancellationToken.ThrowIfCancellationRequested();
@@ -374,8 +375,8 @@ private void GatherData(LogList logList, ProgressBar progressBar, Label progress
}
catch
{
- failed++;
- continue;
+ Interlocked.Increment(ref failed);
+ return;
}
foreach (var agent in processedLog.Agents.OfType())
@@ -387,17 +388,11 @@ private void GatherData(LogList logList, ProgressBar progressBar, Label progress
if (id == 0) continue;
var speciesData = new SpeciesData(id, name);
- if (!species.ContainsKey(id))
- {
- species[id] = new Dictionary>();
- }
-
- if (!species[id].ContainsKey(speciesData))
- {
- species[id][speciesData] = new List();
- }
+ var dictForSpecies =
+ species.GetOrAdd(id, new ConcurrentDictionary>());
- species[id][speciesData].Add(log);
+ var listForSpeciesData = dictForSpecies.GetOrAdd(speciesData, new List());
+ listForSpeciesData.Add(log);
}
foreach (var skill in processedLog.Skills)
@@ -409,22 +404,15 @@ private void GatherData(LogList logList, ProgressBar progressBar, Label progress
if (id == 0) continue;
var skillData = new SkillData(id, name);
- if (!skills.ContainsKey(id))
- {
- skills[id] = new Dictionary>();
- }
+ var dictForSkill = skills.GetOrAdd(id, new ConcurrentDictionary>());
- if (!skills[id].ContainsKey(skillData))
- {
- skills[id][skillData] = new List();
- }
-
- skills[id][skillData].Add(log);
+ var listForSkillData = dictForSkill.GetOrAdd(skillData, new List());
+ listForSkillData.Add(log);
}
- done++;
+ Interlocked.Increment(ref done);
progress?.Report((done, logs.Count, failed));
- }
+ });
var speciesEnumerable = (IEnumerable) species.Values.SelectMany(x => x).Select(x =>
{
diff --git a/ArcdpsLogManager/Sections/LogList.cs b/ArcdpsLogManager/Sections/LogList.cs
index b843afd3..b936bc8d 100644
--- a/ArcdpsLogManager/Sections/LogList.cs
+++ b/ArcdpsLogManager/Sections/LogList.cs
@@ -42,6 +42,7 @@ public class LogList : Panel
{ "★", "Favorite" },
{ "CM", "Challenge Mode or Emboldened" },
{ "Instabilities", "Fractals of the Mists" },
+ { "Scale", "Fractals of the Mists" },
};
public bool ReadOnly { get; init; }
@@ -217,6 +218,16 @@ private GridView ConstructLogGridView(LogDetailPanel detailPanel, Multi
};
gridView.Columns.Add(resultColumn);
+ var fractalScaleColumn = new GridColumn
+ {
+ HeaderText = "Scale",
+ DataCell = new TextBoxCell
+ {
+ Binding = new DelegateBinding(x => x.LogExtras?.FractalExtras?.FractalScale?.ToString() ?? "")
+ }
+ };
+ gridView.Columns.Add(fractalScaleColumn);
+
var instabilityCell = new DrawableCell();
instabilityCell.Paint += (sender, args) =>
{
@@ -245,7 +256,7 @@ private GridView ConstructLogGridView(LogDetailPanel detailPanel, Multi
Width = 3 * (PlayerIconSize + PlayerIconSpacing)
};
gridView.Columns.Add(instabilityColumn);
-
+
var dateColumn = new GridColumn()
{
HeaderText = "Date",
diff --git a/EVTCAnalytics.Benchmark/GW2Scratch.EVTCAnalytics.Benchmark.csproj b/EVTCAnalytics.Benchmark/GW2Scratch.EVTCAnalytics.Benchmark.csproj
index 3f8a3093..97044098 100644
--- a/EVTCAnalytics.Benchmark/GW2Scratch.EVTCAnalytics.Benchmark.csproj
+++ b/EVTCAnalytics.Benchmark/GW2Scratch.EVTCAnalytics.Benchmark.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/EVTCAnalytics/Events/AgentEvents.cs b/EVTCAnalytics/Events/AgentEvents.cs
index dec11839..85b7a430 100644
--- a/EVTCAnalytics/Events/AgentEvents.cs
+++ b/EVTCAnalytics/Events/AgentEvents.cs
@@ -150,10 +150,19 @@ public AgentMaxHealthUpdateEvent(long time, Agent agent, ulong newMaxHealth) : b
public class AgentTagEvent : AgentEvent
{
public Marker Marker { get; }
+
+ ///
+ /// True if this is a commander tag.
+ ///
+ ///
+ /// Introduced in EVTC20220823.
+ ///
+ public bool? IsCommander { get; }
- public AgentTagEvent(long time, Agent agent, Marker marker) : base(time, agent)
+ public AgentTagEvent(long time, Agent agent, Marker marker, bool? isCommander) : base(time, agent)
{
Marker = marker;
+ IsCommander = isCommander;
}
}
@@ -165,7 +174,7 @@ public class InitialBuffEvent : BuffApplyEvent
public InitialBuffEvent(long time, Agent agent, Skill buff, Agent sourceAgent, int durationApplied,
uint durationOfRemovedStack) : base(time, agent, buff, sourceAgent, durationApplied, durationOfRemovedStack) {
- }
+ }
}
///
@@ -321,7 +330,10 @@ public DefianceBarStateUpdateEvent(long time, Agent agent, DefianceBarState stat
/// An event specifying that an effect was created.
///
///
- /// Introduced in EVTC20220602.
+ /// Introduced in EVTC20220602. Retired in EVTC20230718.
+ ///
+ /// Meaning of some values silently changed sometime in-between without any mention in the changelog.
+ /// Notably, ZAxisOrientationOnly likely changed meaning.
///
public class EffectEvent : AgentEvent
{
@@ -374,4 +386,81 @@ public EffectEvent(long time, Agent agent, Effect effect, Agent agentTarget, flo
Duration = duration;
}
}
+
+ ///
+ /// An event specifying that an effect was created.
+ ///
+ ///
+ /// Introduced in EVTC20220602.
+ ///
+ public class EffectStartEvent : Event
+ {
+ ///
+ /// The owner of this effect.
+ ///
+ public Agent EffectOwner { get; }
+
+ ///
+ /// The Effect created.
+ ///
+ public Effect Effect { get; }
+
+ ///
+ /// The this effect is anchored to if anchored. May be .
+ ///
+ ///
+ public Agent AgentTarget { get; }
+
+ ///
+ /// Position (x, y, z) of the effect when not anchored to a target. May be .
+ ///
+ ///
+ public float[] Position { get; }
+
+ ///
+ /// The orientation (x, y, z) of the effect.
+ ///
+ public ushort[] Orientation { get; }
+
+ ///
+ /// The duration of the effect in milliseconds.
+ ///
+ public ushort Duration { get; }
+
+ ///
+ /// Trackable id of this effect. Used for pairing with a corresponding EffectEndEvent.
+ ///
+ public uint TrackableId { get; }
+
+ public EffectStartEvent(long time, Agent effectOwner, Effect effect, Agent agentTarget, float[] position,
+ ushort[] orientation, ushort duration, uint trackableId) : base(time)
+ {
+ EffectOwner = effectOwner;
+ Effect = effect;
+ AgentTarget = agentTarget;
+ Position = position;
+ Orientation = orientation;
+ Duration = duration;
+ TrackableId = trackableId;
+ }
+ }
+
+ ///
+ /// An event specifying that an effect ended.
+ ///
+ ///
+ /// Introduced in EVTC20220602.
+ ///
+ public class EffectEndEvent : Event
+ {
+ ///
+ /// Trackable id of this effect. Used for pairing with a corresponding EffectStartEvent.
+ ///
+ public uint TrackableId { get; }
+
+ public EffectEndEvent(long time, uint trackableId) : base(time)
+ {
+ TrackableId = trackableId;
+ }
+ }
}
\ No newline at end of file
diff --git a/EVTCAnalytics/GameData/Encounters/Encounter.cs b/EVTCAnalytics/GameData/Encounters/Encounter.cs
index 351ebc34..c1b2bda7 100644
--- a/EVTCAnalytics/GameData/Encounters/Encounter.cs
+++ b/EVTCAnalytics/GameData/Encounters/Encounter.cs
@@ -87,6 +87,9 @@ public enum Encounter
AiKeeperOfThePeakDayOnly = 10022,
AiKeeperOfThePeakNightOnly = 10023,
AiKeeperOfThePeakDayAndNight = 10024,
+
+ // Fractals - Silent Surf
+ Kanaxai = 10031,
// Festivals - Wintersday
Freezie = 20001,
diff --git a/EVTCAnalytics/GameData/Encounters/EncounterCategories.cs b/EVTCAnalytics/GameData/Encounters/EncounterCategories.cs
index 8c2c56b5..6f30ee0e 100644
--- a/EVTCAnalytics/GameData/Encounters/EncounterCategories.cs
+++ b/EVTCAnalytics/GameData/Encounters/EncounterCategories.cs
@@ -50,6 +50,7 @@ public static class EncounterCategories
{Encounter.AiKeeperOfThePeakDayOnly, EncounterCategory.Fractal},
{Encounter.AiKeeperOfThePeakNightOnly, EncounterCategory.Fractal},
{Encounter.AiKeeperOfThePeakDayAndNight, EncounterCategory.Fractal},
+ {Encounter.Kanaxai, EncounterCategory.Fractal},
{Encounter.Freezie, EncounterCategory.StrikeMissionFestival},
{Encounter.StandardKittyGolem, EncounterCategory.SpecialForcesTrainingArea},
{Encounter.MediumKittyGolem, EncounterCategory.SpecialForcesTrainingArea},
diff --git a/EVTCAnalytics/GameData/Encounters/EncounterNames.cs b/EVTCAnalytics/GameData/Encounters/EncounterNames.cs
index f427c3ef..2ed17e20 100644
--- a/EVTCAnalytics/GameData/Encounters/EncounterNames.cs
+++ b/EVTCAnalytics/GameData/Encounters/EncounterNames.cs
@@ -48,6 +48,7 @@ public static class EncounterNames
{Encounter.AiKeeperOfThePeakDayOnly, "Ai, Keeper of the Peak – Elemental"},
{Encounter.AiKeeperOfThePeakNightOnly, "Ai, Keeper of the Peak – Dark"},
{Encounter.AiKeeperOfThePeakDayAndNight, "Ai, Keeper of the Peak – Both Phases"},
+ {Encounter.Kanaxai, "Kanaxai, Scythe of House Aurkus"},
{Encounter.Freezie, "Freezie"},
{Encounter.StandardKittyGolem, "Standard Kitty Golem"},
{Encounter.MediumKittyGolem, "Medium Kitty Golem"},
diff --git a/EVTCAnalytics/GameData/MapIds.cs b/EVTCAnalytics/GameData/MapIds.cs
index cff32a72..f77b3149 100644
--- a/EVTCAnalytics/GameData/MapIds.cs
+++ b/EVTCAnalytics/GameData/MapIds.cs
@@ -19,6 +19,8 @@ public static class MapIds
public const int XunlaiJadeJunkyard = 1451;
public const int OldLionsCourt = 1485;
+ public const int HarvestTemple = 1437;
+ public const int SilentSurf = 1500;
public static bool IsRaidMap(int id)
{
diff --git a/EVTCAnalytics/GameData/SpeciesIds.cs b/EVTCAnalytics/GameData/SpeciesIds.cs
index b86d76dc..817221d3 100644
--- a/EVTCAnalytics/GameData/SpeciesIds.cs
+++ b/EVTCAnalytics/GameData/SpeciesIds.cs
@@ -101,6 +101,9 @@ public static class SpeciesIds
public const int Arkk = 17759;
// Sunqua Peak
public const int AiKeeperOfThePeak = 23254;
+ // Silent Surf
+ public const int KanaxaiNM = 25572;
+ public const int KanaxaiCM = 25577;
// Festivals
public const int Freezie = 21333;
diff --git a/EVTCAnalytics/Model/Log.cs b/EVTCAnalytics/Model/Log.cs
index f4be97fc..6ed70a07 100644
--- a/EVTCAnalytics/Model/Log.cs
+++ b/EVTCAnalytics/Model/Log.cs
@@ -110,6 +110,14 @@ public class Log
/// Provides a numeric ID of the map the encounter occured in.
///
public int? MapId { get; }
+
+ ///
+ /// Provides the number of the fractal scale of the encounter.
+ ///
+ ///
+ /// Introduced in arcdps 20230718.
+ ///
+ public int? FractalScale { get; }
///
/// Provides access to encounter-specific data and definitions.
@@ -120,7 +128,7 @@ public class Log
/// Provides the start time of the recorded instance if available.
///
///
- /// Added in arcdps 20211214.
+ /// Introduced in arcdps 20211214.
///
public InstanceStart InstanceStart { get; }
@@ -128,7 +136,7 @@ public class Log
/// Provides a list of all errors logged by arcdps.
///
///
- /// Added in arcdps 20200513.
+ /// Introduced in arcdps 20200513.
///
public IReadOnlyList Errors { get; }
@@ -150,6 +158,7 @@ internal Log(LogProcessorState state)
GameLanguage = state.GameLanguage;
GameBuild = state.GameBuild;
GameShardId = state.GameShardId;
+ FractalScale = state.FractalScale;
MapId = state.MapId;
Events = state.Events;
Agents = state.Agents;
@@ -167,7 +176,7 @@ internal Log(Agent mainTarget, LogType logType, IEnumerable events, IEnum
IEnumerable skills, IEnumerable effects, IEnumerable markers, IEnumerable errors,
IEncounterData encounterData, GameLanguage gameLanguage, string evtcVersion, LogTime startTime,
LogTime endTime, Player pointOfView, int? language, int? gameBuild, int? gameShardId, int? mapId,
- InstanceStart instanceStart)
+ InstanceStart instanceStart, int? fractalScale)
{
MainTarget = mainTarget;
LogType = logType;
@@ -182,6 +191,7 @@ internal Log(Agent mainTarget, LogType logType, IEnumerable events, IEnum
GameShardId = gameShardId;
MapId = mapId;
InstanceStart = instanceStart;
+ FractalScale = fractalScale;
Events = events as Event[] ?? events.ToArray();
Agents = agents as Agent[] ?? agents.ToArray();
Skills = skills as Skill[] ?? skills.ToArray();
diff --git a/EVTCAnalytics/Model/Skills/BuffCategory.cs b/EVTCAnalytics/Model/Skills/BuffCategory.cs
new file mode 100644
index 00000000..b828a80d
--- /dev/null
+++ b/EVTCAnalytics/Model/Skills/BuffCategory.cs
@@ -0,0 +1,14 @@
+namespace GW2Scratch.EVTCAnalytics.Model.Skills;
+
+public enum BuffCategory
+{
+ Boon = 0,
+ Any = 1,
+ Condition = 2,
+ Food = 4,
+ Upgrade = 6,
+ Boost = 8,
+ Trait = 11,
+ Enhancement = 13,
+ Stance = 16
+}
\ No newline at end of file
diff --git a/EVTCAnalytics/Model/Skills/BuffData.cs b/EVTCAnalytics/Model/Skills/BuffData.cs
new file mode 100644
index 00000000..c30ac92b
--- /dev/null
+++ b/EVTCAnalytics/Model/Skills/BuffData.cs
@@ -0,0 +1,28 @@
+namespace GW2Scratch.EVTCAnalytics.Model.Skills;
+
+public class BuffData
+{
+ ///
+ /// Is probably Invulnerability.
+ ///
+ public bool IsInvulnerability { get; internal set; }
+
+ ///
+ /// Is probably damage inversion.
+ ///
+ public bool IsInversion { get; internal set; }
+
+ ///
+ /// Is probably Resistance.
+ ///
+ ///
+ /// Added with EVTC20200428.
+ ///
+ public bool? IsResistance { get; internal set; }
+
+ public BuffCategory Category { get; internal set; }
+
+ public byte StackingType { get; internal set; }
+ public uint MaxStacks { get; internal set; }
+ public uint DurationCap { get; internal set; }
+}
\ No newline at end of file
diff --git a/EVTCAnalytics/Model/Skills/Skill.cs b/EVTCAnalytics/Model/Skills/Skill.cs
index 4c9e7510..f150c0c8 100644
--- a/EVTCAnalytics/Model/Skills/Skill.cs
+++ b/EVTCAnalytics/Model/Skills/Skill.cs
@@ -22,7 +22,25 @@ public class Skill
/// It is very common for skills to not have a name other than the numerical ID.
///
public string Name { get; }
+
+ ///
+ /// Contains skill data.
+ ///
+ ///
+ /// This data was first added with EVTC20191225.
+ /// May be null for older versions and for buffs.
+ ///
+ public SkillData SkillData { get; internal set; }
+ ///
+ /// Contains buff data.
+ ///
+ ///
+ /// This data was first added with EVTC20191225.
+ /// May be null for older versions and for abilities.
+ ///
+ public BuffData BuffData { get; internal set; }
+
///
/// Creates a new instance of a .
///
diff --git a/EVTCAnalytics/Model/Skills/SkillAttunement.cs b/EVTCAnalytics/Model/Skills/SkillAttunement.cs
deleted file mode 100644
index ba727964..00000000
--- a/EVTCAnalytics/Model/Skills/SkillAttunement.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace GW2Scratch.EVTCAnalytics.Model.Skills
-{
- ///
- /// Represents a skill attunement, as used by the Elementalist profession.
- ///
- public enum SkillAttunement
- {
- None,
- Fire,
- Air,
- Water,
- Earth,
- Other
- }
-}
\ No newline at end of file
diff --git a/EVTCAnalytics/Model/Skills/SkillData.cs b/EVTCAnalytics/Model/Skills/SkillData.cs
index 0347dc0b..f48dbae1 100644
--- a/EVTCAnalytics/Model/Skills/SkillData.cs
+++ b/EVTCAnalytics/Model/Skills/SkillData.cs
@@ -1,73 +1,9 @@
-using System.Collections.Generic;
-using System.Linq;
-using GW2Scratch.EVTCAnalytics.Model.Agents;
+namespace GW2Scratch.EVTCAnalytics.Model.Skills;
-namespace GW2Scratch.EVTCAnalytics.Model.Skills
+public class SkillData
{
- ///
- /// Data associated with a as it may be acquired from the official Guild Wars 2 API.
- ///
- public class SkillData
- {
- ///
- /// The numeric ID of the skill.
- ///
- public int Id { get; }
-
- ///
- /// The localized name of the skill.
- ///
- public string Name { get; }
-
- ///
- /// A URL to an icon representing the skill.
- ///
- public string IconUrl { get; }
-
- ///
- /// The type of the skill
- ///
- public SkillType Type { get; }
-
- ///
- /// The type of weapon associated with this skill.
- ///
- public WeaponType WeaponType { get; }
-
- ///
- /// A list of s with access to this skill.
- ///
- public IEnumerable Professions { get; }
-
- ///
- /// The slot this skill appears in.
- ///
- public SkillSlot Slot { get; }
-
- ///
- /// The attunement this skill is associated with.
- ///
- public SkillAttunement Attunement { get; }
-
- /* TODO: This makes serialization significantly more difficult (although doable)
- public SkillData NextChain { get; internal set; }
- public SkillData PrevChain { get; internal set; }
- */
-
- ///
- /// Creates a new instance of .
- ///
- public SkillData(int id, string name, string iconUrl, SkillType type, WeaponType weaponType,
- IEnumerable professions, SkillSlot slot, SkillAttunement attunement)
- {
- Id = id;
- Name = name;
- IconUrl = iconUrl;
- Type = type;
- WeaponType = weaponType;
- Professions = professions.ToArray();
- Slot = slot;
- Attunement = attunement;
- }
- }
+ public float Recharge { get; internal set; }
+ public float Range0 { get; internal set; }
+ public float Range1 { get; internal set; }
+ public float TooltipTime { get; internal set; }
}
\ No newline at end of file
diff --git a/EVTCAnalytics/Model/Skills/SkillSlot.cs b/EVTCAnalytics/Model/Skills/SkillSlot.cs
deleted file mode 100644
index 52135514..00000000
--- a/EVTCAnalytics/Model/Skills/SkillSlot.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-namespace GW2Scratch.EVTCAnalytics.Model.Skills
-{
- ///
- /// A slot for skills used by players in the game.
- ///
- ///
- ///
- /// Some slots may only be accessible for certain s.
- ///
- ///
- /// A player may have access to multiple slots of the same kind (notably the slot).
- ///
- ///
- public enum SkillSlot
- {
- Weapon1,
- Weapon2,
- Weapon3,
- Weapon4,
- Weapon5,
- Profession1,
- Profession2,
- Profession3,
- Profession4,
- Profession5,
- Downed1,
- Downed2,
- Downed3,
- Downed4,
- Utility,
- Heal,
- Elite,
- Toolbelt,
- Pet,
- Other,
- None
- }
-}
\ No newline at end of file
diff --git a/EVTCAnalytics/Model/Skills/SkillType.cs b/EVTCAnalytics/Model/Skills/SkillType.cs
deleted file mode 100644
index f015facf..00000000
--- a/EVTCAnalytics/Model/Skills/SkillType.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace GW2Scratch.EVTCAnalytics.Model.Skills
-{
- ///
- /// The type of a by its accessibility.
- ///
- public enum SkillType
- {
- None,
- Bundle,
- Elite,
- Heal,
- Profession,
- Utility,
- Weapon,
- Other,
- }
-}
\ No newline at end of file
diff --git a/EVTCAnalytics/Parsed/Enums/StateChange.cs b/EVTCAnalytics/Parsed/Enums/StateChange.cs
index 3c7c2d88..f0b292b0 100644
--- a/EVTCAnalytics/Parsed/Enums/StateChange.cs
+++ b/EVTCAnalytics/Parsed/Enums/StateChange.cs
@@ -50,7 +50,12 @@ public enum StateChange : byte
InstanceStart = 42,
TickRate = 43,
Last90BeforeDown = 44,
- Effect = 45,
+ Effect = 45, // Not used since 20230716
IdToGuid = 46,
+ LogNPCUpdate = 47,
+ IdleEvent = 48, // Internal in arcdps; shouldn't appear
+ ExtensionCombat = 49, // Intended for extensions to indicate used skills for the skill table
+ FractalScale = 50,
+ Effect2 = 51, // Replaces Effect since 20230716
};
}
\ No newline at end of file
diff --git a/EVTCAnalytics/Processing/DefaultEncounterIdentifier.cs b/EVTCAnalytics/Processing/DefaultEncounterIdentifier.cs
index 7cd3645c..7ae83a9d 100644
--- a/EVTCAnalytics/Processing/DefaultEncounterIdentifier.cs
+++ b/EVTCAnalytics/Processing/DefaultEncounterIdentifier.cs
@@ -649,6 +649,13 @@ private IEncounterData GetPvEEncounterData(Agent mainTarget, IReadOnlyList agents
}
}
}
+ case SpeciesIds.KanaxaiCM:
+ return Encounter.Kanaxai;
case SpeciesIds.Freezie:
return Encounter.Freezie;
case SpeciesIds.IcebroodConstruct:
@@ -833,6 +842,7 @@ public Encounter IdentifyEncounter(Agent mainTarget, IReadOnlyList agents
case SpeciesIds.MinisterLiChallengeMode:
return Encounter.KainengOverlook;
case SpeciesIds.VoidAmalgamate:
+ case SpeciesIds.VoidMelter:
return Encounter.HarvestTemple;
case SpeciesIds.PrototypeVermillion:
case SpeciesIds.PrototypeArsenite:
diff --git a/EVTCAnalytics/Processing/LogProcessor.cs b/EVTCAnalytics/Processing/LogProcessor.cs
index c359f3d4..3fb71e8f 100644
--- a/EVTCAnalytics/Processing/LogProcessor.cs
+++ b/EVTCAnalytics/Processing/LogProcessor.cs
@@ -745,6 +745,50 @@ private void ProcessCombatItem(LogProcessorState state, in ParsedCombatItem item
// Only used for master assignment
// Contains if the attack target is targetable as the value.
return;
+ case StateChange.BuffInfo:
+ {
+ if (state.SkillsById.TryGetValue(item.SkillId, out var skill))
+ {
+ bool isResistanceAvailable = string.Compare(state.EvtcVersion, "EVTC20200428", StringComparison.OrdinalIgnoreCase) >= 0;
+
+ Span padding = stackalloc byte[4];
+ BitConverter.TryWriteBytes(padding, item.Padding);
+
+ skill.BuffData = new BuffData
+ {
+ IsInversion = item.IsShields != 0,
+ IsInvulnerability = item.IsFlanking != 0,
+ IsResistance = isResistanceAvailable ? padding[1] != 0 : null,
+ Category = (BuffCategory) item.IsOffCycle,
+ StackingType = padding[0],
+ MaxStacks = item.SrcMasterId,
+ DurationCap = item.OverstackValue
+ };
+
+ }
+
+ return;
+ }
+ case StateChange.SkillInfo:
+ {
+ // (float*)&time[4]
+ // recharge range0 range1 tooltiptime
+ Span bytes = stackalloc byte[16];
+ BitConverter.TryWriteBytes(bytes[..8], item.Time);
+ BitConverter.TryWriteBytes(bytes[8..16], item.SrcAgent);
+ if (state.SkillsById.TryGetValue(item.SkillId, out var skill))
+ {
+ skill.SkillData = new SkillData
+ {
+ Recharge = BitConverter.ToSingle(bytes[..4]),
+ Range0 = BitConverter.ToSingle(bytes[4..8]),
+ Range1 = BitConverter.ToSingle(bytes[8..12]),
+ TooltipTime = BitConverter.ToSingle(bytes[12..16]),
+ };
+ }
+
+ return;
+ }
case StateChange.InstanceStart:
state.InstanceStart = new InstanceStart(item.SrcAgent);
return;
@@ -783,6 +827,9 @@ private void ProcessCombatItem(LogProcessorState state, in ParsedCombatItem item
state.Errors.Add(new LogError(errorString));
return;
+ case StateChange.FractalScale:
+ state.FractalScale = (int) item.SrcAgent;
+ return;
default:
{
var processedEvent = GetEvent(state, item);
@@ -960,12 +1007,8 @@ Skill GetSkillByIdOrAdd(uint id)
float breakbarHealthFraction = BitConversions.ToSingle(item.Value);
return new DefianceBarHealthUpdateEvent(item.Time, GetAgentByAddress(item.SrcAgent),
breakbarHealthFraction);
- case StateChange.BuffInfo:
- // TODO: Figure out what the contents are
case StateChange.BuffFormula:
- // TODO: Figure out what the contents are
- case StateChange.SkillInfo:
- // TODO: Figure out what the contents are
+ // TODO: Figure out what the contents are
case StateChange.SkillTiming:
// TODO: Figure out what the contents are
return new UnknownEvent(item.Time, item);
@@ -976,8 +1019,16 @@ Skill GetSkillByIdOrAdd(uint id)
marker = new Marker(markerId);
state.MarkersById[markerId] = marker;
}
+
+ // Added in aug.23.2022
+ bool isCommanderAvailable = string.Compare(state.EvtcVersion, "EVTC20220823", StringComparison.OrdinalIgnoreCase) >= 0;
+ bool? isCommander = isCommanderAvailable switch
+ {
+ true => item.Buff != 0,
+ false => null,
+ };
- return new AgentTagEvent(item.Time, GetAgentByAddress(item.SrcAgent), marker);
+ return new AgentTagEvent(item.Time, GetAgentByAddress(item.SrcAgent), marker, isCommander);
case StateChange.BarrierUpdate:
var barrierFraction = item.DstAgent / 10000f;
return new BarrierUpdateEvent(item.Time, GetAgentByAddress(item.SrcAgent), barrierFraction);
@@ -995,9 +1046,12 @@ Skill GetSkillByIdOrAdd(uint id)
case StateChange.Last90BeforeDown:
return new UnknownEvent(item.Time, item);
case StateChange.Effect:
+ {
+ // Note that the meaning of fields silently changed at some point (notably, is_flanking).
+
// src_agent effect master.
Agent master = GetAgentByAddress(item.SrcAgent);
-
+
// skillid = effectid,
uint effectId = item.SkillId;
if (!state.EffectsById.TryGetValue(effectId, out Effect effect))
@@ -1005,7 +1059,7 @@ Skill GetSkillByIdOrAdd(uint id)
effect = new Effect(effectId);
state.EffectsById[effectId] = effect;
}
-
+
// dst_agent if around dst,
// else value/buffdmg/overstack = float[3] xyz,
// &iff = float[2] xy orient,
@@ -1014,34 +1068,34 @@ Skill GetSkillByIdOrAdd(uint id)
float[] position = null;
float[] orientation = new float[3];
-
+
if (item.DstAgent == 0)
{
position = new float[3];
- position[0] = BitConversions.ToSingle(item.Value); // x
- position[1] = BitConversions.ToSingle(item.BuffDmg); // y
+ position[0] = BitConversions.ToSingle(item.Value); // x
+ position[1] = BitConversions.ToSingle(item.BuffDmg); // y
position[2] = BitConversions.ToSingle(item.OverstackValue); // z
}
-
+
// iff + buff + result + is_activation = x orientation
Span xOrientationBytes = stackalloc byte[4];
xOrientationBytes[0] = (byte) item.Iff;
xOrientationBytes[1] = item.Buff;
xOrientationBytes[2] = (byte) item.Result;
xOrientationBytes[3] = (byte) item.IsActivation;
-
+
// is_buffremove + is_ninety + is_fifty + is_moving = y orientation
Span yOrientationBytes = stackalloc byte[4];
yOrientationBytes[0] = (byte) item.IsBuffRemove;
yOrientationBytes[1] = item.IsNinety;
yOrientationBytes[2] = item.IsFifty;
yOrientationBytes[3] = item.IsMoving;
-
+
// &is_shields = uint16 duration,
Span durationBytes = stackalloc byte[2];
durationBytes[0] = item.IsShields;
durationBytes[1] = item.IsOffCycle;
-
+
orientation[0] = BitConverter.ToSingle(xOrientationBytes);
orientation[1] = BitConverter.ToSingle(yOrientationBytes);
orientation[2] = BitConversions.ToSingle(item.Padding);
@@ -1050,8 +1104,91 @@ Skill GetSkillByIdOrAdd(uint id)
// is_flanking = only z orient
bool zOrientationOnly = item.IsFlanking > 0;
- return new EffectEvent(item.Time, master, effect, aroundAgent, position, orientation,
- zOrientationOnly, duration);
+ return new EffectEvent(item.Time, master, effect, aroundAgent, position, orientation, zOrientationOnly, duration);
+ }
+ case StateChange.LogNPCUpdate:
+ // TODO: implement
+ return new UnknownEvent(item.Time, item);
+ case StateChange.IdleEvent:
+ return new UnknownEvent(item.Time, item);
+ case StateChange.ExtensionCombat:
+ // Not sure if this ever appears in logs.
+ return new UnknownEvent(item.Time, item);
+ case StateChange.Effect2:
+ {
+ // Official docs as of 2023-07-18:
+ // src_agent is owner. dst_agent if at agent, else &value = float[3] xyz.
+ // &iff = uint32 duration.
+ // &buffremove = uint32 trackable id.
+ // &is_shields = int16[3] orientation, value ranges from -31415 (pi) to +31415 or int16 MIN/MAX if out of those bounds
+
+ // Official docs as of 2023-07-20:
+ // src_agent is owner. dst_agent if at agent, else &value = float[3] xyz.
+ // &iff = uint32 duraation.
+ // &buffremove = uint32 trackable id.
+ // &is_shields = int16[3] orientation, values are original*1000 clamped to int16 (not in realtime api)
+
+ // src_agent is owner of the effect.
+ Agent master = GetAgentByAddress(item.SrcAgent);
+
+ // is_buffremove + is_ninety + is_fifty + is_moving = trackable id
+ Span trackableIdBytes = stackalloc byte[4];
+ trackableIdBytes[0] = (byte) item.IsBuffRemove;
+ trackableIdBytes[1] = item.IsNinety;
+ trackableIdBytes[2] = item.IsFifty;
+ trackableIdBytes[3] = item.IsMoving;
+ uint trackableId = BitConverter.ToUInt32(trackableIdBytes);
+
+ // skillid = effectid (not documented)
+ uint effectId = item.SkillId;
+ Effect effect;
+ if (effectId != 0)
+ {
+ if (!state.EffectsById.TryGetValue(effectId, out effect))
+ {
+ effect = new Effect(effectId);
+ state.EffectsById[effectId] = effect;
+ }
+ }
+ else
+ {
+ // TODO: retrieve effect from a list of ongoing effects
+ return new EffectEndEvent(item.Time, trackableId);
+ }
+
+ // dst_agent if around dst,
+ Agent aroundAgent = GetAgentByAddress(item.DstAgent);
+
+ float[] position = null;
+ ushort[] orientation = new ushort[3];
+
+ // else &value = float[3] xyz
+ if (item.DstAgent == 0)
+ {
+ position = new float[3];
+ position[0] = BitConversions.ToSingle(item.Value); // x
+ position[1] = BitConversions.ToSingle(item.BuffDmg); // y
+ position[2] = BitConversions.ToSingle(item.OverstackValue); // z
+ }
+
+ // iff + buff + result + is_activation = duration
+ Span durationBytes = stackalloc byte[4];
+ durationBytes[0] = (byte) item.Iff;
+ durationBytes[1] = item.Buff;
+ durationBytes[2] = (byte) item.Result;
+ durationBytes[3] = (byte) item.IsActivation;
+
+
+ // is shields + is_offcycle + pad61 + pad62 + pad63 + pad64 = int16[3] orientation
+ Span orientationBytes = stackalloc byte[6];
+ durationBytes[0] = item.IsShields;
+ durationBytes[1] = item.IsOffCycle;
+ BitConverter.TryWriteBytes(orientationBytes[2..6], item.Padding);
+
+ ushort duration = BitConverter.ToUInt16(durationBytes);
+
+ return new EffectStartEvent(item.Time, master, effect, aroundAgent, position, orientation, duration, trackableId);
+ }
default:
return new UnknownEvent(item.Time, item);
}
diff --git a/EVTCAnalytics/Processing/LogProcessorState.cs b/EVTCAnalytics/Processing/LogProcessorState.cs
index 3314abfd..2f53ad16 100644
--- a/EVTCAnalytics/Processing/LogProcessorState.cs
+++ b/EVTCAnalytics/Processing/LogProcessorState.cs
@@ -32,6 +32,7 @@ public class LogProcessorState
public int? GameShardId { get; set; }
public int? GameLanguageId { get; set; }
public int? MapId { get; set; }
+ public int? FractalScale { get; set; }
public LogType LogType { get; set; }
public GameLanguage GameLanguage { get; set; }
public IEncounterData EncounterData { get; set; }
diff --git a/EVTCInspector/InspectorForm.cs b/EVTCInspector/InspectorForm.cs
index 9d839c9c..a45da618 100644
--- a/EVTCInspector/InspectorForm.cs
+++ b/EVTCInspector/InspectorForm.cs
@@ -41,6 +41,7 @@ public sealed class InspectorForm : Form
// Processed skills
private readonly FilterCollection skills = new FilterCollection();
+ private readonly SkillControl skillControl;
// Processed effects
private readonly FilterCollection effects = new FilterCollection();
@@ -84,12 +85,15 @@ public InspectorForm()
agentsDetailLayout.Add(agentControl);
agentsDetailLayout.EndVertical();
- var agentSplitter = new Splitter {Panel1 = agentsGridView, Panel2 = agentsDetailLayout, Position = 300};
+ var agentSplitter = new Splitter {Panel1 = agentsGridView, Panel2 = agentsDetailLayout, Position = 400};
+
+ skillControl = new SkillControl();
+ var skillSplitter = new Splitter {Panel1 = ConstructSkillGridView(), Panel2 = skillControl, Position = 400};
var processedTabControl = new TabControl();
processedTabControl.Pages.Add(new TabPage(eventsDetailLayout) {Text = "Events"});
processedTabControl.Pages.Add(new TabPage(agentSplitter) {Text = "Agents"});
- processedTabControl.Pages.Add(new TabPage(ConstructSkillGridView()) {Text = "Skills"});
+ processedTabControl.Pages.Add(new TabPage(skillSplitter) {Text = "Skills"});
processedTabControl.Pages.Add(new TabPage(ConstructEffectGridView()) {Text = "Effects"});
processedTabControl.Pages.Add(new TabPage(ConstructMarkerGridView()) {Text = "Markers"});
@@ -270,6 +274,7 @@ public void SelectLog(string logFilename)
processedLog.GameLanguage,
processedLog.GameShardId,
processedLog.MapId,
+ processedLog.FractalScale,
processedLog.Errors
);
@@ -315,7 +320,26 @@ private GridView ConstructSkillGridView()
Binding = new DelegateBinding(x => x.Name)
}
});
-
+ grid.Columns.Add(new GridColumn
+ {
+ HeaderText = "Type",
+ DataCell = new TextBoxCell
+ {
+ // TODO: Improve for older versions without SkillInfo
+ Binding = new DelegateBinding(x =>
+ {
+ return (x.SkillData != null, x.BuffData != null) switch
+ {
+ (true, true) => "Both?",
+ (true, false) => "Ability",
+ (false, true) => "Buff",
+ (false, false) => "Unknown",
+ };
+ })
+ }
+ });
+
+ grid.SelectedItemsChanged += (_, _) => {skillControl.Skill = grid.SelectedItem;};
grid.DataStore = skills;
new GridViewSorter(grid, skills).EnableSorting();
diff --git a/EVTCInspector/SkillControl.cs b/EVTCInspector/SkillControl.cs
new file mode 100644
index 00000000..819c6ef3
--- /dev/null
+++ b/EVTCInspector/SkillControl.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Linq;
+using Eto.Drawing;
+using Eto.Forms;
+using GW2Scratch.EVTCAnalytics.Events;
+using GW2Scratch.EVTCAnalytics.Model.Agents;
+using GW2Scratch.EVTCAnalytics.Model.Skills;
+
+namespace GW2Scratch.EVTCInspector
+{
+ public class SkillControl : Panel
+ {
+ public Skill Skill
+ {
+ get => skill;
+ set
+ {
+ skill = value;
+ nameLabel.Text = skill == null ? "No skill selected." : $"Name: {skill.Name}";
+ jsonControl.Object = skill;
+ }
+ }
+
+ private Skill skill;
+
+ private readonly JsonSerializationControl jsonControl;
+ private readonly Label nameLabel = new Label();
+
+ public SkillControl()
+ {
+ var dataLayout = new DynamicLayout();
+ Content = dataLayout;
+
+ jsonControl = new JsonSerializationControl {Height = 200};
+
+ dataLayout.BeginVertical(new Padding(5));
+ dataLayout.AddRow(nameLabel);
+ dataLayout.AddRow(jsonControl);
+ dataLayout.EndVertical();
+ }
+ }
+}
\ No newline at end of file
diff --git a/EVTCInspector/Statistics.cs b/EVTCInspector/Statistics.cs
index 77e8ac3c..9fd0e5a7 100644
--- a/EVTCInspector/Statistics.cs
+++ b/EVTCInspector/Statistics.cs
@@ -27,10 +27,11 @@ public class Statistics
public GameLanguage GameLanguage { get; }
public int? GameShardId { get; }
public int? MapId { get; }
+ public int? FractalScale { get; }
public Statistics(DateTimeOffset fightStart, Player logAuthor, EncounterResult encounterResult,
EncounterMode encounterMode, Encounter encounter, string logVersion, TimeSpan encounterDuration,
- int? gameBuild, GameLanguage language, int? gameShardId, int? mapId, IEnumerable logErrors)
+ int? gameBuild, GameLanguage language, int? gameShardId, int? mapId, int? fractalScale, IEnumerable logErrors)
{
Encounter = encounter;
LogVersion = logVersion;
@@ -44,6 +45,7 @@ public Statistics(DateTimeOffset fightStart, Player logAuthor, EncounterResult e
GameLanguage = language;
GameShardId = gameShardId;
MapId = mapId;
+ FractalScale = fractalScale;
}
}
}
\ No newline at end of file
diff --git a/README.md b/README.md
index df58b50e..7309ac04 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[![Build and test (.NET Core)](https://img.shields.io/github/workflow/status/gw2scratch/evtc/Build%20and%20test%20(.NET%20Core)?logo=github)](https://github.com/gw2scratch/evtc/actions?query=workflow%3A%22Build+and+test+%28.NET+Core%29%22)
+[![Build and test (.NET Core)](https://img.shields.io/github/actions/workflow/status/gw2scratch/evtc/dotnet-core.yml?branch=master&logo=github)](https://github.com/gw2scratch/evtc/actions?query=workflow%3A%22Build+and+test+%28.NET+Core%29%22)
[![Discord](https://img.shields.io/discord/543804828808249374?label=discord&logo=discord&logoColor=white&)](https://discord.gg/TnHpN34)
# GW2Scratch EVTC Tools