main
ilitirit 3 years ago
commit e0f96ce3cb

350
.gitignore vendored

@ -0,0 +1,350 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Lumina.Excel.GeneratedSheets;
namespace Expedience
{
public class CurrentDutyInfo
{
public PlayerCharacter Player { get; set; }
public string PlaceName { get; set; }
public List<ClassJob> Party { get; set; }
}
}

@ -0,0 +1,69 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>MgAl2O4</Authors>
<Version>1.8.0.0</Version>
<Description>Pat counter</Description>
<Copyright>(c) 2023 MgAl2O4</Copyright>
<PackageProjectUrl>https://github.com/MgAl2O4/ExpedienceDalamud</PackageProjectUrl>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<Platforms>x64</Platforms>
<LangVersion>9.0</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
</PropertyGroup>
<ItemGroup>
<None Remove="assets\fan-kit-lala.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="assets\fan-kit-lala.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DalamudPackager" Version="2.1.11" />
<Reference Include="Dalamud">
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(DalamudLibPath)ImGui.NET.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(DalamudLibPath)ImGuiScene.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<None Update="Expedience.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<Target Name="CopyAssets" BeforeTargets="Build">
<Copy SourceFiles="$(ProjectDir)\assets\icon.png" DestinationFolder="$(OutDir)\images" />
</Target>
</Project>

@ -0,0 +1,11 @@
{
"Author": "ilitirit",
"Name": "Expedience",
"Punchline": "Ahahaha worthless buff",
"Description": "Records and uploads Duty Completion times",
"InternalName": "Expedience",
"ApplicableVersion": "any",
"Tags": [ "speed", "duty" ],
"CategoryTags": [ "social" ],
"DalamudApiLevel": 8
}

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.33424.131
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Expedience", "Expedience.csproj", "{8F1BDDC2-28F2-4CE2-8FB0-8FC58B4F6D9F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8F1BDDC2-28F2-4CE2-8FB0-8FC58B4F6D9F}.Debug|x64.ActiveCfg = Debug|x64
{8F1BDDC2-28F2-4CE2-8FB0-8FC58B4F6D9F}.Debug|x64.Build.0 = Debug|x64
{8F1BDDC2-28F2-4CE2-8FB0-8FC58B4F6D9F}.Release|x64.ActiveCfg = Release|x64
{8F1BDDC2-28F2-4CE2-8FB0-8FC58B4F6D9F}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8C1CD108-3B08-43AC-9FD6-A37A6E06CD4B}
EndGlobalSection
EndGlobal

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 MgAl2O4
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,3 @@
# Expedience
Dalamud plugin for recording and uploading duty completion times

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@ -0,0 +1,13 @@
{
"version": 1,
"dependencies": {
"net7.0-windows7.0": {
"DalamudPackager": {
"type": "Direct",
"requested": "[2.1.11, )",
"resolved": "2.1.11",
"contentHash": "9qlAWoRRTiL/geAvuwR/g6Bcbrd/bJJgVnB/RurBiyKs6srsP0bvpoo8IK+Eg8EA6jWeM6/YJWs66w4FIAzqPw=="
}
}
}
}

@ -0,0 +1,90 @@
using Dalamud.Configuration;
using Dalamud.Plugin;
using System;
using System.Collections.Generic;
namespace Expedience
{
[Serializable]
public class EmoteDataConfig
{
public string OwnerName { get; set; }
public int EmoteId { get; set; } = 0;
public int Counter { get; set; } = 0;
public bool IsValid() { return !string.IsNullOrEmpty(OwnerName) && (EmoteId > 0) && (Counter > 0); }
}
[Serializable]
public class Configuration : IPluginConfiguration
{
private static int VersionLatest = 1;
public int Version { get; set; } = VersionLatest;
public Dictionary<string, int> mapPats { internal get; set; } = new();
public List<EmoteDataConfig> Emotes { get; set; } = new();
public bool showSpecialPats { get; set; } = true;
public bool showFlyText { get; set; } = true;
public bool showCounterUI { get; set; } = false;
public bool canTrackDotes { get; set; } = true;
[NonSerialized]
private DalamudPluginInterface pluginInterface;
public void Initialize(DalamudPluginInterface pluginInterface)
{
this.pluginInterface = pluginInterface;
switch (Version)
{
case 0:
MigrateVersion0();
break;
default: break;
}
Emotes.RemoveAll(x => !x.IsValid());
}
public void Save()
{
Emotes.RemoveAll(x => !x.IsValid());
pluginInterface.SavePluginConfig(this);
}
public EmoteDataConfig FindOrAddEmote(string ownerName, int emoteId)
{
if (string.IsNullOrEmpty(ownerName) || emoteId <= 0)
{
return null;
}
EmoteDataConfig emoteOb = Emotes.Find(x => (x.EmoteId == emoteId && x.OwnerName.Equals(ownerName, StringComparison.OrdinalIgnoreCase)));
if (emoteOb != null)
{
return emoteOb;
}
emoteOb = new EmoteDataConfig() { OwnerName = ownerName, EmoteId = emoteId };
Emotes.Add(emoteOb);
return emoteOb;
}
private void MigrateVersion0()
{
foreach (var kvp in mapPats)
{
var emoteConfig = new EmoteDataConfig() { OwnerName = kvp.Key, EmoteId = EmoteReaderHooks.petEmoteId, Counter = kvp.Value };
Emotes.Add(emoteConfig);
}
mapPats.Clear();
Version = VersionLatest;
}
}
}

@ -0,0 +1,138 @@
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging;
using System;
using System.Collections.Generic;
namespace Expedience
{
public class EmoteCounter
{
public Action<int> OnChanged;
public bool isActive = true;
public int counterEmoteId;
public int[] triggerEmoteIds;
public string counterDesc;
public string counterDescPlural;
public string uiDesc;
private Dictionary<string, int> mapEmotesInZone = new();
private string currentPlayerName;
public void OnEmote(GameObject instigator, ushort emoteId)
{
bool canUse = emoteId == counterEmoteId;
if (triggerEmoteIds != null)
{
canUse = Array.FindIndex(triggerEmoteIds, x => x == emoteId) >= 0;
}
if (canUse && isActive)
{
var canNotify = IncCounter();
if (canNotify)
{
var playerInstigator = instigator as PlayerCharacter;
uint instigatorWorld = (playerInstigator != null) ? playerInstigator.HomeWorld.Id : 0;
Service.counterBroadcast.SendMessage(counterDesc, emoteId, instigator.Name.ToString(), instigatorWorld);
}
var instigatorName = (instigator != null) ? instigator.Name.ToString() : "??";
if (mapEmotesInZone.TryGetValue(instigatorName, out int counter))
{
mapEmotesInZone[instigatorName] = counter + 1;
}
else
{
mapEmotesInZone.Add(instigatorName, 1);
}
}
}
public int GetCounter()
{
if (currentPlayerName == null)
{
currentPlayerName = GetCurrentPlayerName();
}
var emoteData = Service.pluginConfig.FindOrAddEmote(currentPlayerName, counterEmoteId);
if (emoteData != null)
{
return emoteData.Counter;
}
return 0;
}
public bool IncCounter()
{
if (currentPlayerName == null)
{
currentPlayerName = GetCurrentPlayerName();
}
var emoteData = Service.pluginConfig.FindOrAddEmote(currentPlayerName, counterEmoteId);
if (emoteData != null)
{
emoteData.Counter++;
Service.pluginConfig.Save();
OnChanged?.Invoke(emoteData.Counter);
return true;
}
return false;
}
public void OnLogout()
{
currentPlayerName = null;
}
public void OnTerritoryChanged()
{
mapEmotesInZone.Clear();
}
public (string, int) GetTopEmotesInZone()
{
string maxPatsPlayer = null;
int maxPats = 0;
foreach (var kvp in mapEmotesInZone)
{
if (kvp.Value > maxPats)
{
maxPats = kvp.Value;
maxPatsPlayer = kvp.Key;
}
}
return (maxPatsPlayer, maxPats);
}
public int GetEmotesInCurrentZone(string instigatorName)
{
if (mapEmotesInZone.TryGetValue(instigatorName, out int numPats))
{
return numPats;
}
return 0;
}
private string GetCurrentPlayerName()
{
if (Service.clientState == null || Service.clientState.LocalPlayer == null || Service.clientState.LocalPlayer.Name == null)
{
return null;
}
return Service.clientState.LocalPlayer.Name.TextValue;
}
}
}

@ -0,0 +1,69 @@
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Hooking;
using Dalamud.Logging;
using System;
using System.Linq;
namespace Expedience
{
public class EmoteReaderHooks : IDisposable
{
public static ushort petEmoteId = 105; // TODO: read from lumina?
public Action<GameObject, ushort> OnEmote;
public delegate void OnEmoteFuncDelegate(ulong unk, ulong instigatorAddr, ushort emoteId, ulong targetId, ulong unk2);
private readonly Hook<OnEmoteFuncDelegate> hookEmote;
public bool IsValid = false;
public EmoteReaderHooks()
{
try
{
var emoteFuncPtr = Service.sigScanner.ScanText("48 89 5c 24 08 48 89 6c 24 10 48 89 74 24 18 48 89 7c 24 20 41 56 48 83 ec 30 4c 8b 74 24 60 48 8b d9 48 81 c1 60 2f 00 00");
hookEmote = Hook<OnEmoteFuncDelegate>.FromAddress(emoteFuncPtr, OnEmoteDetour);
hookEmote.Enable();
IsValid = true;
}
catch (Exception ex)
{
PluginLog.Error(ex, "oh noes!");
}
}
public void Dispose()
{
hookEmote?.Dispose();
IsValid = false;
}
void OnEmoteDetour(ulong unk, ulong instigatorAddr, ushort emoteId, ulong targetId, ulong unk2)
{
// unk - some field of event framework singleton? doesn't matter here anyway
// PluginLog.Log($"Emote >> unk:{unk:X}, instigatorAddr:{instigatorAddr:X}, emoteId:{emoteId}, targetId:{targetId:X}, unk2:{unk2:X}");
if (Service.clientState.LocalPlayer != null)
{
if (targetId == Service.clientState.LocalPlayer.ObjectId)
{
var instigatorOb = Service.objectTable.FirstOrDefault(x => (ulong)x.Address == instigatorAddr);
if (instigatorOb != null)
{
bool canCount = (instigatorOb.ObjectId != targetId);
#if DEBUG
canCount = true;
#endif
if (canCount)
{
OnEmote?.Invoke(instigatorOb, emoteId);
}
}
}
}
hookEmote.Original(unk, instigatorAddr, emoteId, targetId, unk2);
}
}
}

@ -0,0 +1,40 @@
using Dalamud.Interface.Windowing;
using ImGuiNET;
using System;
namespace Expedience
{
public class PatCountUI : Window, IDisposable
{
public PatCountUI() : base("Pat Count")
{
IsOpen = false;
Flags = ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.AlwaysAutoResize;
RespectCloseHotkey = false;
}
public void Dispose()
{
}
public override void Draw()
{
int pats = Service.patCounter.GetCounter();
ImGui.Text($"{Service.patCounter.uiDesc}: {pats}");
// add more counters if they want to be there
foreach (var counter in Service.plugin.emoteCounters)
{
if (counter != null && counter != Service.patCounter && !string.IsNullOrEmpty(counter.uiDesc) && counter.isActive)
{
int numEmotes = counter.GetCounter();
if (numEmotes > 0)
{
ImGui.Text($"{counter.uiDesc}: {numEmotes}");
}
}
}
}
}
}

@ -0,0 +1,264 @@
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Gui.FlyText;
using Dalamud.Game.Gui.Toast;
using Dalamud.Interface.Windowing;
using Dalamud.Logging;
using Dalamud.Plugin;
using ImGuiScene;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
namespace Expedience
{
public class Plugin : IDalamudPlugin
{
public string Name => "Expedience";
private readonly WindowSystem windowSystem = new("Expedience");
private PluginUI pluginUI;
private EmoteReaderHooks emoteReader;
private UIReaderVoteMvp uiReaderVoteMvp;
private UIReaderBannerMIP uiReaderBannerMIP;
private PluginWindowConfig windowConfig;
private PatCountUI patCountUI;
public readonly List<EmoteCounter> emoteCounters = new();
private ExcelSheet<TerritoryType> _territories;
private Tuple<PlaceName, DateTime> _currentDuty;
public Plugin(DalamudPluginInterface pluginInterface)
{
pluginInterface.Create<Service>();
Service.plugin = this;
Service.pluginConfig = pluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
Service.pluginConfig.Initialize(pluginInterface);
_territories = Service.DataManager.GetExcelSheet<TerritoryType>();
Service.patCounter = new EmoteCounter()
{
counterEmoteId = EmoteReaderHooks.petEmoteId,
counterDesc = "pat",
counterDescPlural = "pats",
uiDesc = "Head pats",
};
Service.DutyState.DutyStarted += (sender, args) =>
{
var territory = Service.ClientState.TerritoryType;
var player = Service.ClientState.LocalPlayer;
PlaceName place = _territories.GetRow(territory)?.PlaceName.Value;
foreach (var member in Service.PartyList)
{
}
};
Service.DutyState.DutyCompleted += (sender, args) =>
{
var territory = Service.ClientState.TerritoryType;
var place = _territories.GetRow(territory)?.PlaceName.Value;
};
Service.patCounter.OnChanged += (num) => OnEmoteReward(Service.patCounter, num);
emoteCounters.Add(Service.patCounter);
// two different emote ids?
Service.doteCounter = new EmoteCounter()
{
counterEmoteId = 146,
triggerEmoteIds = new int[] { 146, 147 },
counterDesc = "dote",
counterDescPlural = "dotes",
uiDesc = "Ranged pats",
};
Service.doteCounter.OnChanged += (num) => OnEmoteReward(Service.doteCounter, num);
Service.doteCounter.isActive = Service.pluginConfig.canTrackDotes;
emoteCounters.Add(Service.doteCounter);
pluginUI = new PluginUI();
pluginUI.overlayImage = LoadEmbeddedImage("fan-kit-lala.png");
uiReaderVoteMvp = new UIReaderVoteMvp();
uiReaderBannerMIP = new UIReaderBannerMIP();
windowConfig = new PluginWindowConfig();
windowSystem.AddWindow(windowConfig);
patCountUI = new PatCountUI();
windowSystem.AddWindow(patCountUI);
Service.commandManager.AddHandler("/Expedience", new(OnCommand) { HelpMessage = "Show pat counter" });
Service.commandManager.AddHandler("/patcount", new(OnCommand) { HelpMessage = "Show persistent pat counter" });
pluginInterface.UiBuilder.Draw += OnDraw;
pluginInterface.UiBuilder.OpenConfigUi += OnOpenConfig;
emoteReader = new EmoteReaderHooks();
emoteReader.OnEmote += (instigator, emoteId) => emoteCounters.ForEach(x => x.OnEmote(instigator, emoteId));
Service.framework.Update += Framework_Update;
Service.ClientState.TerritoryChanged += ClientState_TerritoryChanged;
Service.ClientState.Logout += ClientState_Logout;
if (Service.pluginConfig.showCounterUI)
{
patCountUI.IsOpen = true;
}
Service.counterBroadcast = pluginInterface.GetIpcProvider<string, ushort, string, uint, object>("ExpedienceEmoteCounter");
}
private void Framework_Update(Framework framework)
{
float deltaSeconds = (float)framework.UpdateDelta.TotalSeconds;
uiReaderVoteMvp.Tick(deltaSeconds);
uiReaderBannerMIP.Tick(deltaSeconds);
}
private void ClientState_TerritoryChanged(object sender, ushort e)
{
emoteCounters.ForEach(x => x.OnTerritoryChanged());
}
private void ClientState_Logout(object sender, EventArgs e)
{
emoteCounters.ForEach(x => x.OnLogout());
}
public void Dispose()
{
pluginUI.Dispose();
emoteReader.Dispose();
windowSystem.RemoveAllWindows();
Service.commandManager.RemoveHandler("/Expedience");
Service.commandManager.RemoveHandler("/patcount");
Service.framework.Update -= Framework_Update;
Service.ClientState.TerritoryChanged -= ClientState_TerritoryChanged;
Service.ClientState.Logout -= ClientState_Logout;
}
private void OnCommand(string command, string args)
{
if (command == "/Expedience")
{
DescribeCounter(Service.patCounter, false);
foreach (var counter in emoteCounters)
{
if (counter != Service.patCounter)
{
DescribeCounter(counter);
}
}
}
else if (command == "/patcount")
{
patCountUI.Toggle();
}
}
private void DescribeCounter(EmoteCounter counter, bool hideEmpty = true)
{
if (counter == null || string.IsNullOrEmpty(counter.counterDesc) || !counter.isActive)
{
return;
}
int numEmotes = counter.GetCounter();
if (numEmotes <= 0 && hideEmpty)
{
return;
}
var useName = counter.counterDesc[0].ToString().ToUpper() + counter.counterDesc.Substring(1);
Service.chatGui.Print($"{useName} counter: {numEmotes}");
var (maxPlayerName, maxCount) = counter.GetTopEmotesInZone();
if (maxCount > 0)
{
string countDesc = (maxCount == 1) ? counter.counterDesc : counter.counterDescPlural;
Service.chatGui.Print($"♥ {maxPlayerName}: {maxCount} {countDesc}");
}
}
private void OnDraw()
{
pluginUI.Draw();
windowSystem.Draw();
}
private void OnOpenConfig()
{
windowConfig.IsOpen = true;
}
public void OnShowCounterConfigChanged(bool wantsUI)
{
patCountUI.IsOpen = wantsUI;
}
private void OnEmoteReward(EmoteCounter counter, int numEmotes)
{
// thresholds on: 5, 15, 25, 50, 75, ...
bool isSpecial = (numEmotes < 25) ? (numEmotes == 5 || numEmotes == 15) : ((numEmotes % 25) == 0);
if (isSpecial && Service.pluginConfig.showSpecialPats)
{
// pats get special rewards.
if (counter == Service.patCounter)
{
pluginUI.Show();
}
var useDesc = counter.counterDescPlural.ToUpper();
Service.toastGui?.ShowQuest($"{numEmotes} {useDesc}!", new QuestToastOptions
{
Position = QuestToastPosition.Centre,
DisplayCheckmark = true,
IconId = 0,
PlaySound = true
});
}
else if (Service.pluginConfig.showFlyText)
{
var useDesc = counter.counterDesc.ToUpper();
Service.flyTextGui?.AddFlyText(FlyTextKind.NamedCriticalDirectHit, 0, (uint)numEmotes, 0, useDesc, " ", 0xff00ff00, 0, 0);
}
}
private TextureWrap LoadEmbeddedImage(string name)
{
TextureWrap resultImage = null;
try
{
var myAssembly = GetType().Assembly;
var myAssemblyName = myAssembly.GetName().Name;
var resourceName = $"{myAssemblyName}.assets.{name}";
var resStream = myAssembly.GetManifestResourceStream(resourceName);
if (resStream != null && resStream.Length > 0)
{
var contentBytes = new byte[(int)resStream.Length];
resStream.Read(contentBytes, 0, contentBytes.Length);
resultImage = Service.pluginInterface.UiBuilder.LoadImage(contentBytes);
resStream.Close();
}
}
catch (Exception ex)
{
PluginLog.Error(ex, "failed to load overlay image");
}
return resultImage;
}
}
}

@ -0,0 +1,101 @@
using ImGuiNET;
using ImGuiScene;
using System;
using System.Numerics;
namespace Expedience
{
public class PluginUI : IDisposable
{
private enum AnimPhase
{
None,
Appear,
Keep,
Disappear,
}
public TextureWrap overlayImage;
private AnimPhase anim = AnimPhase.None;
private static readonly float[] animDuration = new float[] { 0.0f, 1.0f, 1.0f, 1.0f };
private float animTimeRemaining = 0.0f;
public void Show()
{
SetAnim(AnimPhase.Appear);
}
public void Draw()
{
if (anim == AnimPhase.None || overlayImage == null)
{
return;
}
float animPct = 1.0f - Math.Max(0.0f, animTimeRemaining / animDuration[(int)anim]);
// draw image
{
var viewport = ImGui.GetMainViewport();
var viewportCenter = viewport.GetCenter();
var drawHalfSize = new Vector2(overlayImage.Width * 0.5f, overlayImage.Height * 0.5f);
if (drawHalfSize.X > viewportCenter.X || drawHalfSize.Y > viewportCenter.Y)
{
drawHalfSize.Y = viewportCenter.Y * 3 / 4;
drawHalfSize.X = overlayImage.Width * drawHalfSize.Y / overlayImage.Height;
}
if (anim == AnimPhase.Appear)
{
drawHalfSize *= AnimElastic(animPct);
}
var drawAlpha =
(anim == AnimPhase.Appear) ? 1 - Math.Pow(1 - animPct, 4) :
(anim == AnimPhase.Disappear) ? (1.0f - animPct) :
1.0f;
uint drawColor = 0xffffff | (uint)(drawAlpha * 255) << 24;
var drawList = ImGui.GetForegroundDrawList();
drawList.AddImage(overlayImage.ImGuiHandle, viewportCenter - drawHalfSize, viewportCenter + drawHalfSize, Vector2.Zero, Vector2.One, drawColor);
}
// state transitions
animTimeRemaining -= ImGui.GetIO().DeltaTime;
if (animTimeRemaining <= 0.0f)
{
if (anim == AnimPhase.Disappear)
{
SetAnim(AnimPhase.None);
}
else
{
SetAnim(anim + 1);
}
}
}
private float AnimElastic(float alpha)
{
const float c4 = (float)(2 * Math.PI) / 3.0f;
return (alpha == 0) ? 0.0f :
(alpha == 1) ? 1.0f :
(float)(Math.Pow(2, -10 * alpha) * Math.Sin((alpha * 10 - 0.75) * c4) + 1);
}
private void SetAnim(AnimPhase anim)
{
this.anim = anim;
animTimeRemaining = animDuration[(int)anim];
}
public void Dispose()
{
overlayImage.Dispose();
}
}
}

@ -0,0 +1,55 @@
using Dalamud.Interface.Windowing;
using ImGuiNET;
using System;
using System.Numerics;
namespace Expedience
{
public class PluginWindowConfig : Window, IDisposable
{
public PluginWindowConfig() : base("Pat Config")
{
IsOpen = false;
SizeConstraints = new WindowSizeConstraints() { MinimumSize = new Vector2(100, 0), MaximumSize = new Vector2(300, 3000) };
Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar;
}
public void Dispose()
{
}
public override void Draw()
{
bool showSpecialPats = Service.pluginConfig.showSpecialPats;
bool showFlyText = Service.pluginConfig.showFlyText;
bool showCounterOnScreen = Service.pluginConfig.showCounterUI;
bool canTrackDotes = Service.pluginConfig.canTrackDotes;
bool hasChanges = false;
hasChanges = ImGui.Checkbox("Show notify on reaching pat goals", ref showSpecialPats) || hasChanges;
hasChanges = ImGui.Checkbox("Show fly text counter on each emote", ref showFlyText) || hasChanges;
hasChanges = ImGui.Checkbox("Show pat counter on screen", ref showCounterOnScreen) || hasChanges;
ImGui.Separator();
hasChanges = ImGui.Checkbox("Track emote: dote", ref canTrackDotes) || hasChanges;
if (showCounterOnScreen != Service.pluginConfig.showCounterUI)
{
Service.plugin.OnShowCounterConfigChanged(showCounterOnScreen);
}
if (hasChanges)
{
Service.pluginConfig.showSpecialPats = showSpecialPats;
Service.pluginConfig.showFlyText = showFlyText;
Service.pluginConfig.showCounterUI = showCounterOnScreen;
Service.pluginConfig.canTrackDotes = canTrackDotes;
Service.pluginConfig.Save();
Service.doteCounter.isActive = canTrackDotes;
}
}
}
}

@ -0,0 +1,71 @@
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Party;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Command;
using Dalamud.Game.DutyState;
using Dalamud.Game.Gui;
using Dalamud.Game.Gui.FlyText;
using Dalamud.Game.Gui.Toast;
using Dalamud.IoC;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Lumina.Excel.GeneratedSheets;
namespace Expedience
{
internal class Service
{
public static Plugin plugin;
public static Configuration pluginConfig;
public static EmoteCounter patCounter;
public static EmoteCounter doteCounter;
public static ICallGateProvider<string, ushort, string, uint, object> counterBroadcast;
[PluginService]
public static DalamudPluginInterface pluginInterface { get; private set; } = null!;
[PluginService]
public static CommandManager commandManager { get; private set; } = null!;
[PluginService]
public static FlyTextGui flyTextGui { get; private set; } = null!;
[PluginService]
public static ToastGui toastGui { get; private set; } = null!;
[PluginService]
public static ClientState ClientState { get; private set; } = null!;
[PluginService]
public static ChatGui chatGui { get; private set; } = null!;
[PluginService]
public static SigScanner sigScanner { get; private set; } = null!;
[PluginService]
public static ObjectTable objectTable { get; private set; } = null!;
[PluginService]
public static Framework framework { get; private set; } = null!;
[PluginService]
public static GameGui gameGui { get; private set; } = null!;
[PluginService]
public static DataManager DataManager { get; private set; } = null!;
[PluginService]
public static DutyState DutyState { get; private set; } = null!;
[PluginService]
public static PartyList PartyList { get; private set; } = null!;
[PluginService]
public static InstanceContent InstanceContent { get; private set; } = null!;
}
}

@ -0,0 +1,107 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using MgAl2O4.Utils;
using System;
using System.Collections.Generic;
namespace Expedience
{
public class UIReaderBannerMIP
{
public const float UpdateInterval = 0.5f;
private float updateTimeRemaining = 0.0f;
private IntPtr cachedAddonPtr;
private Dictionary<int, string> playerNames = new Dictionary<int, string>();
public void Tick(float deltaSeconds)
{
updateTimeRemaining -= deltaSeconds;
if (updateTimeRemaining < 0.0f)
{
updateTimeRemaining = UpdateInterval;
UpdateAddon();
}
}
private unsafe void UpdateAddon()
{
var addonPtr = Service.gameGui.GetAddonByName("BannerMIP", 1);
var addonBaseNode = (AtkUnitBase*)addonPtr;
if (addonBaseNode == null || addonBaseNode->RootNode == null || !addonBaseNode->RootNode->IsVisible)
{
// reset when closed
cachedAddonPtr = IntPtr.Zero;
playerNames.Clear();
return;
}
cachedAddonPtr = addonPtr;
var level0 = GUINodeUtils.GetImmediateChildNodes(addonBaseNode->RootNode);
var listRoot = GUINodeUtils.PickNode(level0, 2, 4);
var listNodes = GUINodeUtils.GetImmediateChildNodes(listRoot);
var collectsPlayerNames = playerNames.Count == 0;
if (listNodes != null)
{
for (int idx = 0; idx < listNodes.Length; idx++)
{
var innerList = GUINodeUtils.GetChildNode(listNodes[idx]);
if (innerList != null && innerList->IsVisible)
{
var nodeLastName = GUINodeUtils.PickChildNode(innerList, 20, 27);
var nodeFirstName = GUINodeUtils.PickChildNode(innerList, 21, 27);
var nodeCombined = GUINodeUtils.PickChildNode(innerList, 22, 27);
UpdatePlayerNames(idx, collectsPlayerNames, nodeLastName, nodeFirstName, nodeCombined);
}
}
}
}
private unsafe void UpdatePlayerNames(int entryIdx, bool collectsPlayerNames, AtkResNode* nodeLastName, AtkResNode* nodeFirstName, AtkResNode* nodeCombined)
{
if (nodeLastName == null || nodeFirstName == null || nodeCombined == null ||
nodeLastName->Type != NodeType.Text || nodeFirstName->Type != NodeType.Text || nodeCombined->Type != NodeType.Text)
{
return;
}
var lastName = GUINodeUtils.GetNodeText(nodeLastName);
if (lastName.Length == 0 || lastName.StartsWith("pats:"))
{
return;
}
var playerName = "";
if (collectsPlayerNames)
{
var firstName = GUINodeUtils.GetNodeText(nodeFirstName);
playerName = $"{firstName} {lastName}";
playerNames.Add(entryIdx, playerName);
}
else
{
playerNames.TryGetValue(entryIdx, out playerName);
}
if (playerName.Length <= 1) // include separator
{
return;
}
int numPats = Service.patCounter.GetEmotesInCurrentZone(playerName);
if (numPats > 0)
{
((AtkTextNode*)nodeLastName)->SetText($"pats: {numPats}");
nodeCombined->Flags &= ~0x10; // hide
nodeLastName->Flags |= 0x10; // show
nodeFirstName->Flags |= 0x10; // show
}
}
}
}

@ -0,0 +1,74 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using MgAl2O4.Utils;
using System;
namespace Expedience
{
public class UIReaderVoteMvp
{
public const float UpdateInterval = 0.5f;
private float updateTimeRemaining = 0.0f;
private IntPtr cachedAddonPtr;
public void Tick(float deltaSeconds)
{
updateTimeRemaining -= deltaSeconds;
if (updateTimeRemaining < 0.0f)
{
updateTimeRemaining = UpdateInterval;
UpdateAddon();
}
}
private unsafe void UpdateAddon()
{
var addonPtr = Service.gameGui.GetAddonByName("VoteMvp", 1);
var addonBaseNode = (AtkUnitBase*)addonPtr;
if (addonBaseNode == null || addonBaseNode->RootNode == null || !addonBaseNode->RootNode->IsVisible)
{
// reset when closed
cachedAddonPtr = IntPtr.Zero;
return;
}
// update once
if (cachedAddonPtr == addonPtr)
{
return;
}
cachedAddonPtr = addonPtr;
var childNodesL0 = GUINodeUtils.GetImmediateChildNodes(addonBaseNode->RootNode);
if (childNodesL0 != null)
{
foreach (var nodeL0 in childNodesL0)
{
var nodeL1 = GUINodeUtils.PickChildNode(nodeL0, 3, 7);
if (nodeL1 != null && nodeL1->Type == NodeType.Text)
{
var textNode = (AtkTextNode*)nodeL1;
var playerName = textNode->NodeText.ToString();
if (!playerName.Contains("pats ]") && !playerName.Contains("pat ]"))
{
int numPats = Service.patCounter.GetEmotesInCurrentZone(playerName);
if (numPats == 1)
{
playerName += " [ 1 pat ]";
textNode->SetText(playerName);
}
else if (numPats > 1)
{
playerName += $" [ {numPats} pats ]";
textNode->SetText(playerName);
}
}
}
}
}
}
}
}

@ -0,0 +1,280 @@
using Dalamud.Logging;
using FFXIVClientStructs.FFXIV.Component.GUI;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
namespace MgAl2O4.Utils
{
// Dalamud.Interface.UIDebug is amazing
public class GUINodeUtils
{
public static unsafe AtkResNode* PickChildNode(AtkResNode* maybeCompNode, int childIdx, int expectedNumChildren)
{
if (maybeCompNode != null && (int)maybeCompNode->Type >= 1000)
{
var compNode = (AtkComponentNode*)maybeCompNode;
if (compNode->Component->UldManager.NodeListCount == expectedNumChildren && childIdx < expectedNumChildren)
{
return compNode->Component->UldManager.NodeList[childIdx];
}
}
return null;
}
public static unsafe AtkResNode* PickChildNode(AtkComponentBase* compPtr, int childIdx, int expectedNumChildren)
{
if (compPtr != null && compPtr->UldManager.NodeListCount == expectedNumChildren && childIdx < expectedNumChildren)
{
return compPtr->UldManager.NodeList[childIdx];
}
return null;
}
public static unsafe AtkResNode*[] GetImmediateChildNodes(AtkResNode* node)
{
var listAddr = new List<ulong>();
if (node != null && node->ChildNode != null)
{
listAddr.Add((ulong)node->ChildNode);
node = node->ChildNode;
while (node->PrevSiblingNode != null)
{
listAddr.Add((ulong)node->PrevSiblingNode);
node = node->PrevSiblingNode;
}
}
return ConvertToNodeArr(listAddr);
}
public static unsafe AtkResNode*[] GetAllChildNodes(AtkResNode* node)
{
if (node != null)
{
var list = new List<ulong>();
RecursiveAppendChildNodes(node, list);
return ConvertToNodeArr(list);
}
return null;
}
private static unsafe void RecursiveAppendChildNodes(AtkResNode* node, List<ulong> listAddr)
{
if (node != null)
{
listAddr.Add((ulong)node);
// step inside
if (node->ChildNode != null)
{
RecursiveAppendChildNodes(node->ChildNode, listAddr);
AtkResNode* linkNode = node->ChildNode;
while (linkNode->PrevSiblingNode != null)
{
RecursiveAppendChildNodes(linkNode->PrevSiblingNode, listAddr);
linkNode = linkNode->PrevSiblingNode;
}
// no need to check next siblings here?
}
}
}
private static unsafe AtkResNode*[] ConvertToNodeArr(List<ulong> listAddr)
{
if (listAddr.Count > 0)
{
var typedArr = new AtkResNode*[listAddr.Count];
for (int idx = 0; idx < listAddr.Count; idx++)
{
typedArr[idx] = (AtkResNode*)listAddr[idx];
}
return typedArr;
}
return null;
}
public static unsafe AtkResNode* PickNode(AtkResNode*[] nodes, int nodeIdx, int expectedNumNodes)
{
if (nodes != null && nodes.Length == expectedNumNodes && nodeIdx < expectedNumNodes)
{
return nodes[nodeIdx];
}
return null;
}
public static unsafe AtkResNode* GetChildNode(AtkResNode* node)
{
return node != null ? node->ChildNode : null;
}
public static unsafe string GetNodeTexturePath(AtkResNode* maybeImageNode)
{
if (maybeImageNode != null && maybeImageNode->Type == NodeType.Image)
{
var imageNode = (AtkImageNode*)maybeImageNode;
if (imageNode->PartsList != null && imageNode->PartId <= imageNode->PartsList->PartCount)
{
var textureInfo = imageNode->PartsList->Parts[imageNode->PartId].UldAsset;
var texType = textureInfo->AtkTexture.TextureType;
if (texType == TextureType.Resource)
{
var texFileNameStdString = &textureInfo->AtkTexture.Resource->TexFileResourceHandle->ResourceHandle.FileName;
var texString = texFileNameStdString->Length < 16
? Marshal.PtrToStringAnsi((IntPtr)texFileNameStdString->Buffer)
: Marshal.PtrToStringAnsi((IntPtr)texFileNameStdString->BufferPtr);
return texString;
}
}
}
return null;
}
public static unsafe string GetNodeText(AtkResNode* maybeTextNode)
{
if (maybeTextNode != null && maybeTextNode->Type == NodeType.Text)
{
var textNode = (AtkTextNode*)maybeTextNode;
var text = Marshal.PtrToStringUTF8(new IntPtr(textNode->NodeText.StringPtr));
return text;
}
return null;
}
public static unsafe Vector2 GetNodePosition(AtkResNode* node)
{
var pos = new Vector2(node->X, node->Y);
var par = node->ParentNode;
while (par != null)
{
pos *= new Vector2(par->ScaleX, par->ScaleY);
pos += new Vector2(par->X, par->Y);
par = par->ParentNode;
}
return pos;
}
public static unsafe Vector2 GetNodeScale(AtkResNode* node)
{
if (node == null) return new Vector2(1, 1);
var scale = new Vector2(node->ScaleX, node->ScaleY);
while (node->ParentNode != null)
{
node = node->ParentNode;
scale *= new Vector2(node->ScaleX, node->ScaleY);
}
return scale;
}
public static unsafe (Vector2, Vector2) GetNodePosAndSize(AtkResNode* node)
{
if (node != null)
{
var pos = GetNodePosition(node);
var scale = GetNodeScale(node);
var size = new Vector2(node->Width * scale.X, node->Height * scale.Y);
return (pos, size);
}
return (Vector2.Zero, Vector2.Zero);
}
#if DEBUG
private class ParsableNode
{
public ulong nodeAddr;
public string content;
public int childIdx;
public int numChildren;
public int depth;
public NodeType type;
public string debugPath;
}
private static unsafe bool RecursiveAppendParsableChildNodes(AtkResNode* node, int depth, int childIdx, List<ParsableNode> list, string debugPath)
{
bool hasParsableChildNodes = false;
bool hasContent = false;
if (node != null)
{
// check if this node is interesting for parser (empty string is still interesting)
string content = GetNodeText(node);
content = (content != null) ? content : GetNodeTexturePath(node);
hasContent = (content != null);
int numChildNodes = 0;
int insertIdx = list.Count;
if ((int)node->Type < 1000)
{
// step inside
if (node->ChildNode != null)
{
hasParsableChildNodes = RecursiveAppendParsableChildNodes(node->ChildNode, depth + 1, numChildNodes, list, debugPath + "," + numChildNodes);
numChildNodes++;
AtkResNode* linkNode = node->ChildNode;
while (linkNode->PrevSiblingNode != null)
{
var hasParsableSibling = RecursiveAppendParsableChildNodes(linkNode->PrevSiblingNode, depth + 1, numChildNodes, list, debugPath + "," + numChildNodes);
hasParsableChildNodes = hasParsableChildNodes || hasParsableSibling;
linkNode = linkNode->PrevSiblingNode;
numChildNodes++;
}
// no need to check next siblings here?
}
}
else
{
var compNode = (AtkComponentNode*)node;
for (int idx = 0; idx < compNode->Component->UldManager.NodeListCount; idx++)
{
hasParsableChildNodes = RecursiveAppendParsableChildNodes(compNode->Component->UldManager.NodeList[idx], depth + 1, numChildNodes, list, debugPath + "," + numChildNodes);
numChildNodes++;
}
}
if (hasParsableChildNodes || hasContent)
{
list.Insert(insertIdx, new ParsableNode() { nodeAddr = (ulong)node, content = content, childIdx = childIdx, numChildren = numChildNodes, depth = depth, debugPath = debugPath, type = node->Type });
}
}
return hasParsableChildNodes || hasContent;
}
public static unsafe void LogParsableNodes(AtkResNode* node)
{
var list = new List<ParsableNode>();
RecursiveAppendParsableChildNodes(node, 0, 0, list, "");
foreach (var entry in list)
{
var prefix = entry.depth > 0 ? new string(' ', entry.depth * 2) : "";
PluginLog.Log($"{prefix}> '{entry.content}' idx:{entry.childIdx}, children:{entry.numChildren}, type:{entry.type}, addr:{entry.nodeAddr:X}, path:{entry.debugPath}");
}
}
#endif // DEBUG
}
}
Loading…
Cancel
Save