├── .github
├── FUNDING.yml
└── workflows
│ ├── build-debug.yml
│ └── release.yml
├── .gitignore
├── .vscode
└── tasks.json
├── Cactbot.md
├── ChangelogFixer
├── ChangelogFixer.csproj
└── Program.cs
├── LICENSE
├── LMeter.sln
├── LMeter
├── LMeter.csproj
├── LMeter.json
└── src
│ ├── Act
│ ├── ActClient.cs
│ ├── ActEvent.cs
│ ├── ActEventParser.cs
│ ├── ActWebSocketClient.cs
│ ├── EnumConverter.cs
│ ├── IActClient.cs
│ ├── IinactClient.cs
│ ├── LazyFloat.cs
│ ├── LazyFloatConverter.cs
│ ├── LazyString.cs
│ └── TextTagFormatter.cs
│ ├── Cactbot
│ ├── CactbotRaidbossWindows.cs
│ ├── CactbotState.cs
│ ├── CactbotTimeLineElement.cs
│ ├── IinactCactbotClient.cs
│ ├── TotallyNotCefBrowserState.cs
│ ├── TotallyNotCefCactbotHttpSource.cs
│ ├── TotallyNotCefConnectionState.cs
│ └── TotallyNotCefHealthCheckResponse.cs
│ ├── Config
│ ├── AboutPage.cs
│ ├── ActConfig.cs
│ ├── BarColorsConfig.cs
│ ├── BarConfig.cs
│ ├── CactbotConfig.cs
│ ├── ConfigColor.cs
│ ├── FontConfig.cs
│ ├── GeneralConfig.cs
│ ├── HeaderConfig.cs
│ ├── IConfigPage.cs
│ ├── IConfigurable.cs
│ ├── LMeterConfig.cs
│ ├── MeterListConfig.cs
│ └── VisibilityConfig.cs
│ ├── Helpers
│ ├── CharacterState.cs
│ ├── ConfigHelpers.cs
│ ├── DrawChildScope.cs
│ ├── DrawHelpers.cs
│ ├── Enums.cs
│ ├── Extensions.cs
│ ├── FontsManager.cs
│ ├── TexturesCache.cs
│ └── Utils.cs
│ ├── MagicValues.cs
│ ├── Meter
│ └── MeterWindow.cs
│ ├── Plugin.cs
│ ├── PluginManager.cs
│ ├── Runtime
│ ├── MonoMD5CryptoServiceProvider.cs
│ ├── ProcessLauncher.cs
│ ├── ShaFixer.cs
│ └── WineChecker.cs
│ └── Windows
│ └── ConfigWindow.cs
├── README.md
├── Version
└── Version.csproj
├── build.sh
├── deps
├── fonts
│ ├── Expressway.ttf
│ ├── Roboto-Black.ttf
│ ├── Roboto-Light.ttf
│ └── big-noodle-too.ttf
├── img
│ ├── icon.png
│ └── icon_small.png
└── txt
│ └── changelog.md
├── repo.json
└── repo
├── act_connection.png
├── auto_hide.png
├── cactbot_browser_settings.png
├── cactbot_connection_settings.png
├── cactbot_preview_positioning.png
├── dalamud_settings_part1.png
├── dalamud_settings_part2.png
├── end_encounter.png
├── meter_demo_1.png
└── meter_demo_2.png
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: lichie
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/workflows/build-debug.yml:
--------------------------------------------------------------------------------
1 | name: Debug Build
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build-debug:
7 | runs-on: windows-latest
8 |
9 | env:
10 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
11 |
12 | steps:
13 | - name: Checkout and initialise
14 | uses: actions/checkout@v2
15 | with:
16 | submodules: recursive
17 |
18 | - name: Setup Dalamud
19 | shell: pwsh
20 | run: |
21 | Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile ./latest.zip
22 | Expand-Archive -Path ./latest.zip ./dalamud
23 |
24 | - name: Restore project dependencies
25 | run: dotnet restore --verbosity normal
26 |
27 | - name: Build Debug
28 | run: dotnet build --no-restore --verbosity normal --configuration Debug
29 |
30 | - name: Upload Artifact
31 | uses: actions/upload-artifact@v2
32 | with:
33 | name: LMeter-debug-${{ github.sha }}
34 | path: |
35 | LMeter/bin/x64/Debug
36 | !LMeter/bin/x64/Debug/LMeter
37 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 |
8 | jobs:
9 | build-release:
10 | runs-on: windows-latest
11 |
12 | env:
13 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
14 |
15 | steps:
16 | - name: Checkout and initialise
17 | uses: actions/checkout@v2
18 | with:
19 | submodules: recursive
20 |
21 | - name: Setup Dalamud
22 | shell: pwsh
23 | run: |
24 | Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile ./latest.zip
25 | Expand-Archive -Path ./latest.zip ./dalamud
26 |
27 | - name: Restore project dependencies
28 | run: dotnet restore --verbosity normal
29 |
30 | - name: Build Release
31 | run: dotnet build --no-restore --verbosity normal --configuration Release
32 |
33 | - name: Upload Artifact
34 | uses: actions/upload-artifact@v2
35 | with:
36 | name: LMeter-release-${{ github.sha }}
37 | path: |
38 | LMeter/bin/x64/Release
39 | !LMeter/bin/x64/Release/LMeter
40 |
41 | - name: Create Release
42 | id: create_release
43 | uses: actions/create-release@v1
44 | env:
45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 | with:
47 | tag_name: ${{ github.ref }}
48 | release_name: LMeter ${{ github.ref }}
49 | draft: false
50 | prerelease: false
51 | - name: Upload Release Asset
52 | id: upload-release-asset
53 | uses: actions/upload-release-asset@v1
54 | env:
55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56 | with:
57 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
58 | asset_path: ./LMeter/bin/x64/Release/LMeter/latest.zip
59 | asset_name: LMeter.zip
60 | asset_content_type: application/zip
61 |
62 | - name: Write out repo.json
63 | run: |
64 | $ver = '${{ github.ref }}' -replace 'refs/tags/',''
65 | $path = './base_repo.json'
66 | $new_path = './repo.json'
67 | $content = get-content -path $path
68 | $content = $content -replace '1.0.0.0',$ver
69 | set-content -Path $new_path -Value $content
70 | - name: Commit repo.json
71 | run: |
72 | git config --global user.name "Actions User"
73 | git config --global user.email "actions@github.com"
74 | git fetch origin main && git checkout main
75 | git add repo.json
76 | git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true
77 | git push origin main || true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Our Additions
2 |
3 | .idea/
4 | .vs/
5 | .vscode/
6 | dalamud/
7 | deps/lib
8 | deps/lib/
9 | deps/lib/*
10 | Naowh.ttf
11 |
12 | # Local dev script
13 | publish_dev.sh
14 |
15 | ## Ignore Visual Studio temporary files, build results, and
16 | ## files generated by popular Visual Studio add-ons.
17 | ##
18 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
19 |
20 | # Packaging
21 | pack/
22 |
23 | # User-specific files
24 | *.rsuser
25 | *.suo
26 | *.user
27 | *.userosscache
28 | *.sln.docstates
29 |
30 | # User-specific files (MonoDevelop/Xamarin Studio)
31 | *.userprefs
32 |
33 | # Mono auto generated files
34 | mono_crash.*
35 |
36 | # Build results
37 | [Dd]ebug/
38 | [Dd]ebugPublic/
39 | [Rr]elease/
40 | [Rr]eleases/
41 | x64/
42 | x86/
43 | [Ww][Ii][Nn]32/
44 | [Aa][Rr][Mm]/
45 | [Aa][Rr][Mm]64/
46 | bld/
47 | [Bb]in/
48 | [Oo]bj/
49 | [Ll]og/
50 | [Ll]ogs/
51 |
52 | # Visual Studio 2015/2017 cache/options directory
53 | .vs/
54 | # Uncomment if you have tasks that create the project's static files in wwwroot
55 | #wwwroot/
56 |
57 | # Visual Studio 2017 auto generated files
58 | Generated\ Files/
59 |
60 | # MSTest test Results
61 | [Tt]est[Rr]esult*/
62 | [Bb]uild[Ll]og.*
63 |
64 | # NUnit
65 | *.VisualState.xml
66 | TestResult.xml
67 | nunit-*.xml
68 |
69 | # Build Results of an ATL Project
70 | [Dd]ebugPS/
71 | [Rr]eleasePS/
72 | dlldata.c
73 |
74 | # Benchmark Results
75 | BenchmarkDotNet.Artifacts/
76 |
77 | # .NET Core
78 | project.lock.json
79 | project.fragment.lock.json
80 | artifacts/
81 |
82 | # ASP.NET Scaffolding
83 | ScaffoldingReadMe.txt
84 |
85 | # StyleCop
86 | StyleCopReport.xml
87 |
88 | # Files built by Visual Studio
89 | *_i.c
90 | *_p.c
91 | *_h.h
92 | *.ilk
93 | *.meta
94 | *.obj
95 | *.iobj
96 | *.pch
97 | *.pdb
98 | *.ipdb
99 | *.pgc
100 | *.pgd
101 | *.rsp
102 | *.sbr
103 | *.tlb
104 | *.tli
105 | *.tlh
106 | *.tmp
107 | *.tmp_proj
108 | *_wpftmp.csproj
109 | *.log
110 | *.vspscc
111 | *.vssscc
112 | .builds
113 | *.pidb
114 | *.svclog
115 | *.scc
116 |
117 | # Chutzpah Test files
118 | _Chutzpah*
119 |
120 | # Visual C++ cache files
121 | ipch/
122 | *.aps
123 | *.ncb
124 | *.opendb
125 | *.opensdf
126 | *.sdf
127 | *.cachefile
128 | *.VC.db
129 | *.VC.VC.opendb
130 |
131 | # Visual Studio profiler
132 | *.psess
133 | *.vsp
134 | *.vspx
135 | *.sap
136 |
137 | # Visual Studio Trace Files
138 | *.e2e
139 |
140 | # TFS 2012 Local Workspace
141 | $tf/
142 |
143 | # Guidance Automation Toolkit
144 | *.gpState
145 |
146 | # ReSharper is a .NET coding add-in
147 | _ReSharper*/
148 | *.[Rr]e[Ss]harper
149 | *.DotSettings.user
150 |
151 | # TeamCity is a build add-in
152 | _TeamCity*
153 |
154 | # DotCover is a Code Coverage Tool
155 | *.dotCover
156 |
157 | # AxoCover is a Code Coverage Tool
158 | .axoCover/*
159 | !.axoCover/settings.json
160 |
161 | # Coverlet is a free, cross platform Code Coverage Tool
162 | coverage*.json
163 | coverage*.xml
164 | coverage*.info
165 |
166 | # Visual Studio code coverage results
167 | *.coverage
168 | *.coveragexml
169 |
170 | # NCrunch
171 | _NCrunch_*
172 | .*crunch*.local.xml
173 | nCrunchTemp_*
174 |
175 | # MightyMoose
176 | *.mm.*
177 | AutoTest.Net/
178 |
179 | # Web workbench (sass)
180 | .sass-cache/
181 |
182 | # Installshield output folder
183 | [Ee]xpress/
184 |
185 | # DocProject is a documentation generator add-in
186 | DocProject/buildhelp/
187 | DocProject/Help/*.HxT
188 | DocProject/Help/*.HxC
189 | DocProject/Help/*.hhc
190 | DocProject/Help/*.hhk
191 | DocProject/Help/*.hhp
192 | DocProject/Help/Html2
193 | DocProject/Help/html
194 |
195 | # Click-Once directory
196 | publish/
197 |
198 | # Publish Web Output
199 | *.[Pp]ublish.xml
200 | *.azurePubxml
201 | # Note: Comment the next line if you want to checkin your web deploy settings,
202 | # but database connection strings (with potential passwords) will be unencrypted
203 | *.pubxml
204 | *.publishproj
205 |
206 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
207 | # checkin your Azure Web App publish settings, but sensitive information contained
208 | # in these scripts will be unencrypted
209 | PublishScripts/
210 |
211 | # NuGet Packages
212 | *.nupkg
213 | # NuGet Symbol Packages
214 | *.snupkg
215 | # The packages folder can be ignored because of Package Restore
216 | **/[Pp]ackages/*
217 | # except build/, which is used as an MSBuild target.
218 | !**/[Pp]ackages/build/
219 | # Uncomment if necessary however generally it will be regenerated when needed
220 | #!**/[Pp]ackages/repositories.config
221 | # NuGet v3's project.json files produces more ignorable files
222 | *.nuget.props
223 | *.nuget.targets
224 |
225 | # Microsoft Azure Build Output
226 | csx/
227 | *.build.csdef
228 |
229 | # Microsoft Azure Emulator
230 | ecf/
231 | rcf/
232 |
233 | # Windows Store app package directories and files
234 | AppPackages/
235 | BundleArtifacts/
236 | Package.StoreAssociation.xml
237 | _pkginfo.txt
238 | *.appx
239 | *.appxbundle
240 | *.appxupload
241 |
242 | # Visual Studio cache files
243 | # files ending in .cache can be ignored
244 | *.[Cc]ache
245 | # but keep track of directories ending in .cache
246 | !?*.[Cc]ache/
247 |
248 | # Others
249 | ClientBin/
250 | ~$*
251 | *~
252 | *.dbmdl
253 | *.dbproj.schemaview
254 | *.jfm
255 | *.pfx
256 | *.publishsettings
257 | orleans.codegen.cs
258 |
259 | # Including strong name files can present a security risk
260 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
261 | #*.snk
262 |
263 | # Since there are multiple workflows, uncomment next line to ignore bower_components
264 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
265 | #bower_components/
266 |
267 | # RIA/Silverlight projects
268 | Generated_Code/
269 |
270 | # Backup & report files from converting an old project file
271 | # to a newer Visual Studio version. Backup files are not needed,
272 | # because we have git ;-)
273 | _UpgradeReport_Files/
274 | Backup*/
275 | UpgradeLog*.XML
276 | UpgradeLog*.htm
277 | ServiceFabricBackup/
278 | *.rptproj.bak
279 |
280 | # SQL Server files
281 | *.mdf
282 | *.ldf
283 | *.ndf
284 |
285 | # Business Intelligence projects
286 | *.rdl.data
287 | *.bim.layout
288 | *.bim_*.settings
289 | *.rptproj.rsuser
290 | *- [Bb]ackup.rdl
291 | *- [Bb]ackup ([0-9]).rdl
292 | *- [Bb]ackup ([0-9][0-9]).rdl
293 |
294 | # Microsoft Fakes
295 | FakesAssemblies/
296 |
297 | # GhostDoc plugin setting file
298 | *.GhostDoc.xml
299 |
300 | # Node.js Tools for Visual Studio
301 | .ntvs_analysis.dat
302 | node_modules/
303 |
304 | # Visual Studio 6 build log
305 | *.plg
306 |
307 | # Visual Studio 6 workspace options file
308 | *.opt
309 |
310 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
311 | *.vbw
312 |
313 | # Visual Studio LightSwitch build output
314 | **/*.HTMLClient/GeneratedArtifacts
315 | **/*.DesktopClient/GeneratedArtifacts
316 | **/*.DesktopClient/ModelManifest.xml
317 | **/*.Server/GeneratedArtifacts
318 | **/*.Server/ModelManifest.xml
319 | _Pvt_Extensions
320 |
321 | # Paket dependency manager
322 | .paket/paket.exe
323 | paket-files/
324 |
325 | # FAKE - F# Make
326 | .fake/
327 |
328 | # CodeRush personal settings
329 | .cr/personal
330 |
331 | # Python Tools for Visual Studio (PTVS)
332 | __pycache__/
333 | *.pyc
334 |
335 | # Cake - Uncomment if you are using it
336 | # tools/**
337 | # !tools/packages.config
338 |
339 | # Tabs Studio
340 | *.tss
341 |
342 | # Telerik's JustMock configuration file
343 | *.jmconfig
344 |
345 | # BizTalk build output
346 | *.btp.cs
347 | *.btm.cs
348 | *.odx.cs
349 | *.xsd.cs
350 |
351 | # OpenCover UI analysis results
352 | OpenCover/
353 |
354 | # Azure Stream Analytics local run output
355 | ASALocalRun/
356 |
357 | # MSBuild Binary and Structured Log
358 | *.binlog
359 |
360 | # NVidia Nsight GPU debugger configuration file
361 | *.nvuser
362 |
363 | # MFractors (Xamarin productivity tool) working folder
364 | .mfractor/
365 |
366 | # Local History for Visual Studio
367 | .localhistory/
368 |
369 | # BeatPulse healthcheck temp database
370 | healthchecksdb
371 |
372 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
373 | MigrationBackup/
374 |
375 | # Ionide (cross platform F# VS Code tools) working folder
376 | .ionide/
377 |
378 | # Fody - auto-generated XML schema
379 | FodyWeavers.xsd
380 | desktop.ini
381 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "build debug",
8 | "command": "dotnet",
9 | "type": "shell",
10 | "args": [
11 | "build",
12 | // Ask dotnet build to generate full paths for file names.
13 | "/property:GenerateFullPaths=true",
14 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel
15 | "/consoleloggerparameters:NoSummary",
16 | "--configuration",
17 | "Debug"
18 | ],
19 | "group": "build",
20 | "presentation": {
21 | "reveal": "silent"
22 | },
23 | "problemMatcher": "$msCompile"
24 | },
25 | {
26 | "label": "build release",
27 | "command": "dotnet",
28 | "type": "shell",
29 | "args": [
30 | "build",
31 | // Ask dotnet build to generate full paths for file names.
32 | "/property:GenerateFullPaths=true",
33 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel
34 | "/consoleloggerparameters:NoSummary",
35 | "--configuration",
36 | "Release"
37 | ],
38 | "group": "build",
39 | "presentation": {
40 | "reveal": "silent"
41 | },
42 | "problemMatcher": "$msCompile"
43 | }
44 | ]
45 | }
--------------------------------------------------------------------------------
/ChangelogFixer/ChangelogFixer.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net7.0
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/ChangelogFixer/Program.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using Newtonsoft.Json;
3 |
4 |
5 | var repoText = File.ReadAllText(args[0]);
6 | var changelogText = File.ReadAllText(args[1]);
7 |
8 | dynamic repo = JsonConvert.DeserializeObject(repoText);
9 | repo[1].Changelog = changelogText.Split("\n\n")[0];
10 |
11 | var serializer = new Newtonsoft.Json.JsonSerializer();
12 | serializer.Formatting = Formatting.Indented;
13 | using (var sw = new StreamWriter(args[0]))
14 | {
15 | using (var writer = new JsonTextWriter(sw))
16 | {
17 | writer.Indentation = 4;
18 | serializer.Serialize(writer, repo);
19 | sw.Write("\n");
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/LMeter.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{B57241FD-A62F-46E6-8662-AA7F70F0B6A5}") = "LMeter", "LMeter\LMeter.csproj", "{347B5BB0-810D-4083-BF0E-D920B14C4213}"
4 | EndProject
5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{226FB92B-E6FD-4D1A-9D6F-C4292450B706}"
6 | ProjectSection(SolutionItems) = preProject
7 | .gitignore = .gitignore
8 | build.sh = build.sh
9 | Version\Version.csproj = Version\Version.csproj
10 | EndProjectSection
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Release|Any CPU = Release|Any CPU
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {347B5BB0-810D-4083-BF0E-D920B14C4213}.Debug|Any CPU.ActiveCfg = Debug|x64
19 | {347B5BB0-810D-4083-BF0E-D920B14C4213}.Debug|Any CPU.Build.0 = Debug|x64
20 | {347B5BB0-810D-4083-BF0E-D920B14C4213}.Release|Any CPU.ActiveCfg = Release|x64
21 | {347B5BB0-810D-4083-BF0E-D920B14C4213}.Release|Any CPU.Build.0 = Release|x64
22 | EndGlobalSection
23 | GlobalSection(SolutionProperties) = preSolution
24 | HideSolutionNode = FALSE
25 | EndGlobalSection
26 | GlobalSection(ExtensibilityGlobals) = postSolution
27 | SolutionGuid = {D932CF5E-10FC-4D3A-A1D9-FC39D1961D24}
28 | EndGlobalSection
29 | EndGlobal
30 |
--------------------------------------------------------------------------------
/LMeter/LMeter.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | x64
6 | net7.0-windows
7 | 11
8 | x64
9 | Debug;Release
10 |
11 |
12 |
13 |
14 | LMeter
15 | LMeter
16 | LMeter
17 | Copyright © Lichie 2021
18 | $(PluginVersion)
19 | $(PluginVersion)
20 | $(PluginVersion)
21 |
22 |
23 |
24 |
25 | true
26 | false
27 | true
28 | true
29 | $(FeatureFlags.Replace("#",";"))
30 | false
31 | enable
32 | bin/$(Configuration)/
33 | Library
34 | false
35 | Nullable
36 |
37 |
38 |
39 |
40 | true
41 | full
42 | DEBUG;TRACE;$(DefineConstants)
43 |
44 |
45 |
46 |
47 | false
48 | none
49 | true
50 | $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)/../'))=C:\
51 |
52 |
53 |
54 |
55 | dev
56 | $(DALAMUD_HOME)
57 | $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)/../deps/lib/dalamud/'))
58 |
59 |
60 |
61 |
62 | $([System.IO.Path]::GetFullPath('$(APPDATA)\XIVLauncher\addon\Hooks\$(DalamudVersion)\'))
63 |
64 |
65 |
66 |
67 | $([System.IO.Path]::GetFullPath('$(HOME)/.xlcore/dalamud/Hooks/$(DalamudVersion)/'))
68 |
69 |
70 |
71 |
72 |
73 | $(AssemblySearchPaths);
74 | $(DalamudXIVLauncher);
75 | $(DalamudHome);
76 | $(DalamudLocal);
77 |
78 |
79 |
80 |
81 |
82 |
89 |
90 |
91 |
92 |
93 |
98 |
99 |
100 |
101 |
102 |
107 |
108 |
109 |
110 |
111 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | Media/Fonts/%(FileName)%(Extension)
127 | PreserveNewest
128 |
129 |
130 | Media/Images/%(FileName)%(Extension)
131 | PreserveNewest
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | false
140 |
141 |
142 | false
143 |
144 |
145 | false
146 |
147 |
148 | false
149 |
150 |
151 | false
152 |
153 |
154 | false
155 |
156 |
157 | false
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
175 |
176 |
177 |
178 |
184 |
185 |
186 |
187 |
188 |
--------------------------------------------------------------------------------
/LMeter/LMeter.json:
--------------------------------------------------------------------------------
1 | {
2 | "Name": "LMeter",
3 | "Author": "Lichie, joshua.software.dev",
4 | "Punchline": "Plugin to display ACT combat log data.",
5 | "Description": "Renders ACT combat log data (and optionally Cactbot) as a plugin instead of using a web based overlay",
6 | "RepoUrl": "https://github.com/joshua-software-dev/LMeter",
7 | "Tags": ["UI"]
8 | }
--------------------------------------------------------------------------------
/LMeter/src/Act/ActClient.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Game.Gui;
2 | using Dalamud.Plugin;
3 | using LMeter.Config;
4 |
5 |
6 | namespace LMeter.Act;
7 |
8 | public class ActClient
9 | {
10 | private readonly ActConfig _config;
11 | private readonly ChatGui _chatGui;
12 | private readonly DalamudPluginInterface _dpi;
13 |
14 | public IActClient Current;
15 |
16 | public ActClient(ChatGui chatGui, ActConfig config, DalamudPluginInterface dpi)
17 | {
18 | _chatGui = chatGui;
19 | _config = config;
20 | _dpi = dpi;
21 |
22 | Current = GetNewActClient();
23 | }
24 |
25 | public IActClient GetNewActClient()
26 | {
27 | Current?.Dispose();
28 | return Current = _config.IinactMode
29 | ? new IinactClient(_chatGui, _config, _dpi)
30 | : new ActWebSocketClient(_chatGui, _config);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LMeter/src/Act/ActEvent.cs:
--------------------------------------------------------------------------------
1 | using LMeter.Helpers;
2 | using Newtonsoft.Json;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System;
7 |
8 |
9 | namespace LMeter.Act;
10 |
11 | public class ActEvent
12 | {
13 | [JsonIgnore]
14 | private bool _parsedActive = false;
15 |
16 | [JsonIgnore]
17 | private bool _active = false;
18 |
19 | [JsonIgnore]
20 | public DateTime Timestamp;
21 |
22 | [JsonProperty("type")]
23 | public string EventType = string.Empty;
24 |
25 | [JsonProperty("isActive")]
26 | public string IsActive = string.Empty;
27 |
28 | [JsonProperty("Encounter")]
29 | public Encounter? Encounter;
30 |
31 | [JsonProperty("Combatant")]
32 | public Dictionary? Combatants;
33 |
34 | public bool IsEncounterActive()
35 | {
36 | if (_parsedActive) return _active;
37 |
38 | if (bool.TryParse(this.IsActive, out _active)) return false;
39 |
40 | _parsedActive = true;
41 | return _active;
42 | }
43 |
44 | public static ActEvent GetTestData()
45 | {
46 | return new ActEvent()
47 | {
48 | Encounter = Encounter.GetTestData(),
49 | Combatants = Combatant.GetTestData()
50 | };
51 | }
52 | }
53 |
54 | public class Encounter
55 | {
56 | [JsonIgnore]
57 | public static string[] TextTags { get; } =
58 | typeof(Encounter).GetFields().Select(x => $"[{x.Name.ToLower()}]").ToArray();
59 |
60 | [JsonIgnore]
61 | private static readonly Random _rand = new ();
62 |
63 | [JsonIgnore]
64 | private static readonly Dictionary _fields =
65 | typeof(Encounter).GetFields().ToDictionary(x => x.Name.ToLower());
66 |
67 | public string GetFormattedString(string format, string numberFormat) =>
68 | TextTagFormatter.TextTagRegex.Replace(format, new TextTagFormatter(this, numberFormat, _fields).Evaluate);
69 |
70 | [JsonProperty("title")]
71 | public string Title = string.Empty;
72 |
73 | [JsonProperty("duration")]
74 | public string Duration = string.Empty;
75 |
76 | [JsonProperty("DURATION")]
77 | private string _duration = string.Empty;
78 |
79 | [JsonProperty("encdps")]
80 | [JsonConverter(typeof(LazyFloatConverter))]
81 | public LazyFloat? Dps;
82 |
83 | [JsonProperty("damage")]
84 | [JsonConverter(typeof(LazyFloatConverter))]
85 | public LazyFloat? DamageTotal;
86 |
87 | [JsonProperty("enchps")]
88 | [JsonConverter(typeof(LazyFloatConverter))]
89 | public LazyFloat? Hps;
90 |
91 | [JsonProperty("healed")]
92 | [JsonConverter(typeof(LazyFloatConverter))]
93 | public LazyFloat? HealingTotal;
94 |
95 | [JsonProperty("damagetaken")]
96 | [JsonConverter(typeof(LazyFloatConverter))]
97 | public LazyFloat? DamageTaken;
98 |
99 | [JsonProperty("deaths")]
100 | public string? Deaths;
101 |
102 | [JsonProperty("kills")]
103 | public string? Kills;
104 |
105 | public static Encounter GetTestData()
106 | {
107 | float damage = _rand.Next(212345 * 8);
108 | float healing = _rand.Next(41234 * 8);
109 |
110 | return new Encounter()
111 | {
112 | Duration = "00:30",
113 | Title = "Preview",
114 | Dps = new LazyFloat(damage / 30),
115 | Hps = new LazyFloat(healing / 30),
116 | Deaths = "0",
117 | DamageTotal = new LazyFloat(damage),
118 | HealingTotal = new LazyFloat(healing)
119 | };
120 | }
121 | }
122 |
123 | public class Combatant
124 | {
125 | [JsonIgnore]
126 | public static string[] TextTags { get; } =
127 | typeof(Combatant).GetFields().Select(x => $"[{x.Name.ToLower()}]").ToArray();
128 |
129 | [JsonIgnore]
130 | private static readonly Random _rand = new ();
131 |
132 | [JsonIgnore]
133 | private static readonly Dictionary _fields =
134 | typeof(Combatant).GetFields().ToDictionary((x) => x.Name.ToLower());
135 |
136 | public string GetFormattedString(string format, string numberFormat) =>
137 | TextTagFormatter.TextTagRegex.Replace(format, new TextTagFormatter(this, numberFormat, _fields).Evaluate);
138 |
139 | [JsonProperty("name")]
140 | public string Name = string.Empty;
141 |
142 | [JsonIgnore]
143 | public LazyString? Name_First;
144 |
145 | [JsonIgnore]
146 | public LazyString? Name_Last;
147 |
148 | [JsonIgnore]
149 | public string Rank = string.Empty;
150 |
151 | [JsonProperty("job")]
152 | [JsonConverter(typeof(EnumConverter))]
153 | public Job Job;
154 |
155 | [JsonIgnore]
156 | public LazyString? JobName;
157 |
158 | [JsonProperty("duration")]
159 | public string Duration = string.Empty;
160 |
161 | [JsonProperty("encdps")]
162 | [JsonConverter(typeof(LazyFloatConverter))]
163 | public LazyFloat? EncDps;
164 |
165 | [JsonProperty("dps")]
166 | [JsonConverter(typeof(LazyFloatConverter))]
167 | public LazyFloat? Dps;
168 |
169 | [JsonProperty("damage")]
170 | [JsonConverter(typeof(LazyFloatConverter))]
171 | public LazyFloat? DamageTotal;
172 |
173 | [JsonProperty("damage%")]
174 | public string DamagePct = string.Empty;
175 |
176 | [JsonProperty("crithit%")]
177 | public string CritHitPct = string.Empty;
178 |
179 | [JsonProperty("DirectHitPct")]
180 | public string DirectHitPct = string.Empty;
181 |
182 | [JsonProperty("CritDirectHitPct")]
183 | public string CritDirectHitPct = string.Empty;
184 |
185 | [JsonProperty("enchps")]
186 | [JsonConverter(typeof(LazyFloatConverter))]
187 | public LazyFloat? EncHps;
188 |
189 | [JsonProperty("hps")]
190 | [JsonConverter(typeof(LazyFloatConverter))]
191 | public LazyFloat? Hps;
192 |
193 | public LazyFloat? EffectiveHealing;
194 |
195 | [JsonProperty("healed")]
196 | [JsonConverter(typeof(LazyFloatConverter))]
197 | public LazyFloat? HealingTotal;
198 |
199 | [JsonProperty("healed%")]
200 | public string HealingPct = string.Empty;
201 |
202 | [JsonProperty("overHeal")]
203 | [JsonConverter(typeof(LazyFloatConverter))]
204 | public LazyFloat? OverHeal;
205 |
206 | [JsonProperty("OverHealPct")]
207 | public string OverHealPct = string.Empty;
208 |
209 | [JsonProperty("damagetaken")]
210 | [JsonConverter(typeof(LazyFloatConverter))]
211 | public LazyFloat? DamageTaken;
212 |
213 | [JsonProperty("deaths")]
214 | public string Deaths = string.Empty;
215 |
216 | [JsonProperty("kills")]
217 | public string Kills = string.Empty;
218 |
219 | [JsonProperty("maxhit")]
220 | public string MaxHit = string.Empty;
221 |
222 | [JsonProperty("MAXHIT")]
223 | private string _maxHit = string.Empty;
224 |
225 | public LazyString MaxHitName;
226 |
227 | public LazyFloat? MaxHitValue;
228 |
229 | public Combatant()
230 | {
231 | this.Name_First = new LazyString(() => this.Name, LazyStringConverters.FirstName);
232 | this.Name_Last = new LazyString(() => this.Name, LazyStringConverters.LastName);
233 | this.JobName = new LazyString(() => this.Job, LazyStringConverters.JobName);
234 | this.EffectiveHealing = new LazyFloat(() => (this.HealingTotal?.Value ?? 0) - (this.OverHeal?.Value ?? 0));
235 | this.MaxHitName = new LazyString(() => this.MaxHit, LazyStringConverters.MaxHitName);
236 | this.MaxHitValue = new LazyFloat(() => LazyStringConverters.MaxHitValue(this.MaxHit));
237 | }
238 |
239 | public static Dictionary GetTestData()
240 | {
241 | Dictionary mockCombatants = new Dictionary();
242 | mockCombatants.Add("1", GetCombatant("GNB", "DRK", "WAR", "PLD"));
243 | mockCombatants.Add("2", GetCombatant("GNB", "DRK", "WAR", "PLD"));
244 |
245 | mockCombatants.Add("3", GetCombatant("WHM", "AST", "SCH", "SGE"));
246 | mockCombatants.Add("4", GetCombatant("WHM", "AST", "SCH", "SGE"));
247 |
248 | mockCombatants.Add("5", GetCombatant("SAM", "DRG", "MNK", "NIN", "RPR"));
249 | mockCombatants.Add("6", GetCombatant("SAM", "DRG", "MNK", "NIN", "RPR"));
250 | mockCombatants.Add("7", GetCombatant("BLM", "SMN", "RDM"));
251 | mockCombatants.Add("8", GetCombatant("DNC", "MCH", "BRD"));
252 |
253 | return mockCombatants;
254 | }
255 |
256 | private static Combatant GetCombatant(params string[] jobs)
257 | {
258 | int damage = _rand.Next(212345);
259 | int healing = _rand.Next(41234);
260 |
261 | return new Combatant()
262 | {
263 | Name = "Firstname Lastname",
264 | Duration = "00:30",
265 | Job = Enum.Parse(jobs[_rand.Next(jobs.Length)]),
266 | DamageTotal = new LazyFloat(damage.ToString()),
267 | Dps = new LazyFloat((damage / 30).ToString()),
268 | EncDps = new LazyFloat((damage / 30).ToString()),
269 | HealingTotal = new LazyFloat(healing.ToString()),
270 | OverHeal = new LazyFloat(5000),
271 | Hps = new LazyFloat((healing / 30).ToString()),
272 | EncHps = new LazyFloat((healing / 30).ToString()),
273 | DamagePct = "100%",
274 | HealingPct = "100%",
275 | CritHitPct = "20%",
276 | DirectHitPct = "25%",
277 | CritDirectHitPct = "5%",
278 | DamageTaken = new LazyFloat((damage / 20).ToString()),
279 | Deaths = _rand.Next(2).ToString(),
280 | MaxHit = "Full Thrust-42069"
281 | };
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/LMeter/src/Act/ActEventParser.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Logging;
2 | using LMeter.Helpers;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Runtime.CompilerServices;
7 |
8 |
9 | namespace LMeter.Act;
10 |
11 | public class ActEventParser
12 | {
13 | public ActEvent? LastEvent { get; set; }
14 | public List PastEvents { get; set; } = null!;
15 |
16 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
17 | public bool ParseNewEvent(ActEvent? newEvent, int encounterHistorySize)
18 | {
19 | try
20 | {
21 | if
22 | (
23 | newEvent?.Encounter is not null &&
24 | newEvent?.Combatants is not null &&
25 | newEvent.Combatants.Any() &&
26 | (CharacterState.IsInCombat() || !newEvent.IsEncounterActive())
27 | )
28 | {
29 | var lastEventIsDifferentEncounterOrInvalid =
30 | (
31 | LastEvent is not null &&
32 | LastEvent.IsEncounterActive() == newEvent.IsEncounterActive() &&
33 | LastEvent.Encounter is not null &&
34 | LastEvent.Encounter.Duration.Equals(newEvent.Encounter.Duration)
35 | );
36 |
37 | if (!lastEventIsDifferentEncounterOrInvalid)
38 | {
39 | if (!newEvent.IsEncounterActive())
40 | {
41 | PastEvents.Add(newEvent);
42 |
43 | while (PastEvents.Count > encounterHistorySize)
44 | {
45 | PastEvents.RemoveAt(0);
46 | }
47 | }
48 |
49 | newEvent.Timestamp = DateTime.UtcNow;
50 | LastEvent = newEvent;
51 | }
52 | }
53 | }
54 | catch (Exception ex)
55 | {
56 | PluginLog.Verbose(ex.ToString());
57 | return false;
58 | }
59 |
60 | return true;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/LMeter/src/Act/EnumConverter.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 |
4 |
5 | namespace LMeter.Act;
6 |
7 | public class EnumConverter : JsonConverter
8 | {
9 | public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
10 | {
11 | throw new NotImplementedException("Write not supported.");
12 | }
13 |
14 | public override object? ReadJson
15 | (
16 | JsonReader reader,
17 | Type objectType,
18 | object? existingValue,
19 | JsonSerializer serializer
20 | )
21 | {
22 | if (!objectType.IsEnum) return serializer.Deserialize(reader, objectType);
23 |
24 | if (reader.TokenType != JsonToken.String) return 0;
25 |
26 | var value = serializer.Deserialize(reader, typeof(string))?.ToString();
27 | return Enum.TryParse(objectType, value, true, out object? result) ? result : 0;
28 | }
29 |
30 | public override bool CanRead =>
31 | true;
32 |
33 | public override bool CanWrite =>
34 | false;
35 |
36 | public override bool CanConvert(Type objectType) =>
37 | objectType.IsEnum;
38 | }
39 |
--------------------------------------------------------------------------------
/LMeter/src/Act/IActClient.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System;
3 |
4 |
5 | namespace LMeter.Act;
6 |
7 | public interface IActClient : IDisposable
8 | {
9 | public List PastEvents { get; set; }
10 |
11 | public void Clear();
12 | public bool ClientReady();
13 | public bool ConnectionIncompleteOrFailed();
14 | public void DrawConnectionStatus();
15 | public void EndEncounter();
16 | public ActEvent? GetEvent(int index = -1);
17 | public void Start();
18 | public void RetryConnection();
19 | }
20 |
--------------------------------------------------------------------------------
/LMeter/src/Act/LazyFloat.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using System;
3 |
4 |
5 | namespace LMeter.Act;
6 |
7 | public class LazyFloat
8 | {
9 | private readonly Func? _getStringInput;
10 | private readonly Func? _getFloatInput;
11 | private float _value = 0;
12 |
13 | public string? Input { get; private set; }
14 |
15 | public bool WasGenerated { get; private set; }
16 |
17 | public float Value
18 | {
19 | get
20 | {
21 | if (this.WasGenerated) return _value;
22 |
23 | if (this.Input is null)
24 | {
25 | if (_getFloatInput is not null)
26 | {
27 | _value = _getFloatInput.Invoke();
28 | this.WasGenerated = true;
29 | return _value;
30 | }
31 | else if (_getStringInput is not null)
32 | {
33 | this.Input = _getStringInput.Invoke();
34 | }
35 | }
36 |
37 | if
38 | (
39 | float.TryParse(this.Input, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsed) &&
40 | !float.IsNaN(parsed)
41 | )
42 | {
43 | _value = parsed;
44 | }
45 | else
46 | {
47 | _value = 0;
48 | }
49 |
50 | this.WasGenerated = true;
51 | return _value;
52 | }
53 | }
54 |
55 | public LazyFloat(string? input) =>
56 | this.Input = input;
57 |
58 | public LazyFloat(float value)
59 | {
60 | _value = value;
61 | this.WasGenerated = true;
62 | }
63 |
64 | public LazyFloat(Func input) =>
65 | _getFloatInput = input;
66 |
67 | public LazyFloat(Func input) =>
68 | _getStringInput = input;
69 |
70 | public string? ToString(string format, bool kilo) =>
71 | kilo
72 | ? KiloFormat(this.Value, format)
73 | : this.Value.ToString(format, CultureInfo.InvariantCulture);
74 |
75 | public override string? ToString() =>
76 | this.Value.ToString();
77 |
78 | private static string KiloFormat(float num, string format) =>
79 | num switch
80 | {
81 | >= 1000000 => (num / 1000000f).ToString(format, CultureInfo.InvariantCulture) + "M",
82 | >= 1000 => (num / 1000f).ToString(format, CultureInfo.InvariantCulture) + "K",
83 | _ => num.ToString(format, CultureInfo.InvariantCulture)
84 | };
85 | }
86 |
--------------------------------------------------------------------------------
/LMeter/src/Act/LazyFloatConverter.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 |
4 |
5 | namespace LMeter.Act;
6 |
7 | public class LazyFloatConverter : JsonConverter
8 | {
9 | public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) =>
10 | throw new NotImplementedException("Write not supported.");
11 |
12 | public override object? ReadJson
13 | (
14 | JsonReader reader,
15 | Type objectType,
16 | object? existingValue,
17 | JsonSerializer serializer
18 | )
19 | {
20 | if (objectType != typeof(LazyFloat)) return serializer.Deserialize(reader, objectType);
21 |
22 | if (reader.TokenType != JsonToken.String) return new LazyFloat(0f);
23 |
24 | return new LazyFloat(serializer.Deserialize(reader, typeof(string))?.ToString());
25 | }
26 |
27 | public override bool CanRead =>
28 | true;
29 |
30 | public override bool CanWrite =>
31 | false;
32 |
33 | public override bool CanConvert(Type objectType) =>
34 | objectType == typeof(LazyFloat);
35 | }
36 |
--------------------------------------------------------------------------------
/LMeter/src/Act/LazyString.cs:
--------------------------------------------------------------------------------
1 | using LMeter.Helpers;
2 | using System;
3 |
4 |
5 | namespace LMeter.Act;
6 |
7 | public class LazyString
8 | {
9 | private string _value = string.Empty;
10 | private readonly Func _generator;
11 | private readonly Func _getInput;
12 |
13 | public bool WasGenerated { get; private set; }
14 |
15 | public string Value
16 | {
17 | get
18 | {
19 | if (this.WasGenerated) return this._value;
20 |
21 | this._value = this._generator.Invoke(this._getInput.Invoke());
22 | this.WasGenerated = true;
23 | return this._value;
24 | }
25 | }
26 |
27 | public LazyString(Func getInput, Func generator)
28 | {
29 | this._getInput = getInput;
30 | this._generator = generator;
31 | }
32 |
33 | public override string? ToString()
34 | {
35 | return this.Value;
36 | }
37 | }
38 |
39 | public static class LazyStringConverters
40 | {
41 | public static string FirstName(string? input)
42 | {
43 | if (string.IsNullOrWhiteSpace(input)) return string.Empty;
44 |
45 | string[] splits = input.Split(" ");
46 |
47 | if (splits.Length < 2) return input;
48 |
49 | return splits[0];
50 | }
51 |
52 | public static string LastName(string? input)
53 | {
54 | if (string.IsNullOrWhiteSpace(input)) return string.Empty;
55 |
56 | var splits = input.Split(" ");
57 |
58 | if (splits.Length < 2) return string.Empty;
59 |
60 | return splits[1];
61 | }
62 |
63 | public static string MaxHitName(string? input)
64 | {
65 | if (string.IsNullOrWhiteSpace(input)) return string.Empty;
66 |
67 | var splits = input.Split('-');
68 |
69 | if (splits.Length < 2) return input;
70 |
71 | return splits[0];
72 | }
73 |
74 | public static string MaxHitValue(string? input)
75 | {
76 | if (string.IsNullOrWhiteSpace(input)) return string.Empty;
77 |
78 | var splits = input.Split('-');
79 |
80 | if (splits.Length < 2) return input;
81 |
82 | return splits[1];
83 | }
84 |
85 | public static string JobName(Job input) =>
86 | input switch
87 | {
88 | Job.GLA => "Gladiator",
89 | Job.MRD => "Marauder",
90 | Job.PLD => "Paladin",
91 | Job.WAR => "Warrior",
92 | Job.DRK => "Dark Knight",
93 | Job.GNB => "Gunbreaker",
94 |
95 | Job.CNJ => "Conjurer",
96 | Job.WHM => "White Mage",
97 | Job.SCH => "Scholar",
98 | Job.AST => "Astrologian",
99 | Job.SGE => "Sage",
100 |
101 | Job.PGL => "Pugilist",
102 | Job.LNC => "Lancer",
103 | Job.ROG => "Rogue",
104 | Job.MNK => "Monk",
105 | Job.DRG => "Dragoon",
106 | Job.NIN => "Ninja",
107 | Job.SAM => "Samurai",
108 | Job.RPR => "Reaper",
109 |
110 | Job.ARC => "Archer",
111 | Job.BRD => "Bard",
112 | Job.MCH => "Machinist",
113 | Job.DNC => "Dancer",
114 |
115 | Job.THM => "Thaumaturge",
116 | Job.ACN => "Arcanist",
117 | Job.BLM => "Black Mage",
118 | Job.SMN => "Summoner",
119 | Job.RDM => "Red Mage",
120 | Job.BLU => "Blue Mage",
121 |
122 | Job.CRP => "Carpenter",
123 | Job.BSM => "Blacksmith",
124 | Job.ARM => "Armorer",
125 | Job.GSM => "Goldsmith",
126 | Job.LTW => "Leatherworker",
127 | Job.WVR => "Weaver",
128 | Job.ALC => "Alchemist",
129 | Job.CUL => "Culinarian",
130 |
131 | Job.MIN => "Miner",
132 | Job.BOT => "Botanist",
133 | Job.FSH => "Fisher",
134 |
135 | _ => string.Empty
136 | };
137 | }
138 |
--------------------------------------------------------------------------------
/LMeter/src/Act/TextTagFormatter.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Reflection;
3 | using System.Text.RegularExpressions;
4 | using System;
5 |
6 |
7 | namespace LMeter.Act;
8 |
9 | public partial class TextTagFormatter
10 | {
11 | [GeneratedRegex(@"\[(\w*)(:k)?\.?(\d+)?\]", RegexOptions.Compiled)]
12 | private static partial Regex _textTagRegex();
13 | public static Regex TextTagRegex { get; } = _textTagRegex();
14 |
15 | private readonly string _format;
16 | private readonly Dictionary _fields;
17 | private readonly object _source;
18 |
19 | public TextTagFormatter(object source, string format, Dictionary fields) =>
20 | (_source, _format, _fields) = (source, format, fields);
21 |
22 | public string Evaluate(Match m)
23 | {
24 | if (m.Groups.Count != 4) return m.Value;
25 |
26 | var format = string.IsNullOrEmpty(m.Groups[3].Value)
27 | ? $"{_format}0"
28 | : $"{_format}{m.Groups[3].Value}";
29 |
30 | var key = m.Groups[1].Value;
31 | string? value = null;
32 |
33 | if (_fields.TryGetValue(key, out var fieldInfo))
34 | {
35 | object? propValue = fieldInfo.GetValue(_source);
36 |
37 | if (propValue is null) return string.Empty;
38 |
39 | if (propValue is LazyFloat lazyFloat)
40 | {
41 | var kilo = !string.IsNullOrEmpty(m.Groups[2].Value);
42 | value = lazyFloat.ToString(format, kilo) ?? m.Value;
43 | }
44 | else
45 | {
46 | value = propValue?.ToString();
47 |
48 | if
49 | (
50 | !string.IsNullOrEmpty(value) &&
51 | int.TryParse(m.Groups[3].Value, out int trim) &&
52 | trim < value.Length
53 | )
54 | {
55 | value = propValue?.ToString().AsSpan(0, trim).ToString();
56 | }
57 | }
58 | }
59 |
60 | return value ?? m.Value;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/LMeter/src/Cactbot/CactbotRaidbossWindows.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Logging;
2 | using ImGuiNET;
3 | using LMeter.Config;
4 | using LMeter.Helpers;
5 | using System;
6 | using System.Linq;
7 | using System.Numerics;
8 |
9 |
10 | namespace LMeter.Cactbot;
11 |
12 | public class CactbotRaidbossWindows
13 | {
14 | public static void DrawAlerts(CactbotConfig config, TotallyNotCefCactbotHttpSource cactbot, Vector2 pos)
15 | {
16 | var localPos = pos + config.RaidbossAlertsPosition;
17 | var size = config.RaidbossAlertsSize;
18 |
19 | DrawHelpers.DrawInWindow
20 | (
21 | name: "##___LMETER_CACTBOT_ALERTS",
22 | pos: localPos,
23 | size: size,
24 | needsInput: false,
25 | needsFocus: false,
26 | locked: true,
27 | drawAction: drawList =>
28 | {
29 | localPos = ImGui.GetWindowPos();
30 | config.RaidbossAlertsPosition = localPos - pos;
31 |
32 | size = ImGui.GetWindowSize();
33 | config.RaidbossAlertsSize = size;
34 |
35 | try
36 | {
37 | if (config.RaidbossAlertsPreview)
38 | {
39 | ImGui.BeginChild("##CactbotAlertRender", new Vector2(size.X, size.Y), true);
40 | }
41 |
42 | using var bigFontScope = PluginManager.Instance.FontsManager
43 | .PushFont(FontsManager.DefaultBigFontKey);
44 |
45 | var cursorY = 0f;
46 |
47 | var state = config.RaidbossAlertsPreview
48 | ? CactbotState.PreviewState
49 | : cactbot.CactbotState;
50 |
51 | if (config.RaidbossAlarmsEnabled && !string.IsNullOrEmpty(state.Alarm))
52 | {
53 | var textSize = ImGui.CalcTextSize(state.Alarm);
54 | var textWidthOffset = (size.X - textSize.X) * 0.5f;
55 | var textCenteredPos = localPos with
56 | {
57 | X = localPos.X + textWidthOffset,
58 | Y = localPos.Y + cursorY
59 | };
60 |
61 | DrawHelpers.DrawText
62 | (
63 | drawList,
64 | state.Alarm,
65 | textCenteredPos,
66 | 4278190335, // red
67 | config.RaidbossAlarmTextOutlineThickness > 0,
68 | thickness: (int) config.RaidbossAlarmTextOutlineThickness
69 | );
70 |
71 | cursorY += textSize.Y + 10;
72 | }
73 |
74 | if (config.RaidbossAlertsEnabled && !string.IsNullOrEmpty(state.Alert))
75 | {
76 | var textSize = ImGui.CalcTextSize(state.Alert);
77 | var textWidthOffset = (size.X - textSize.X) * 0.5f;
78 | var textCenteredPos = localPos with
79 | {
80 | X = localPos.X + textWidthOffset,
81 | Y = localPos.Y + cursorY
82 | };
83 |
84 | DrawHelpers.DrawText
85 | (
86 | drawList,
87 | state.Alert,
88 | textCenteredPos,
89 | 4278255615, // yellow
90 | config.RaidbossAlertsTextOutlineThickness > 0,
91 | thickness: (int) config.RaidbossAlertsTextOutlineThickness
92 | );
93 |
94 | cursorY += textSize.Y + 10;
95 | }
96 |
97 | if (config.RaidbossInfoEnabled && !string.IsNullOrEmpty(state.Info))
98 | {
99 | var textSize = ImGui.CalcTextSize(state.Info);
100 | var textWidthOffset = (size.X - textSize.X) * 0.5f;
101 | var textCenteredPos = localPos with
102 | {
103 | X = localPos.X + textWidthOffset,
104 | Y = localPos.Y + cursorY
105 | };
106 |
107 | DrawHelpers.DrawText
108 | (
109 | drawList,
110 | state.Info,
111 | textCenteredPos,
112 | 4278255360, // green
113 | config.RaidbossInfoTextOutlineThickness > 0,
114 | thickness: (int) config.RaidbossInfoTextOutlineThickness
115 | );
116 |
117 | cursorY += textSize.Y + 10;
118 | }
119 | }
120 | finally
121 | {
122 | if (config.RaidbossAlertsPreview) ImGui.EndChild();
123 | }
124 | }
125 | );
126 | }
127 |
128 | private static void DrawColoredProgressBar(CactbotConfig config, CactbotTimeLineElement timelineInfo, Vector2 size)
129 | {
130 | var remainingTime = timelineInfo.ApproxCompletionTime - DateTime.Now;
131 | var progress = (float)
132 | (
133 | remainingTime.TotalSeconds /
134 | timelineInfo.OriginalRemainingTime.TotalSeconds
135 | );
136 |
137 | if (config.RaidbossTimelinePreview) progress = 0.5f;
138 |
139 | if (timelineInfo.StyleFill == "fill")
140 | {
141 | if (timelineInfo.RgbValue != null)
142 | {
143 | ImGui.PushStyleColor(ImGuiCol.PlotHistogram, timelineInfo.RgbValue.Value);
144 | }
145 |
146 | ImGui.ProgressBar
147 | (
148 | 1 - progress,
149 | size,
150 | $"{timelineInfo.LeftText} : {remainingTime:mm\\:ss\\.ff}"
151 | );
152 |
153 | if (timelineInfo.RgbValue != null)
154 | {
155 | ImGui.PopStyleColor();
156 | }
157 | }
158 | else
159 | {
160 | if (timelineInfo.RgbValue != null)
161 | {
162 | ImGui.PushStyleColor(ImGuiCol.PlotHistogram, timelineInfo.RgbValue.Value);
163 | }
164 |
165 | ImGui.ProgressBar
166 | (
167 | progress,
168 | size,
169 | $"{timelineInfo.LeftText} : {remainingTime:mm\\:ss\\.ff}"
170 | );
171 |
172 | if (timelineInfo.RgbValue != null)
173 | {
174 | ImGui.PopStyleColor();
175 | }
176 | }
177 | }
178 |
179 | public static void DrawTimeline(CactbotConfig config, TotallyNotCefCactbotHttpSource cactbot, Vector2 pos)
180 | {
181 | var localPos = pos + config.RaidbossTimelinePosition;
182 | var size = config.RaidbossTimelineSize;
183 |
184 | DrawHelpers.DrawInWindow
185 | (
186 | name: "##___LMETER_CACTBOT_TIMELINE",
187 | pos: localPos,
188 | size: size,
189 | needsInput: false,
190 | needsFocus: false,
191 | locked: true,
192 | drawAction: _ =>
193 | {
194 | try
195 | {
196 | if (config.RaidbossTimelinePreview)
197 | {
198 | ImGui.BeginChild("##CactbotTimelineRender", new Vector2(size.X, size.Y), true);
199 | }
200 |
201 | var windowWidth = ImGui.GetWindowSize().X;
202 | var barWidth = windowWidth * 0.8f;
203 | var progressBarSize = new Vector2(barWidth, 30);
204 |
205 | var state = config.RaidbossTimelinePreview
206 | ? CactbotState.PreviewState
207 | : cactbot.CactbotState;
208 |
209 | if
210 | (
211 | !config.RaidbossTimelinePreview &&
212 | cactbot.ConnectionState != TotallyNotCefConnectionState.Connected &&
213 | !state.Timeline.IsEmpty
214 | )
215 | {
216 | PluginLog.Log("Lost connection to TotallyNotCef, clearing lingering timeline events...");
217 | // Ensure lingering timers aren't left rendering.
218 | state.Timeline.Clear();
219 | }
220 |
221 | foreach (var key in state.Timeline.Keys.OrderBy(it => it))
222 | {
223 | state.Timeline.TryGetValue(key, out var timelineInfo);
224 | if (timelineInfo == null) continue;
225 | DrawColoredProgressBar(config, timelineInfo, progressBarSize);
226 | }
227 | }
228 | finally
229 | {
230 | if (config.RaidbossTimelinePreview) ImGui.EndChild();
231 | }
232 | }
233 | );
234 | }
235 |
236 | public static void Draw(Vector2 pos)
237 | {
238 | var config = PluginManager.Instance.CactbotConfig;
239 | var cactbot = config.Cactbot;
240 | if (cactbot == null) return;
241 | else if (!config.EnableConnection && !config.RaidbossAlertsPreview && !config.RaidbossTimelinePreview) return;
242 |
243 | cactbot.PollingRate = CharacterState.IsInCombat()
244 | ? config.RaidbossInCombatPollingRate
245 | : config.RaidbossOutOfCombatPollingRate;
246 |
247 | DrawAlerts(config, cactbot, pos);
248 | if (config.RaidbossTimelineEnabled)
249 | {
250 | DrawTimeline(config, cactbot, pos);
251 | }
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/LMeter/src/Cactbot/CactbotState.cs:
--------------------------------------------------------------------------------
1 | using AngleSharp.Dom;
2 | using AngleSharp.Html.Dom;
3 | using Dalamud.Game.Text;
4 | using System;
5 | using System.Collections.Concurrent;
6 | using System.Collections.Generic;
7 | using System.Collections.Specialized;
8 | using System.Text;
9 |
10 |
11 | namespace LMeter.Cactbot;
12 |
13 | public class CactbotState
14 | {
15 | public static CactbotState PreviewState = new (preview: true);
16 | public string? Alarm { get; private set; }
17 | public string? Alert { get; private set; }
18 | public string? Info { get; private set; }
19 | public event EventHandler? AlarmStateChanged = null;
20 | public event EventHandler? AlertStateChanged = null;
21 | public event EventHandler? InfoStateChanged = null;
22 | public readonly ConcurrentDictionary Timeline = new ();
23 |
24 | public CactbotState()
25 | {
26 | AlarmStateChanged += OnAlarmStateChange;
27 | AlertStateChanged += OnAlertStateChange;
28 | InfoStateChanged += OnInfoStateChange;
29 | }
30 |
31 | public CactbotState(bool preview)
32 | {
33 | Alarm = "ALARM!";
34 | Alert = "ALERT!";
35 | Info = "INFO!";
36 |
37 | Timeline[1] = new CactbotTimeLineElement(1);
38 | Timeline[2] = new CactbotTimeLineElement(2);
39 | Timeline[3] = new CactbotTimeLineElement(3);
40 | }
41 |
42 | private void OnAlarmStateChange(object? sender, EventArgs eventArgs)
43 | {
44 | if (!PluginManager.Instance.CactbotConfig.RaidbossAlarmsInChatEnabled || this.Alarm == null) return;
45 |
46 | var message = new XivChatEntry
47 | {
48 | Message = $"RAIDBOSS ALARM: {Alarm}",
49 | Type = XivChatType.ErrorMessage
50 | };
51 | PluginManager.Instance.ChatGui.PrintChat(message);
52 | }
53 |
54 | private void OnAlertStateChange(object? sender, EventArgs eventArgs)
55 | {
56 | if (!PluginManager.Instance.CactbotConfig.RaidbossAlertsInChatEnabled || this.Alert == null) return;
57 |
58 | var message = new XivChatEntry
59 | {
60 | Message = Alert,
61 | Name = "RAIDBOSS ALERT",
62 | Type = XivChatType.Yell
63 | };
64 | PluginManager.Instance.ChatGui.PrintChat(message);
65 | }
66 |
67 | private void OnInfoStateChange(object? sender, EventArgs eventArgs)
68 | {
69 | if (!PluginManager.Instance.CactbotConfig.RaidbossInfoInChatEnabled || this.Info == null) return;
70 |
71 | var message = new XivChatEntry
72 | {
73 | Message = Info,
74 | Name = "RAIDBOSS INFO",
75 | Type = XivChatType.NPCDialogueAnnouncements
76 | };
77 | PluginManager.Instance.ChatGui.PrintChat(message);
78 | }
79 |
80 | private void UpdateTimeline(IHtmlDocument html)
81 | {
82 | var timeline = html.GetElementById("timeline");
83 | if (timeline == null) return;
84 |
85 | var currentIds = new Dictionary();
86 | foreach (var container in timeline.GetElementsByClassName("timer-bar"))
87 | {
88 | if (container == null) continue;
89 | var parsedContainer = new CactbotTimeLineElement(container);
90 |
91 | if (Timeline.TryGetValue(parsedContainer.ContainerId, out var existingTimer))
92 | {
93 | existingTimer.Update(parsedContainer);
94 | }
95 | else
96 | {
97 | Timeline[parsedContainer.ContainerId] = parsedContainer;
98 | }
99 |
100 | currentIds[parsedContainer.ContainerId] = true;
101 | }
102 |
103 | // TODO: Find a way to remove multiple keys atomically. This works, but
104 | // only because there is only one other accessor, who exclusively reads
105 | // by making a complete copy of the keys whenever it iterates.
106 | foreach (var key in Timeline.Keys)
107 | {
108 | if (!currentIds.ContainsKey(key))
109 | {
110 | Timeline.TryRemove(key, out var _);
111 | }
112 | }
113 | }
114 |
115 | private string GetHolderTextContent(IElement? holder)
116 | {
117 | if (holder == null || holder.ChildElementCount < 1) return string.Empty;
118 | if (holder.ChildElementCount > 1)
119 | {
120 | var set = new OrderedDictionary();
121 | foreach (var child in holder.Children)
122 | {
123 | set[child.TextContent.Trim()] = string.Empty;
124 | }
125 |
126 | var sb = new StringBuilder();
127 | var i = 0;
128 | foreach (var result in set)
129 | {
130 | if (i > 0) sb.Append('\n');
131 | sb.Append(((System.Collections.DictionaryEntry) result).Key);
132 | i += 1;
133 | }
134 |
135 | return sb.ToString();
136 | }
137 |
138 | return holder.TextContent.Trim();
139 | }
140 |
141 | public void UpdateState(IHtmlDocument? html)
142 | {
143 | if (html == null)
144 | {
145 | Alarm = null;
146 | Alert = null;
147 | Info = null;
148 | Timeline.Clear();
149 | return;
150 | }
151 |
152 | UpdateTimeline(html);
153 |
154 | var alarmContainer = html.GetElementById("popup-text-alarm");
155 | var alarm = alarmContainer?.GetElementsByClassName("holder")?[0];
156 | var alertContainer = html.GetElementById("popup-text-alert");
157 | var alert = alertContainer?.GetElementsByClassName("holder")?[0];
158 | var infoContainer = html.GetElementById("popup-text-info");
159 | var info = infoContainer?.GetElementsByClassName("holder")?[0];
160 |
161 | var alarmWasEmpty = string.IsNullOrEmpty(Alarm);
162 | Alarm = GetHolderTextContent(alarm);
163 | if (alarmWasEmpty && !string.IsNullOrEmpty(Alarm))
164 | {
165 | AlarmStateChanged?.Invoke(this, EventArgs.Empty);
166 | }
167 | else if (Alarm == null && alarmWasEmpty)
168 | {
169 | AlarmStateChanged?.Invoke(this, EventArgs.Empty);
170 | }
171 |
172 | var alertWasEmpty = string.IsNullOrEmpty(Alert);
173 | Alert = GetHolderTextContent(alert);
174 | if (alertWasEmpty && !string.IsNullOrEmpty(Alert))
175 | {
176 | AlertStateChanged?.Invoke(this, EventArgs.Empty);
177 | }
178 | else if (Alert == null && alertWasEmpty)
179 | {
180 | AlertStateChanged?.Invoke(this, EventArgs.Empty);
181 | }
182 |
183 | var infoWasEmpty = string.IsNullOrEmpty(Info);
184 | Info = GetHolderTextContent(info);
185 | if (infoWasEmpty && !string.IsNullOrEmpty(Info))
186 | {
187 | InfoStateChanged?.Invoke(this, EventArgs.Empty);
188 | }
189 | else if (Info == null && infoWasEmpty)
190 | {
191 | InfoStateChanged?.Invoke(this, EventArgs.Empty);
192 | }
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/LMeter/src/Cactbot/CactbotTimeLineElement.cs:
--------------------------------------------------------------------------------
1 | using AngleSharp.Dom;
2 | using ImGuiNET;
3 | using System;
4 | using System.Numerics;
5 | using System.Text.RegularExpressions;
6 |
7 |
8 | namespace LMeter.Cactbot;
9 |
10 | public partial class CactbotTimeLineElement
11 | {
12 | [GeneratedRegex("rgb\\(([0-9]{1,3})[\\w \\n]?\\,[\\w \\n]?([0-9]{1,3})[\\w \\n]?\\,[\\w \\n]?([0-9]{1,3})[\\w \\n]?\\)")]
13 | private static partial Regex _preCompiledRgbRegex();
14 | private readonly static Regex RgbRegex = _preCompiledRgbRegex();
15 |
16 | [GeneratedRegex("order: (\\d+)")]
17 | private static partial Regex _preCompiledStyleToIdRegex();
18 | private readonly static Regex StyleToIdRegex = _preCompiledStyleToIdRegex();
19 |
20 | public int ContainerId;
21 | public string? ContainerStyle;
22 | public double Duration;
23 | public double Value;
24 | public string? RightText;
25 | public string? LeftText;
26 | public string? Toward;
27 | public string? StyleFill;
28 | public string? Fg;
29 | public TimeSpan OriginalRemainingTime;
30 | public DateTime ApproxCompletionTime;
31 | public uint? RgbValue;
32 |
33 | public CactbotTimeLineElement(int containerId)
34 | {
35 | ContainerId = containerId;
36 | Duration = 0;
37 | Value = 0;
38 | RgbValue = 4294936712; // rgb(136, 136, 255);
39 | }
40 |
41 | public CactbotTimeLineElement(IElement container)
42 | {
43 | ContainerStyle = container.GetAttribute("style");
44 | ContainerId = GetIdFromContainerStyle(ContainerStyle);
45 |
46 | var timerBar = container.GetElementsByTagName("timer-bar")[0];
47 |
48 | Duration = -1;
49 | if (double.TryParse(timerBar.GetAttribute("duration"), out var durationFloat))
50 | {
51 | Duration = durationFloat;
52 | }
53 |
54 | Value = -1;
55 | if (double.TryParse(timerBar.GetAttribute("value"), out var valueFloat))
56 | {
57 | Value = valueFloat;
58 | }
59 |
60 | RightText = timerBar.GetAttribute("righttext");
61 | LeftText = timerBar.GetAttribute("lefttext");
62 | Toward = timerBar.GetAttribute("toward");
63 | StyleFill = timerBar.GetAttribute("stylefill");
64 | Fg = timerBar.GetAttribute("fg");
65 |
66 | OriginalRemainingTime = TimeSpan.FromSeconds(Value);
67 | ApproxCompletionTime = DateTime.Now + OriginalRemainingTime;
68 | RgbValue = GetRgbFromInternalStyle(Fg);
69 | }
70 |
71 | private int GetIdFromContainerStyle(string? containerStyle)
72 | {
73 | if (containerStyle == null) return -1;
74 | var match = StyleToIdRegex.Match(containerStyle);
75 | if (!match.Success) return -1;
76 |
77 | const string orderCssPrefix = "order: ";
78 | if
79 | (
80 | int.TryParse
81 | (
82 | match.ValueSpan[(match.ValueSpan.IndexOf(orderCssPrefix) + orderCssPrefix.Length)..],
83 | out var id
84 | )
85 | )
86 | {
87 | return id;
88 | }
89 |
90 | return -1;
91 | }
92 |
93 | private uint? GetRgbFromInternalStyle(string? fg)
94 | {
95 | if (fg == null) return null;
96 | var match = RgbRegex.Match(fg);
97 | if (!match.Success) return null;
98 |
99 | var rgbValues = new ushort [3];
100 | var i = 0;
101 | foreach (var groupObj in match.Groups)
102 | {
103 | if (groupObj is Group group)
104 | {
105 | if (ushort.TryParse(group.Value, out var partialRgbNum))
106 | {
107 | rgbValues[i] = partialRgbNum;
108 | i += 1;
109 | }
110 | }
111 | }
112 |
113 | if (rgbValues.Length == 3)
114 | {
115 | return ImGui.GetColorU32(new Vector4(rgbValues[0] / 255f, rgbValues[1] / 255f, rgbValues[2] / 255f, 1f));
116 | }
117 |
118 | return null;
119 | }
120 |
121 | public void Update(CactbotTimeLineElement newlyParsed)
122 | {
123 | if (newlyParsed.Duration != Duration)
124 | {
125 | OriginalRemainingTime = TimeSpan.FromSeconds(newlyParsed.Value);
126 | ApproxCompletionTime = DateTime.Now + OriginalRemainingTime;
127 | }
128 |
129 | ContainerId = newlyParsed.ContainerId;
130 | ContainerStyle = newlyParsed.ContainerStyle;
131 | Duration = newlyParsed.Duration;
132 | Value = newlyParsed.Value;
133 | RightText = newlyParsed.RightText;
134 | LeftText = newlyParsed.LeftText;
135 | Toward = newlyParsed.Toward;
136 | StyleFill = newlyParsed.StyleFill;
137 | Fg = newlyParsed.Fg;
138 | RgbValue = GetRgbFromInternalStyle(newlyParsed.Fg);
139 | }
140 |
141 | public override string ToString() =>
142 | $"""
143 | ContainerId: {ContainerId}
144 | ContainerStyle: {ContainerStyle}
145 | Duration: {Duration}
146 | Value: {Value}
147 | RightText: {RightText}
148 | LeftText: {LeftText}
149 | Toward: {Toward}
150 | StyleFill: {StyleFill}
151 | Fg: {Fg}
152 | OriginalRemainingTime: {OriginalRemainingTime}
153 | ApproxCompletionTime: {ApproxCompletionTime}
154 | RgbValue: {RgbValue}
155 | """;
156 | }
157 |
--------------------------------------------------------------------------------
/LMeter/src/Cactbot/TotallyNotCefBrowserState.cs:
--------------------------------------------------------------------------------
1 | namespace LMeter.Cactbot;
2 |
3 | public enum TotallyNotCefBrowserState
4 | {
5 | NotStarted,
6 | Downloading,
7 | Starting,
8 | Running,
9 | NotRunning
10 | }
11 |
--------------------------------------------------------------------------------
/LMeter/src/Cactbot/TotallyNotCefConnectionState.cs:
--------------------------------------------------------------------------------
1 | namespace LMeter;
2 |
3 | public enum TotallyNotCefConnectionState
4 | {
5 | Disabled,
6 | WaitingForConnection,
7 | AttemptingHandshake,
8 | BadConnectionHealth,
9 | Connected,
10 | Disconnected
11 | }
12 |
--------------------------------------------------------------------------------
/LMeter/src/Cactbot/TotallyNotCefHealthCheckResponse.cs:
--------------------------------------------------------------------------------
1 | namespace LMeter;
2 |
3 | public enum TotallyNotCefHealthCheckResponse
4 | {
5 | Unverified,
6 | NoResponse,
7 | InvalidResponse,
8 | CorrectResponse,
9 | }
10 |
--------------------------------------------------------------------------------
/LMeter/src/Config/AboutPage.cs:
--------------------------------------------------------------------------------
1 | using ImGuiNET;
2 | using LMeter.Helpers;
3 | using System.Numerics;
4 |
5 |
6 | namespace LMeter.Config;
7 |
8 | public class AboutPage : IConfigPage
9 | {
10 | public string Name =>
11 | "About / Changelog";
12 |
13 | public IConfigPage GetDefault() =>
14 | new AboutPage();
15 |
16 | public void DrawConfig(Vector2 size, float padX, float padY)
17 | {
18 | try
19 | {
20 | if (ImGui.BeginChild("##AboutPage", new Vector2(size.X, size.Y), true))
21 | {
22 | var headerSize = Vector2.Zero;
23 | if (Plugin.IconTexture is not null)
24 | {
25 | var iconSize = new Vector2(Plugin.IconTexture.Width, Plugin.IconTexture.Height);
26 | string versionText =
27 | $"""
28 | LMeter
29 | v{Plugin.Version}
30 | git: {Plugin.GitHash}
31 | """;
32 | var textSize = ImGui.CalcTextSize(versionText);
33 | headerSize = new Vector2(size.X, iconSize.Y + textSize.Y);
34 |
35 | var iconActivated = false;
36 | try
37 | {
38 | iconActivated = ImGui.BeginChild("##Icon", headerSize, false);
39 |
40 | if (iconActivated)
41 | {
42 | ImDrawListPtr drawList = ImGui.GetWindowDrawList();
43 | Vector2 pos = ImGui.GetWindowPos().AddX(size.X / 2 - iconSize.X / 2);
44 | drawList.AddImage(Plugin.IconTexture.ImGuiHandle, pos, pos + iconSize);
45 | Vector2 textPos = ImGui.GetWindowPos().AddX(size.X / 2 - textSize.X / 2).AddY(iconSize.Y);
46 | drawList.AddText(textPos, 0xFFFFFFFF, versionText);
47 | }
48 | }
49 | finally
50 | {
51 | if (iconActivated) ImGui.EndChild();
52 | }
53 | }
54 |
55 | ImGui.Text("Changelog");
56 | var changeLogSize = new Vector2(size.X - padX * 2, size.Y - ImGui.GetCursorPosY() - padY - 30);
57 |
58 | if (ImGui.BeginChild("##Changelog", changeLogSize, true))
59 | {
60 | ImGui.Text(Plugin.Changelog);
61 | ImGui.EndChild();
62 | }
63 | ImGui.NewLine();
64 |
65 | var buttonSize = new Vector2
66 | (
67 | x: (size.X - padX * 2 - padX * 2) / 3,
68 | y: 30 - padY * 2
69 | );
70 |
71 | ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0);
72 | ImGui.SameLine((size.X - (buttonSize.X * 2)) * 0.5f); // start buttons centered
73 | if (ImGui.Button("Github", buttonSize))
74 | {
75 | Utils.OpenUrl(MagicValues.GitRepoUrl);
76 | }
77 |
78 | ImGui.SameLine();
79 | if (ImGui.Button("Discord", buttonSize))
80 | {
81 | Utils.OpenUrl(MagicValues.DiscordUrl);
82 | }
83 | ImGui.PopStyleVar();
84 | }
85 | }
86 | finally
87 | {
88 | ImGui.EndChild();
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/LMeter/src/Config/ActConfig.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Interface;
2 | using ImGuiNET;
3 | using LMeter.Helpers;
4 | using LMeter.Runtime;
5 | using Newtonsoft.Json;
6 | using System;
7 | using System.Numerics;
8 |
9 |
10 | namespace LMeter.Config;
11 |
12 | [JsonObject("ACTConfig")]
13 | public class ActConfig : IConfigPage
14 | {
15 | [JsonIgnore]
16 | private const string DefaultSocketAddress = "ws://127.0.0.1:10501/ws";
17 |
18 | [JsonIgnore]
19 | private DateTime? LastCombatTime { get; set; } = null;
20 |
21 | [JsonIgnore]
22 | private DateTime? LastReconnectAttempt { get; set; } = null;
23 | public string Name =>
24 | "ACT";
25 |
26 | [JsonProperty("IINACTMode")]
27 | public bool IinactMode = false;
28 |
29 | [JsonProperty("ACTSocketAddress")]
30 | public string ActSocketAddress;
31 | public int EncounterHistorySize = 15;
32 | public bool AutoReconnect = false;
33 | public bool WaitForCharacterLogin = false;
34 | public int ReconnectDelay = 30;
35 |
36 | [JsonProperty("ClearACT")]
37 | public bool ClearAct = false;
38 | public bool AutoEnd = false;
39 | public int AutoEndDelay = 3;
40 |
41 | [JsonIgnore]
42 | private bool? WebSocketFixApplied = null;
43 |
44 | public IConfigPage GetDefault() =>
45 | new ActConfig();
46 |
47 | public ActConfig() =>
48 | this.ActSocketAddress = DefaultSocketAddress;
49 |
50 | public void DrawConfig(Vector2 size, float padX, float padY)
51 | {
52 | if (!ImGui.BeginChild($"##{this.Name}", new Vector2(size.X, size.Y), true))
53 | {
54 | ImGui.EndChild();
55 | return;
56 | }
57 |
58 | ImGui.Text("ACT Connection Mode:");
59 |
60 | var newClientRequested = false;
61 | var iinactModeNum = IinactMode ? 1 : 0;
62 |
63 | newClientRequested |= ImGui.RadioButton("ACT WebSocket", ref iinactModeNum, 0);
64 | ImGui.SameLine();
65 | newClientRequested |= ImGui.RadioButton("IINACT", ref iinactModeNum, 1);
66 |
67 | IinactMode = iinactModeNum == 1;
68 | if (newClientRequested)
69 | {
70 | PluginManager.Instance.ActClient.GetNewActClient();
71 | PluginManager.Instance.ActClient.Current.Start();
72 | }
73 |
74 | PluginManager.Instance.ActClient.Current.DrawConnectionStatus();
75 | if (!IinactMode)
76 | {
77 | ImGui.Text("WebSockets Functional: ");
78 | ImGui.SameLine();
79 | ImGui.PushFont(UiBuilder.IconFont);
80 | if (ShaFixer.ValidateSha1IsFunctional())
81 | {
82 | ImGui.Text("");
83 | ImGui.PopFont();
84 | }
85 | else if (ShaFixer.CanRuntimeBeFixed())
86 | {
87 | ImGui.Text("");
88 | ImGui.PopFont();
89 | ImGui.TextColored
90 | (
91 | new Vector4(255, 0, 0, 255),
92 | """
93 | Your Wine runtime cannot correctly perform the SHA1 hashing required for a
94 | functional Web Socket client. A Web Socket is used by this plugin to connect to
95 | ACT to retrieve data, and this functionality has notable limitations without
96 | it. Other plugins such as `Who's Talking` and `TextToTalk` also use Web Sockets
97 | to provide features that will not work with SHA hashing in this state. LMeter
98 | can also attempt to fix this SHA hashing for you, but while the fix IS safe, it
99 | is also INVASIVE, and will modify the C# runtime used for all plugins. For this
100 | reason, LMeter is asking for your authorization before applying the fix. If you
101 | wish to authorize the fix, click the wrench button. You will need to restart
102 | your client after the fix is applied for it to take effect.
103 |
104 | If you do not wish to authorize the installation of the fix, try other Wine
105 | runtimes to see if they fix the problem for you, or consider using IPC mode
106 | with IINACT instead.
107 | """
108 | );
109 | ImGui.Text("Apply Fix:");
110 | ImGui.SameLine();
111 | ImGui.SetWindowFontScale(0.8f);
112 | DrawHelpers.DrawButton
113 | (
114 | string.Empty,
115 | FontAwesomeIcon.Wrench,
116 | () => WebSocketFixApplied = ShaFixer.ModifyRuntimeWithShaFix(),
117 | null,
118 | new Vector2(20, 20)
119 | );
120 | ImGui.SetWindowFontScale(1);
121 |
122 | if (WebSocketFixApplied != null)
123 | {
124 | if (WebSocketFixApplied.Value)
125 | {
126 | ImGui.Text("Fix successfully applied!");
127 | }
128 | else
129 | {
130 | ImGui.Text("Failed to apply fix...");
131 | }
132 | }
133 | }
134 |
135 | ImGui.InputTextWithHint
136 | (
137 | "ACT Websocket Address",
138 | $"Default: '{DefaultSocketAddress}'",
139 | ref this.ActSocketAddress,
140 | 64
141 | );
142 | }
143 |
144 | var buttonSize = new Vector2(40, 0);
145 | DrawHelpers.DrawButton
146 | (
147 | string.Empty,
148 | FontAwesomeIcon.Sync,
149 | PluginManager.Instance.ActClient.Current.RetryConnection,
150 | "Reconnect",
151 | buttonSize
152 | );
153 | ImGui.SameLine();
154 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 1f);
155 | ImGui.Text("Retry Connection");
156 |
157 | ImGui.NewLine();
158 | ImGui.Checkbox("Automatically attempt to reconnect if connection fails", ref this.AutoReconnect);
159 | if (this.AutoReconnect)
160 | {
161 | DrawHelpers.DrawNestIndicator(1);
162 | ImGui.PushItemWidth(30);
163 | ImGui.InputInt("Seconds between reconnect attempts", ref this.ReconnectDelay, 0, 0);
164 | ImGui.PopItemWidth();
165 | }
166 | ImGui.Checkbox("Wait until after logging into character to connect to ACT", ref this.WaitForCharacterLogin);
167 |
168 | ImGui.NewLine();
169 | ImGui.PushItemWidth(30);
170 | ImGui.InputInt("Number of Encounters to save", ref this.EncounterHistorySize, 0, 0);
171 | ImGui.PopItemWidth();
172 |
173 | ImGui.NewLine();
174 | ImGui.Checkbox("Clear ACT when clearing LMeter", ref this.ClearAct);
175 | ImGui.Checkbox("Force ACT to end encounter after combat", ref this.AutoEnd);
176 | if (ImGui.IsItemHovered())
177 | {
178 | ImGui.SetTooltip
179 | (
180 | """
181 | It is recommended to disable ACT Command Sounds if you use this feature.
182 | The option can be found in ACT under Options -> Sound Settings.
183 | """
184 | );
185 | }
186 |
187 | if (this.AutoEnd)
188 | {
189 | DrawHelpers.DrawNestIndicator(1);
190 | ImGui.PushItemWidth(30);
191 | ImGui.InputInt("Seconds delay after combat", ref this.AutoEndDelay, 0, 0);
192 | ImGui.PopItemWidth();
193 | }
194 |
195 | ImGui.NewLine();
196 | DrawHelpers.DrawButton
197 | (
198 | string.Empty,
199 | FontAwesomeIcon.Stop,
200 | PluginManager.Instance.ActClient.Current.EndEncounter,
201 | null,
202 | buttonSize
203 | );
204 | ImGui.SameLine();
205 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 1f);
206 | ImGui.Text("Force End Combat");
207 |
208 | DrawHelpers.DrawButton
209 | (
210 | string.Empty,
211 | FontAwesomeIcon.Trash,
212 | PluginManager.Instance.Clear,
213 | null,
214 | buttonSize
215 | );
216 | ImGui.SameLine();
217 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 1f);
218 | ImGui.Text("Clear LMeter");
219 |
220 | ImGui.EndChild();
221 | }
222 |
223 | public void TryReconnect()
224 | {
225 | if
226 | (
227 | this.LastReconnectAttempt.HasValue &&
228 | PluginManager.Instance.ActClient.Current.ConnectionIncompleteOrFailed()
229 | )
230 | {
231 | if
232 | (
233 | this.AutoReconnect &&
234 | this.LastReconnectAttempt < DateTime.UtcNow - TimeSpan.FromSeconds(this.ReconnectDelay)
235 | )
236 | {
237 | PluginManager.Instance.ActClient.Current.RetryConnection();
238 | this.LastReconnectAttempt = DateTime.UtcNow;
239 | }
240 | }
241 | else
242 | {
243 | this.LastReconnectAttempt = DateTime.UtcNow;
244 | }
245 | }
246 |
247 | public void TryEndEncounter()
248 | {
249 | if (PluginManager.Instance.ActClient.Current.ClientReady())
250 | {
251 | if (this.AutoEnd && CharacterState.IsInCombat())
252 | {
253 | this.LastCombatTime = DateTime.UtcNow;
254 | }
255 | else if
256 | (
257 | this.LastCombatTime is not null &&
258 | this.LastCombatTime < DateTime.UtcNow - TimeSpan.FromSeconds(this.AutoEndDelay)
259 | )
260 | {
261 | PluginManager.Instance.ActClient.Current.EndEncounter();
262 | this.LastCombatTime = null;
263 | }
264 | }
265 | }
266 | }
267 |
268 |
269 | // dummy class to work around serialization dumbness
270 | public class ACTConfig : ActConfig { }
271 |
--------------------------------------------------------------------------------
/LMeter/src/Config/BarColorsConfig.cs:
--------------------------------------------------------------------------------
1 | using ImGuiNET;
2 | using LMeter.Helpers;
3 | using System.Numerics;
4 |
5 |
6 | namespace LMeter.Config;
7 |
8 | public class BarColorsConfig : IConfigPage
9 | {
10 | public string Name => "Colors";
11 |
12 | public IConfigPage GetDefault() =>
13 | new BarColorsConfig();
14 |
15 | public ConfigColor PLDColor = new (r: 168f / 255f, g: 210f / 255f, b: 230f / 255f, a: 1f);
16 | public ConfigColor DRKColor = new (r: 209f / 255f, g: 38f / 255f, b: 204f / 255f, a: 1f);
17 | public ConfigColor WARColor = new (r: 207f / 255f, g: 38f / 255f, b: 33f / 255f, a: 1f);
18 | public ConfigColor GNBColor = new (r: 121f / 255f, g: 109f / 255f, b: 48f / 255f, a: 1f);
19 | public ConfigColor GLAColor = new (r: 168f / 255f, g: 210f / 255f, b: 230f / 255f, a: 1f);
20 | public ConfigColor MRDColor = new (r: 207f / 255f, g: 38f / 255f, b: 33f / 255f, a: 1f);
21 |
22 | public ConfigColor SCHColor = new (r: 134f / 255f, g: 87f / 255f, b: 255f / 255f, a: 1f);
23 | public ConfigColor WHMColor = new (r: 255f / 255f, g: 240f / 255f, b: 220f / 255f, a: 1f);
24 | public ConfigColor ASTColor = new (r: 255f / 255f, g: 231f / 255f, b: 74f / 255f, a: 1f);
25 | public ConfigColor SGEColor = new (r: 144f / 255f, g: 176f / 255f, b: 255f / 255f, a: 1f);
26 | public ConfigColor CNJColor = new (r: 255f / 255f, g: 240f / 255f, b: 220f / 255f, a: 1f);
27 |
28 | public ConfigColor MNKColor = new (r: 214f / 255f, g: 156f / 255f, b: 0f / 255f, a: 1f);
29 | public ConfigColor NINColor = new (r: 175f / 255f, g: 25f / 255f, b: 100f / 255f, a: 1f);
30 | public ConfigColor DRGColor = new (r: 65f / 255f, g: 100f / 255f, b: 205f / 255f, a: 1f);
31 | public ConfigColor SAMColor = new (r: 228f / 255f, g: 109f / 255f, b: 4f / 255f, a: 1f);
32 | public ConfigColor RPRColor = new (r: 150f / 255f, g: 90f / 255f, b: 144f / 255f, a: 1f);
33 | public ConfigColor PGLColor = new (r: 214f / 255f, g: 156f / 255f, b: 0f / 255f, a: 1f);
34 | public ConfigColor ROGColor = new (r: 175f / 255f, g: 25f / 255f, b: 100f / 255f, a: 1f);
35 | public ConfigColor LNCColor = new (r: 65f / 255f, g: 100f / 255f, b: 205f / 255f, a: 1f);
36 |
37 | public ConfigColor BRDColor = new (r: 145f / 255f, g: 186f / 255f, b: 94f / 255f, a: 1f);
38 | public ConfigColor MCHColor = new (r: 110f / 255f, g: 225f / 255f, b: 214f / 255f, a: 1f);
39 | public ConfigColor DNCColor = new (r: 226f / 255f, g: 176f / 255f, b: 175f / 255f, a: 1f);
40 | public ConfigColor ARCColor = new (r: 145f / 255f, g: 186f / 255f, b: 94f / 255f, a: 1f);
41 |
42 | public ConfigColor BLMColor = new (r: 165f / 255f, g: 121f / 255f, b: 214f / 255f, a: 1f);
43 | public ConfigColor SMNColor = new (r: 45f / 255f, g: 155f / 255f, b: 120f / 255f, a: 1f);
44 | public ConfigColor RDMColor = new (r: 232f / 255f, g: 123f / 255f, b: 123f / 255f, a: 1f);
45 | public ConfigColor BLUColor = new (r: 0f / 255f, g: 185f / 255f, b: 247f / 255f, a: 1f);
46 | public ConfigColor THMColor = new (r: 165f / 255f, g: 121f / 255f, b: 214f / 255f, a: 1f);
47 | public ConfigColor ACNColor = new (r: 45f / 255f, g: 155f / 255f, b: 120f / 255f, a: 1f);
48 |
49 | public ConfigColor UKNColor = new (r: 218f / 255f, g: 157f / 255f, b: 46f / 255f, a: 1f);
50 |
51 | public ConfigColor GetColor(Job job) => job switch
52 | {
53 | Job.GLA => this.GLAColor,
54 | Job.MRD => this.MRDColor,
55 | Job.PLD => this.PLDColor,
56 | Job.WAR => this.WARColor,
57 | Job.DRK => this.DRKColor,
58 | Job.GNB => this.GNBColor,
59 |
60 | Job.CNJ => this.CNJColor,
61 | Job.WHM => this.WHMColor,
62 | Job.SCH => this.SCHColor,
63 | Job.AST => this.ASTColor,
64 | Job.SGE => this.SGEColor,
65 |
66 | Job.PGL => this.PGLColor,
67 | Job.LNC => this.LNCColor,
68 | Job.ROG => this.ROGColor,
69 | Job.MNK => this.MNKColor,
70 | Job.DRG => this.DRGColor,
71 | Job.NIN => this.NINColor,
72 | Job.SAM => this.SAMColor,
73 | Job.RPR => this.RPRColor,
74 |
75 | Job.ARC => this.ARCColor,
76 | Job.BRD => this.BRDColor,
77 | Job.MCH => this.MCHColor,
78 | Job.DNC => this.DNCColor,
79 |
80 | Job.THM => this.THMColor,
81 | Job.ACN => this.ACNColor,
82 | Job.BLM => this.BLMColor,
83 | Job.SMN => this.SMNColor,
84 | Job.RDM => this.RDMColor,
85 | Job.BLU => this.BLUColor,
86 | _ => this.UKNColor
87 | };
88 |
89 | public void DrawConfig(Vector2 size, float padX, float padY)
90 | {
91 | if (!ImGui.BeginChild($"##{this.Name}", new Vector2(size.X, size.Y), true))
92 | {
93 | ImGui.EndChild();
94 | return;
95 | }
96 |
97 | var vector = PLDColor.Vector;
98 | ImGui.ColorEdit4("PLD", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
99 | this.PLDColor.Vector = vector;
100 |
101 | vector = WARColor.Vector;
102 | ImGui.ColorEdit4("WAR", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
103 | this.WARColor.Vector = vector;
104 | vector = DRKColor.Vector;
105 | ImGui.ColorEdit4("DRK", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
106 | this.DRKColor.Vector = vector;
107 |
108 | vector = GNBColor.Vector;
109 | ImGui.ColorEdit4("GNB", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
110 | this.GNBColor.Vector = vector;
111 |
112 | ImGui.NewLine();
113 |
114 | vector = SCHColor.Vector;
115 | ImGui.ColorEdit4("SCH", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
116 | this.SCHColor.Vector = vector;
117 |
118 | vector = WHMColor.Vector;
119 | ImGui.ColorEdit4("WHM", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
120 | this.WHMColor.Vector = vector;
121 |
122 | vector = ASTColor.Vector;
123 | ImGui.ColorEdit4("AST", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
124 | this.ASTColor.Vector = vector;
125 |
126 | vector = SGEColor.Vector;
127 | ImGui.ColorEdit4("SGE", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
128 | this.SGEColor.Vector = vector;
129 |
130 | ImGui.NewLine();
131 |
132 | vector = MNKColor.Vector;
133 | ImGui.ColorEdit4("MNK", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
134 | this.MNKColor.Vector = vector;
135 |
136 | vector = NINColor.Vector;
137 | ImGui.ColorEdit4("NIN", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
138 | this.NINColor.Vector = vector;
139 |
140 | vector = DRGColor.Vector;
141 | ImGui.ColorEdit4("DRG", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
142 | this.DRGColor.Vector = vector;
143 |
144 | vector = SAMColor.Vector;
145 | ImGui.ColorEdit4("SAM", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
146 | this.SAMColor.Vector = vector;
147 |
148 | vector = RPRColor.Vector;
149 | ImGui.ColorEdit4("RPR", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
150 | this.RPRColor.Vector = vector;
151 |
152 | ImGui.NewLine();
153 |
154 | vector = BRDColor.Vector;
155 | ImGui.ColorEdit4("BRD", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
156 | this.BRDColor.Vector = vector;
157 |
158 | vector = MCHColor.Vector;
159 | ImGui.ColorEdit4("MCH", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
160 | this.MCHColor.Vector = vector;
161 |
162 | vector = DNCColor.Vector;
163 | ImGui.ColorEdit4("DNC", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
164 | this.DNCColor.Vector = vector;
165 |
166 | ImGui.NewLine();
167 |
168 | vector = BLMColor.Vector;
169 | ImGui.ColorEdit4("BLM", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
170 | this.BLMColor.Vector = vector;
171 |
172 | vector = SMNColor.Vector;
173 | ImGui.ColorEdit4("SMN", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
174 | this.SMNColor.Vector = vector;
175 |
176 | vector = RDMColor.Vector;
177 | ImGui.ColorEdit4("RDM", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
178 | this.RDMColor.Vector = vector;
179 |
180 | vector = BLUColor.Vector;
181 | ImGui.ColorEdit4("BLU", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
182 | this.BLUColor.Vector = vector;
183 |
184 | ImGui.NewLine();
185 |
186 | vector = GLAColor.Vector;
187 | ImGui.ColorEdit4("GLA", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
188 | this.GLAColor.Vector = vector;
189 |
190 | vector = MRDColor.Vector;
191 | ImGui.ColorEdit4("MRD", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
192 | this.MRDColor.Vector = vector;
193 |
194 | vector = CNJColor.Vector;
195 | ImGui.ColorEdit4("CNJ", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
196 | this.CNJColor.Vector = vector;
197 |
198 | vector = PGLColor.Vector;
199 | ImGui.ColorEdit4("PGL", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
200 | this.PGLColor.Vector = vector;
201 |
202 | vector = ROGColor.Vector;
203 | ImGui.ColorEdit4("ROG", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
204 | this.ROGColor.Vector = vector;
205 |
206 | vector = LNCColor.Vector;
207 | ImGui.ColorEdit4("LNC", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
208 | this.LNCColor.Vector = vector;
209 |
210 | vector = ARCColor.Vector;
211 | ImGui.ColorEdit4("ARC", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
212 | this.ARCColor.Vector = vector;
213 |
214 | vector = THMColor.Vector;
215 | ImGui.ColorEdit4("THM", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
216 | this.THMColor.Vector = vector;
217 |
218 | vector = ACNColor.Vector;
219 | ImGui.ColorEdit4("ACN", ref vector, ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar);
220 | this.ACNColor.Vector = vector;
221 |
222 | ImGui.EndChild();
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/LMeter/src/Config/ConfigColor.cs:
--------------------------------------------------------------------------------
1 | using ImGuiNET;
2 | using Newtonsoft.Json;
3 | using System.Numerics;
4 |
5 |
6 | namespace LMeter.Config;
7 |
8 | public class ConfigColor
9 | {
10 | [JsonIgnore]
11 | public uint Base { get; private set; }
12 |
13 | [JsonIgnore]
14 | public uint Background { get; private set; }
15 |
16 | [JsonIgnore]
17 | public uint TopGradient { get; private set; }
18 |
19 | [JsonIgnore]
20 | public uint BottomGradient { get; private set; }
21 |
22 | [JsonIgnore]
23 | private Vector4 _vector;
24 | public Vector4 Vector
25 | {
26 | get => _vector;
27 | set
28 | {
29 | if (_vector == value)
30 | {
31 | return;
32 | }
33 |
34 | _vector = value;
35 |
36 | Update();
37 | }
38 | }
39 |
40 | // Constructor for deserialization
41 | public ConfigColor() : this(Vector4.Zero) { }
42 |
43 | public ConfigColor
44 | (
45 | float r,
46 | float g,
47 | float b,
48 | float a
49 | ) : this(new Vector4(r, g, b, a)) { }
50 |
51 | public ConfigColor(Vector4 vector) =>
52 | this.Vector = vector;
53 |
54 | private void Update() =>
55 | Base = ImGui.ColorConvertFloat4ToU32(_vector);
56 | }
57 |
--------------------------------------------------------------------------------
/LMeter/src/Config/FontConfig.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Interface;
2 | using ImGuiNET;
3 | using LMeter.Helpers;
4 | using Newtonsoft.Json;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Numerics;
8 | using System;
9 |
10 |
11 | namespace LMeter.Config;
12 |
13 | public class FontConfig : IConfigPage
14 | {
15 | public string Name => "Fonts";
16 |
17 | [JsonIgnore]
18 | private static readonly string? _fontPath = FontsManager.GetUserFontPath();
19 | [JsonIgnore]
20 | private int _selectedFont = 0;
21 | [JsonIgnore]
22 | private int _selectedSize = 23;
23 | [JsonIgnore]
24 | private string[] _fonts = FontsManager.GetFontNamesFromPath(FontsManager.GetUserFontPath());
25 | [JsonIgnore]
26 | private readonly string[] _sizes =
27 | {
28 | "1", "2", "3", "4", "5", "6", "7", "8", "9", "10",
29 | "11", "12", "13", "14", "15", "16", "17", "18", "19", "20",
30 | "21", "22", "23", "24", "25", "26", "27", "28", "29", "30",
31 | "31", "32", "33", "34", "35", "36", "37", "38", "39", "40"
32 | };
33 |
34 | [JsonIgnore]
35 | private bool _chinese = false;
36 | [JsonIgnore]
37 | private bool _korean = false;
38 |
39 | public Dictionary Fonts { get; set; }
40 |
41 | public FontConfig()
42 | {
43 | RefreshFontList();
44 | this.Fonts = new Dictionary();
45 |
46 | foreach (var fontKey in FontsManager.DefaultFontKeys)
47 | {
48 | var splits = fontKey.Split("_", StringSplitOptions.RemoveEmptyEntries);
49 | if (splits.Length == 2 && int.TryParse(splits[1], out int size))
50 | {
51 | var newFont = new FontData(splits[0], size, false, false);
52 | this.Fonts.Add(FontsManager.GetFontKey(newFont), newFont);
53 | }
54 | }
55 | }
56 |
57 | public IConfigPage GetDefault() =>
58 | new FontConfig();
59 |
60 | public void DrawConfig(Vector2 size, float padX, float padY)
61 | {
62 | if (_fonts.Length == 0) RefreshFontList();
63 |
64 | if (!ImGui.BeginChild("##FontConfig", new Vector2(size.X, size.Y), true) || _fontPath is null)
65 | {
66 | ImGui.EndChild();
67 | return;
68 | }
69 |
70 | var cursorY = ImGui.GetCursorPosY();
71 | ImGui.SetCursorPosY(cursorY + 2f);
72 | ImGui.Text("Copy Font Folder Path to Clipboard: ");
73 | ImGui.SameLine();
74 |
75 | var buttonSize = new Vector2(40, 0);
76 | ImGui.SetCursorPosY(cursorY);
77 | DrawHelpers.DrawButton
78 | (
79 | string.Empty,
80 | FontAwesomeIcon.Copy,
81 | () => ImGui.SetClipboardText(_fontPath),
82 | null,
83 | buttonSize
84 | );
85 |
86 | ImGui.Combo("Font", ref _selectedFont, _fonts, _fonts.Length);
87 | ImGui.SameLine();
88 | DrawHelpers.DrawButton
89 | (
90 | string.Empty,
91 | FontAwesomeIcon.Sync,
92 | RefreshFontList,
93 | "Reload Font List",
94 | buttonSize
95 | );
96 |
97 | ImGui.Combo("Size", ref _selectedSize, _sizes, _sizes.Length);
98 | ImGui.SameLine();
99 | ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 3f);
100 | DrawHelpers.DrawButton
101 | (
102 | string.Empty,
103 | FontAwesomeIcon.Plus,
104 | () => AddFont(_selectedFont, _selectedSize),
105 | "Add Font",
106 | buttonSize
107 | );
108 |
109 | ImGui.Checkbox("Support Chinese/Japanese", ref _chinese);
110 | ImGui.SameLine();
111 | ImGui.Checkbox("Support Korean", ref _korean);
112 |
113 | DrawHelpers.DrawSpacing(1);
114 | ImGui.Text("Font List");
115 |
116 | ImGuiTableFlags tableFlags =
117 | ImGuiTableFlags.RowBg |
118 | ImGuiTableFlags.Borders |
119 | ImGuiTableFlags.BordersOuter |
120 | ImGuiTableFlags.BordersInner |
121 | ImGuiTableFlags.ScrollY |
122 | ImGuiTableFlags.NoSavedSettings;
123 |
124 | if
125 | (
126 | ImGui.BeginTable
127 | (
128 | "##Font_Table",
129 | 5,
130 | tableFlags,
131 | new Vector2(size.X - padX * 2, size.Y - ImGui.GetCursorPosY() - padY * 2)
132 | )
133 | )
134 | {
135 | ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch, 0, 0);
136 | ImGui.TableSetupColumn("Size", ImGuiTableColumnFlags.WidthFixed, 40, 1);
137 | ImGui.TableSetupColumn("CN/JP", ImGuiTableColumnFlags.WidthFixed, 40, 2);
138 | ImGui.TableSetupColumn("KR", ImGuiTableColumnFlags.WidthFixed, 40, 3);
139 | ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, 45, 4);
140 |
141 | ImGui.TableSetupScrollFreeze(0, 1);
142 | ImGui.TableHeadersRow();
143 |
144 | for (var i = 0; i < this.Fonts.Keys.Count; i++)
145 | {
146 | ImGui.PushID(i.ToString());
147 | ImGui.TableNextRow(ImGuiTableRowFlags.None, 28);
148 |
149 | var key = this.Fonts.Keys.ElementAt(i);
150 | var font = this.Fonts[key];
151 |
152 | if (ImGui.TableSetColumnIndex(0))
153 | {
154 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3f);
155 | ImGui.Text(key);
156 | }
157 |
158 | if (ImGui.TableSetColumnIndex(1))
159 | {
160 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3f);
161 | ImGui.Text(font.Size.ToString());
162 | }
163 |
164 | if (ImGui.TableSetColumnIndex(2))
165 | {
166 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3f);
167 | ImGui.Text(font.Chinese ? "Yes" : "No");
168 | }
169 |
170 | if (ImGui.TableSetColumnIndex(3))
171 | {
172 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3f);
173 | ImGui.Text(font.Korean ? "Yes" : "No");
174 | }
175 |
176 | if (ImGui.TableSetColumnIndex(4))
177 | {
178 | if (!FontsManager.DefaultFontKeys.Contains(key))
179 | {
180 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 1f);
181 | DrawHelpers.DrawButton
182 | (
183 | string.Empty,
184 | FontAwesomeIcon.Trash,
185 | () => RemoveFont(key),
186 | "Remove Font",
187 | new Vector2(45, 0)
188 | );
189 | }
190 | }
191 | }
192 |
193 | ImGui.EndTable();
194 | }
195 |
196 | ImGui.EndChild();
197 | }
198 |
199 | public void RefreshFontList() =>
200 | _fonts = FontsManager.GetFontNamesFromPath(FontsManager.GetUserFontPath());
201 |
202 | private void AddFont(int fontIndex, int size)
203 | {
204 | var newFont = new FontData(_fonts[fontIndex], size + 1, _chinese, _korean);
205 | var key = FontsManager.GetFontKey(newFont);
206 |
207 | if (!this.Fonts.ContainsKey(key))
208 | {
209 | this.Fonts.Add(key, newFont);
210 | PluginManager.Instance.FontsManager.UpdateFonts(this.Fonts.Values);
211 | }
212 | }
213 |
214 | private void RemoveFont(string key)
215 | {
216 | this.Fonts.Remove(key);
217 | PluginManager.Instance.FontsManager.UpdateFonts(this.Fonts.Values);
218 | }
219 | }
220 |
--------------------------------------------------------------------------------
/LMeter/src/Config/GeneralConfig.cs:
--------------------------------------------------------------------------------
1 | using ImGuiNET;
2 | using LMeter.Helpers;
3 | using Newtonsoft.Json;
4 | using System.Numerics;
5 | using System.Runtime.CompilerServices;
6 | using System;
7 |
8 |
9 | namespace LMeter.Config;
10 |
11 | public enum MeterDataType
12 | {
13 | Damage,
14 | Healing,
15 | DamageTaken
16 | }
17 |
18 | public class GeneralConfig : IConfigPage
19 | {
20 | [JsonIgnore]
21 | private static readonly string[] _meterTypeOptions = Enum.GetNames(typeof(MeterDataType));
22 |
23 | [JsonIgnore]
24 | public bool Preview = false;
25 | public string Name => "General";
26 | public Vector2 Position = Vector2.Zero;
27 | public Vector2 Size = new (ImGui.GetMainViewport().Size.Y * 16 / 90, ImGui.GetMainViewport().Size.Y / 10);
28 | public bool Lock = false;
29 | public bool ClickThrough = false;
30 | public ConfigColor BackgroundColor = new (r: 0, g: 0, b: 0, a: 0.5f);
31 | public bool ShowBorder = true;
32 | public bool BorderAroundBars = false;
33 | public ConfigColor BorderColor = new (r: 30f / 255f, g: 30f / 255f, b: 30f / 255f, a: 230f / 255f);
34 | public int BorderThickness = 2;
35 | public MeterDataType DataType = MeterDataType.Damage;
36 | public bool ReturnToCurrent = true;
37 |
38 | public IConfigPage GetDefault() =>
39 | new GeneralConfig();
40 |
41 | public void DrawConfig(Vector2 size, float padX, float padY)
42 | {
43 | if (!ImGui.BeginChild($"##{this.Name}", new Vector2(size.X, size.Y), true))
44 | {
45 | ImGui.EndChild();
46 | return;
47 | }
48 |
49 | var screenSize = ImGui.GetMainViewport().Size;
50 | ImGui.DragFloat2("Position", ref this.Position, 1, -screenSize.X / 2, screenSize.X / 2);
51 | ImGui.DragFloat2("Size", ref this.Size, 1, 0, screenSize.Y);
52 | ImGui.Checkbox("Lock", ref this.Lock);
53 | ImGui.Checkbox("Click Through", ref this.ClickThrough);
54 | ImGui.Checkbox("Preview", ref this.Preview);
55 |
56 | ImGui.NewLine();
57 |
58 | var vector = this.BackgroundColor.Vector;
59 | ImGui.ColorEdit4
60 | (
61 | "Background Color",
62 | ref vector,
63 | ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar
64 | );
65 | this.BackgroundColor.Vector = vector;
66 |
67 | ImGui.Checkbox("Show Border", ref this.ShowBorder);
68 | if (this.ShowBorder)
69 | {
70 | DrawHelpers.DrawNestIndicator(1);
71 | ImGui.DragInt("Border Thickness", ref this.BorderThickness, 1, 1, 20);
72 |
73 | DrawHelpers.DrawNestIndicator(1);
74 | vector = this.BorderColor.Vector;
75 | ImGui.ColorEdit4
76 | (
77 | "Border Color",
78 | ref vector,
79 | ImGuiColorEditFlags.AlphaPreview | ImGuiColorEditFlags.AlphaBar
80 | );
81 | this.BorderColor.Vector = vector;
82 |
83 | DrawHelpers.DrawNestIndicator(1);
84 | ImGui.Checkbox("Hide border around Header", ref this.BorderAroundBars);
85 | }
86 |
87 | ImGui.NewLine();
88 | ImGui.Combo
89 | (
90 | "Sort Type",
91 | ref Unsafe.As(ref this.DataType),
92 | _meterTypeOptions,
93 | _meterTypeOptions.Length
94 | );
95 |
96 | ImGui.Checkbox("Return to Current Data when entering combat", ref this.ReturnToCurrent);
97 |
98 | ImGui.EndChild();
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/LMeter/src/Config/IConfigPage.cs:
--------------------------------------------------------------------------------
1 | using System.Numerics;
2 |
3 |
4 | namespace LMeter.Config;
5 |
6 | public interface IConfigPage
7 | {
8 | string Name { get; }
9 |
10 | IConfigPage GetDefault();
11 | void DrawConfig(Vector2 size, float padX, float padY);
12 | }
13 |
--------------------------------------------------------------------------------
/LMeter/src/Config/IConfigurable.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 |
4 | namespace LMeter.Config;
5 |
6 | public interface IConfigurable
7 | {
8 | string Name { get; set; }
9 |
10 | IEnumerable GetConfigPages();
11 | void ImportPage(IConfigPage page);
12 | }
13 |
--------------------------------------------------------------------------------
/LMeter/src/Config/LMeterConfig.cs:
--------------------------------------------------------------------------------
1 | using LMeter.Helpers;
2 | using Newtonsoft.Json;
3 | using System.Collections.Generic;
4 | using System;
5 |
6 |
7 | namespace LMeter.Config;
8 |
9 | [JsonObject]
10 | public class LMeterConfig : IConfigurable, IDisposable
11 | {
12 | public string Name
13 | {
14 | get => $"LMeter v{this.Version}";
15 | set {}
16 | }
17 |
18 | public string? Version
19 | {
20 | get => Plugin.Version;
21 | set {}
22 | }
23 |
24 | public bool FirstLoad = true;
25 |
26 | public MeterListConfig MeterList { get; init; }
27 |
28 | private ActConfig _actConfig = null!;
29 | public ActConfig ActConfig
30 | {
31 | get => _actConfig;
32 | init
33 | {
34 | if (value is ACTConfig oldTypeName)
35 | {
36 | // I HATE THIS, but I cannot find any way in C# to actually convert an object to a parent type, in any
37 | // other way than creating a brand new object that happens to share every value with a different type.
38 | // So if I don't want to manually maintain mappings from now until the end of time whenever the parent
39 | // class changes for any reason, I am FORCED to ensure the class is serializable. This is because, C#
40 | // does not offer any generic way to simply deep copy an object, without FUCKING SERIALIZING IT! In
41 | // this case, being serializable is a required feature anyway for these config objects, so whatever,
42 | // eat all my performance and memory why dontcha?
43 | var tempConf = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(oldTypeName));
44 | if (tempConf != null)
45 | {
46 | _actConfig = tempConf;
47 | return;
48 | }
49 | }
50 |
51 | _actConfig = value;
52 | }
53 | }
54 |
55 | public CactbotConfig CactbotConfig { get; init; }
56 |
57 | public FontConfig FontConfig { get; init; }
58 |
59 | [JsonIgnore]
60 | private AboutPage AboutPage { get; } = new ();
61 |
62 | public LMeterConfig()
63 | {
64 | this.MeterList = new MeterListConfig();
65 | this.ActConfig = new ActConfig();
66 | this.FontConfig = new FontConfig();
67 | this.CactbotConfig = new CactbotConfig();
68 | }
69 |
70 | public void Dispose()
71 | {
72 | this.Dispose(true);
73 | GC.SuppressFinalize(this);
74 | }
75 |
76 | protected virtual void Dispose(bool disposing)
77 | {
78 | if (disposing)
79 | {
80 | ConfigHelpers.SaveConfig(this);
81 | }
82 | }
83 |
84 | public IEnumerable GetConfigPages()
85 | {
86 | yield return this.MeterList;
87 | yield return this.ActConfig;
88 | yield return this.FontConfig;
89 | yield return this.CactbotConfig;
90 | yield return this.AboutPage;
91 | }
92 |
93 | public void ApplyConfig()
94 | {
95 | this.CactbotConfig.SetNewCactbotUrl(forceStart: false);
96 | }
97 |
98 | public void ImportPage(IConfigPage page) { }
99 | }
100 |
--------------------------------------------------------------------------------
/LMeter/src/Config/MeterListConfig.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Interface.Internal.Notifications;
2 | using Dalamud.Interface;
3 | using ImGuiNET;
4 | using LMeter.Helpers;
5 | using LMeter.Meter;
6 | using Newtonsoft.Json;
7 | using System.Collections.Generic;
8 | using System.Numerics;
9 | using System;
10 |
11 |
12 | namespace LMeter.Config;
13 |
14 | public class MeterListConfig : IConfigPage
15 | {
16 | private const float MenuBarHeight = 40;
17 | [JsonIgnore]
18 | private string _input = string.Empty;
19 | public string Name =>
20 | "Profiles";
21 | public List Meters { get; set; }
22 |
23 | public MeterListConfig() =>
24 | this.Meters = new List();
25 |
26 | public IConfigPage GetDefault() =>
27 | new MeterListConfig();
28 |
29 | public void DrawConfig(Vector2 size, float padX, float padY)
30 | {
31 | this.DrawCreateMenu(size, padX);
32 | this.DrawMeterTable(size.AddY(-padY), padX);
33 | }
34 |
35 | public void ToggleMeter(int meterIndex, bool? toggle = null)
36 | {
37 | if (meterIndex >= 0 && meterIndex < this.Meters.Count)
38 | {
39 | this.Meters[meterIndex].VisibilityConfig.AlwaysHide =
40 | toggle.HasValue
41 | ? !toggle.Value
42 | : !this.Meters[meterIndex].VisibilityConfig.AlwaysHide;
43 | }
44 | }
45 |
46 | public void ToggleClickThrough(int meterIndex)
47 | {
48 | if (meterIndex >= 0 && meterIndex < this.Meters.Count)
49 | {
50 | this.Meters[meterIndex].GeneralConfig.ClickThrough ^= true;
51 | }
52 | }
53 |
54 | private void DrawCreateMenu(Vector2 size, float padX)
55 | {
56 | var buttonSize = new Vector2(40, 0);
57 | var textInputWidth = size.X - buttonSize.X * 2 - padX * 4;
58 |
59 | if (!ImGui.BeginChild("##Buttons", new Vector2(size.X, MenuBarHeight), true))
60 | {
61 | ImGui.EndChild();
62 | return;
63 | }
64 |
65 | ImGui.PushItemWidth(textInputWidth);
66 | ImGui.InputTextWithHint("##Input", "Profile Name/Import String", ref _input, 10000);
67 | ImGui.PopItemWidth();
68 |
69 | ImGui.SameLine();
70 | DrawHelpers.DrawButton
71 | (
72 | string.Empty,
73 | FontAwesomeIcon.Plus,
74 | () => CreateMeter(_input),
75 | "Create new Meter",
76 | buttonSize
77 | );
78 |
79 | ImGui.SameLine();
80 | DrawHelpers.DrawButton
81 | (
82 | string.Empty,
83 | FontAwesomeIcon.Download,
84 | () => ImportMeter(_input),
85 | "Import new Meter",
86 | buttonSize
87 | );
88 | ImGui.PopItemWidth();
89 |
90 | ImGui.EndChild();
91 | }
92 |
93 | private void DrawMeterTable(Vector2 size, float padX)
94 | {
95 | ImGuiTableFlags flags =
96 | ImGuiTableFlags.RowBg |
97 | ImGuiTableFlags.Borders |
98 | ImGuiTableFlags.BordersOuter |
99 | ImGuiTableFlags.BordersInner |
100 | ImGuiTableFlags.ScrollY |
101 | ImGuiTableFlags.NoSavedSettings;
102 |
103 | if (!ImGui.BeginTable("##Meter_Table", 3, flags, new Vector2(size.X, size.Y - MenuBarHeight)))
104 | {
105 | ImGui.EndChild();
106 | return;
107 | }
108 |
109 | var buttonSize = new Vector2(30, 0);
110 | var actionsWidth = buttonSize.X * 3 + padX * 2;
111 |
112 | ImGui.TableSetupColumn(" #", ImGuiTableColumnFlags.WidthFixed, 18, 0);
113 | ImGui.TableSetupColumn("Profile Name", ImGuiTableColumnFlags.WidthStretch, 0, 1);
114 | ImGui.TableSetupColumn("Actions", ImGuiTableColumnFlags.WidthFixed, actionsWidth, 2);
115 |
116 | ImGui.TableSetupScrollFreeze(0, 1);
117 | ImGui.TableHeadersRow();
118 |
119 | for (var i = 0; i < this.Meters.Count; i++)
120 | {
121 | var meter = this.Meters[i];
122 |
123 | if
124 | (
125 | !string.IsNullOrEmpty(_input) &&
126 | !meter.Name.Contains(_input, StringComparison.OrdinalIgnoreCase)
127 | )
128 | {
129 | continue;
130 | }
131 |
132 | ImGui.PushID(i.ToString());
133 | ImGui.TableNextRow(ImGuiTableRowFlags.None, 28);
134 |
135 | if (ImGui.TableSetColumnIndex(0))
136 | {
137 | var num = $" {i + 1}.";
138 | var columnWidth = ImGui.GetColumnWidth();
139 | var cursorPos = ImGui.GetCursorPos();
140 | var textSize = ImGui.CalcTextSize(num);
141 | ImGui.SetCursorPos(new Vector2(cursorPos.X + columnWidth - textSize.X, cursorPos.Y + 3f));
142 | ImGui.Text(num);
143 | }
144 |
145 | if (ImGui.TableSetColumnIndex(1))
146 | {
147 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3f);
148 | ImGui.Text(meter.Name);
149 | }
150 |
151 | if (ImGui.TableSetColumnIndex(2))
152 | {
153 | ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 1f);
154 | DrawHelpers.DrawButton
155 | (
156 | string.Empty,
157 | FontAwesomeIcon.Pen,
158 | () => EditMeter(meter),
159 | "Edit",
160 | buttonSize
161 | );
162 |
163 | ImGui.SameLine();
164 | DrawHelpers.DrawButton
165 | (
166 | string.Empty,
167 | FontAwesomeIcon.Upload,
168 | () => ExportMeter(meter),
169 | "Export",
170 | buttonSize
171 | );
172 |
173 | ImGui.SameLine();
174 | DrawHelpers.DrawButton
175 | (
176 | string.Empty,
177 | FontAwesomeIcon.Trash,
178 | () => DeleteMeter(meter),
179 | "Delete",
180 | buttonSize
181 | );
182 | }
183 | }
184 |
185 | ImGui.EndTable();
186 | }
187 |
188 | private void CreateMeter(string name)
189 | {
190 | if (!string.IsNullOrEmpty(name)) this.Meters.Add(MeterWindow.GetDefaultMeter(name));
191 |
192 | _input = string.Empty;
193 | }
194 |
195 | private void EditMeter(MeterWindow meter) =>
196 | PluginManager.Instance.Edit(meter);
197 |
198 | private void DeleteMeter(MeterWindow meter) =>
199 | this.Meters.Remove(meter);
200 |
201 | private void ImportMeter(string input)
202 | {
203 | var importString = input;
204 | if (string.IsNullOrWhiteSpace(importString))
205 | {
206 | importString = ImGui.GetClipboardText();
207 | }
208 |
209 | var newMeter = ConfigHelpers.GetFromImportString(importString);
210 | if (newMeter is not null)
211 | {
212 | this.Meters.Add(newMeter);
213 | }
214 | else
215 | {
216 | DrawHelpers.DrawNotification("Failed to Import Meter!", NotificationType.Error);
217 | }
218 |
219 | _input = string.Empty;
220 | }
221 |
222 | private void ExportMeter(MeterWindow meter) =>
223 | ConfigHelpers.ExportToClipboard(meter);
224 | }
225 |
--------------------------------------------------------------------------------
/LMeter/src/Config/VisibilityConfig.cs:
--------------------------------------------------------------------------------
1 | using ImGuiNET;
2 | using LMeter.Helpers;
3 | using Newtonsoft.Json;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Numerics;
7 | using System.Runtime.CompilerServices;
8 | using System;
9 |
10 |
11 | namespace LMeter.Config;
12 |
13 | public class VisibilityConfig : IConfigPage
14 | {
15 | public string Name =>
16 | "Visibility";
17 |
18 | [JsonIgnore]
19 | private string _customJobInput = string.Empty;
20 |
21 | public bool AlwaysHide = false;
22 | public bool HideInCombat = false;
23 | public bool HideOutsideCombat = false;
24 | public bool ShowAnywayWhenInDuty = false;
25 | public bool HideOutsideDuty = false;
26 | public bool ShowAnywayWhenInCombat = false;
27 | public bool HideWhilePerforming = false;
28 | public bool HideInGoldenSaucer = false;
29 | public bool HideIfNotConnected = false;
30 |
31 | public JobType ShowForJobTypes = JobType.All;
32 | public string CustomJobString = string.Empty;
33 | public List CustomJobList = new ();
34 |
35 | public IConfigPage GetDefault() =>
36 | new VisibilityConfig();
37 |
38 | public bool IsVisible()
39 | {
40 | if (this.AlwaysHide)
41 | {
42 | return false;
43 | }
44 |
45 | if (this.HideInCombat && CharacterState.IsInCombat())
46 | {
47 | return false;
48 | }
49 |
50 | var shouldHide = false;
51 | if (this.HideOutsideCombat && !CharacterState.IsInCombat())
52 | {
53 | shouldHide |= true;
54 | if (this.ShowAnywayWhenInDuty && CharacterState.IsInDuty())
55 | {
56 | shouldHide = false;
57 | }
58 | }
59 |
60 | if (this.HideOutsideDuty && !CharacterState.IsInDuty())
61 | {
62 | shouldHide |= true;
63 | if (this.ShowAnywayWhenInCombat && CharacterState.IsInCombat())
64 | {
65 | shouldHide = false;
66 | }
67 | }
68 |
69 | if (shouldHide) return false;
70 |
71 | if (this.HideWhilePerforming && CharacterState.IsPerforming())
72 | {
73 | return false;
74 | }
75 |
76 | if (this.HideInGoldenSaucer && CharacterState.IsInGoldenSaucer())
77 | {
78 | return false;
79 | }
80 |
81 | if (this.HideIfNotConnected && !PluginManager.Instance.ActClient.Current.ClientReady())
82 | {
83 | return false;
84 | }
85 |
86 | return CharacterState.IsJobType(CharacterState.GetCharacterJob(), this.ShowForJobTypes, this.CustomJobList);
87 | }
88 |
89 | public void DrawConfig(Vector2 size, float padX, float padY)
90 | {
91 | if (!ImGui.BeginChild($"##{this.Name}", new Vector2(size.X, size.Y), true))
92 | {
93 | ImGui.EndChild();
94 | return;
95 | }
96 |
97 | ImGui.Checkbox("Always Hide", ref this.AlwaysHide);
98 | ImGui.Checkbox("Hide In Combat", ref this.HideInCombat);
99 | ImGui.Checkbox("Hide Outside Combat", ref this.HideOutsideCombat);
100 | ImGui.Indent();
101 | ImGui.Checkbox("Show Anyway When In Duty", ref this.ShowAnywayWhenInDuty);
102 | ImGui.Unindent();
103 | ImGui.Checkbox("Hide Outside Duty", ref this.HideOutsideDuty);
104 | ImGui.Indent();
105 | ImGui.Checkbox("Show Anyway When In Combat", ref this.ShowAnywayWhenInCombat);
106 | ImGui.Unindent();
107 | ImGui.Checkbox("Hide While Performing", ref this.HideWhilePerforming);
108 | ImGui.Checkbox("Hide In Golden Saucer", ref this.HideInGoldenSaucer);
109 | ImGui.Checkbox("Hide While Not Connected to ACT", ref this.HideIfNotConnected);
110 |
111 | DrawHelpers.DrawSpacing(1);
112 | var jobTypeOptions = Enum.GetNames(typeof(JobType));
113 | ImGui.Combo
114 | (
115 | "Show for Jobs",
116 | ref Unsafe.As(ref this.ShowForJobTypes),
117 | jobTypeOptions,
118 | jobTypeOptions.Length
119 | );
120 |
121 | if (this.ShowForJobTypes == JobType.Custom)
122 | {
123 | if (string.IsNullOrEmpty(_customJobInput)) _customJobInput = this.CustomJobString.ToUpper();
124 |
125 | if
126 | (
127 | ImGui.InputTextWithHint
128 | (
129 | "Custom Job List",
130 | "Comma Separated List (ex: WAR, SAM, BLM)",
131 | ref _customJobInput,
132 | 100,
133 | ImGuiInputTextFlags.EnterReturnsTrue
134 | )
135 | )
136 | {
137 | var jobStrings = _customJobInput.Split(',').Select(j => j.Trim());
138 | var jobList = new List();
139 |
140 | foreach (var j in jobStrings)
141 | {
142 | if (Enum.TryParse(j, true, out Job parsed))
143 | {
144 | jobList.Add(parsed);
145 | }
146 | else
147 | {
148 | jobList.Clear();
149 | _customJobInput = string.Empty;
150 | break;
151 | }
152 | }
153 |
154 | _customJobInput = _customJobInput.ToUpper();
155 | this.CustomJobString = _customJobInput;
156 | this.CustomJobList = jobList;
157 | }
158 | }
159 |
160 | ImGui.EndChild();
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/LMeter/src/Helpers/CharacterState.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Game.ClientState.Conditions;
2 | using FFXIVClientStructs.FFXIV.Client.Game.Character;
3 | using Lumina.Excel.GeneratedSheets;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 |
8 | namespace LMeter.Helpers;
9 |
10 | public static class CharacterState
11 | {
12 | public static readonly uint[] _goldenSaucerIDs =
13 | {
14 | 144, // The Gold Saucer | Open world zone
15 | 388, // Chocobo Square | ???
16 | 389, // Chocobo Square | ???
17 | 390, // Chocobo Square | ???
18 | 391, // Chocobo Square | ???
19 | 579, // The Battlehall | ???
20 | 792, // The Fall of Belah'dia | Jump puzzles
21 | 831, // The Manderville Tables | Mojang
22 | 899, // The Falling City of Nym | Jump puzzles
23 | 941, // The Battlehall | ???
24 | };
25 |
26 | public static bool IsCharacterBusy() =>
27 | PluginManager.Instance.Condition[ConditionFlag.WatchingCutscene] ||
28 | PluginManager.Instance.Condition[ConditionFlag.WatchingCutscene78] ||
29 | PluginManager.Instance.Condition[ConditionFlag.OccupiedInCutSceneEvent] ||
30 | PluginManager.Instance.Condition[ConditionFlag.CreatingCharacter] ||
31 | PluginManager.Instance.Condition[ConditionFlag.BetweenAreas] ||
32 | PluginManager.Instance.Condition[ConditionFlag.BetweenAreas51] ||
33 | PluginManager.Instance.Condition[ConditionFlag.OccupiedSummoningBell];
34 |
35 | public static bool IsInCombat() =>
36 | PluginManager.Instance.Condition[ConditionFlag.InCombat];
37 |
38 | public static bool IsInDuty() =>
39 | PluginManager.Instance.Condition[ConditionFlag.BoundByDuty];
40 |
41 | public static bool IsPerforming() =>
42 | PluginManager.Instance.Condition[ConditionFlag.Performing];
43 |
44 | public static bool IsInGoldenSaucer()
45 | {
46 | var territoryId = PluginManager.Instance.ClientState.TerritoryType;
47 | foreach (var id in _goldenSaucerIDs)
48 | {
49 | if (id == territoryId) return true;
50 | }
51 |
52 | return false;
53 | }
54 |
55 | public static Job GetCharacterJob()
56 | {
57 | var player = PluginManager.Instance.ClientState.LocalPlayer;
58 | if (player is null) return Job.UKN;
59 |
60 | unsafe
61 | {
62 | return (Job) ((Character*) player.Address)->ClassJob;
63 | }
64 | }
65 |
66 | public static (ushort territoryId, string? territoryName) GetCharacterLocation()
67 | {
68 | var locationId = PluginManager.Instance?.ClientState.TerritoryType;
69 | if (locationId == null || locationId < 4) return (0, null);
70 |
71 | var locationRow = PluginManager
72 | .Instance?
73 | .DataManager
74 | .GetExcelSheet()?
75 | .GetRow(locationId.Value);
76 |
77 | var instanceContentName = locationRow?.ContentFinderCondition.Value?.Name?.ToString();
78 | var placeName = locationRow?.PlaceName.Value?.Name?.ToString();
79 |
80 | return
81 | (
82 | locationId.Value,
83 | string.IsNullOrEmpty(instanceContentName)
84 | ? placeName
85 | : instanceContentName
86 | );
87 | }
88 |
89 | public static bool IsJobType(Job job, JobType type, IEnumerable? jobList = null) => type switch
90 | {
91 | JobType.All => true,
92 | JobType.Tanks => job is Job.GLA or Job.MRD or Job.PLD or Job.WAR or Job.DRK or Job.GNB,
93 | JobType.Casters => job is Job.THM or Job.ACN or Job.BLM or Job.SMN or Job.RDM or Job.BLU,
94 | JobType.Melee => job is Job.PGL or Job.LNC or Job.ROG or Job.MNK or Job.DRG or Job.NIN or Job.SAM or Job.RPR,
95 | JobType.Ranged => job is Job.ARC or Job.BRD or Job.MCH or Job.DNC,
96 | JobType.Healers => job is Job.CNJ or Job.WHM or Job.SCH or Job.AST or Job.SGE,
97 | JobType.DoH => job is Job.CRP or Job.BSM or Job.ARM or Job.GSM or Job.LTW or Job.WVR or Job.ALC or Job.CUL,
98 | JobType.DoL => job is Job.MIN or Job.BOT or Job.FSH,
99 | JobType.Combat => IsJobType(job, JobType.DoW) || IsJobType(job, JobType.DoM),
100 | JobType.DoW => IsJobType(job, JobType.Tanks) || IsJobType(job, JobType.Melee) || IsJobType(job, JobType.Ranged),
101 | JobType.DoM => IsJobType(job, JobType.Casters) || IsJobType(job, JobType.Healers),
102 | JobType.Crafters => IsJobType(job, JobType.DoH) || IsJobType(job, JobType.DoL),
103 | JobType.Custom => jobList is not null && jobList.Contains(job),
104 | _ => false
105 | };
106 | }
107 |
--------------------------------------------------------------------------------
/LMeter/src/Helpers/ConfigHelpers.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Interface.Internal.Notifications;
2 | using Dalamud.Logging;
3 | using ImGuiNET;
4 | using LMeter.Config;
5 | using Newtonsoft.Json.Serialization;
6 | using Newtonsoft.Json;
7 | using System.Collections.Generic;
8 | using System.IO.Compression;
9 | using System.IO;
10 | using System.Text;
11 | using System;
12 |
13 |
14 | namespace LMeter.Helpers;
15 |
16 | public static class ConfigHelpers
17 | {
18 | private static readonly JsonSerializerSettings _serializerSettings = new ()
19 | {
20 | TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
21 | TypeNameHandling = TypeNameHandling.Objects,
22 | ObjectCreationHandling = ObjectCreationHandling.Replace,
23 | SerializationBinder = new LMeterSerializationBinder()
24 | };
25 |
26 | public static void ExportToClipboard(T toExport)
27 | {
28 | var exportString = GetExportString(toExport);
29 |
30 | if (!string.IsNullOrEmpty(exportString))
31 | {
32 | PluginLog.Log(exportString);
33 | ImGui.SetClipboardText(exportString);
34 | DrawHelpers.DrawNotification("Export string copied to clipboard.");
35 | }
36 | else
37 | {
38 | DrawHelpers.DrawNotification("Failed to Export!", NotificationType.Error);
39 | }
40 | }
41 |
42 | public static string? GetExportString(T toExport)
43 | {
44 | try
45 | {
46 | var jsonString = JsonConvert.SerializeObject(toExport, Formatting.None, _serializerSettings);
47 | using var outputStream = new MemoryStream();
48 | {
49 | using var compressionStream = new DeflateStream(outputStream, CompressionLevel.Optimal);
50 | using var writer = new StreamWriter(compressionStream, Encoding.UTF8);
51 | writer.Write(jsonString);
52 | }
53 |
54 | return Convert.ToBase64String(outputStream.ToArray());
55 | }
56 | catch (Exception ex)
57 | {
58 | PluginLog.Error(ex.ToString());
59 | }
60 |
61 | return null;
62 | }
63 |
64 | public static T? GetFromImportString(string? importString)
65 | {
66 | if (string.IsNullOrEmpty(importString)) return default;
67 |
68 | try
69 | {
70 | var bytes = Convert.FromBase64String(importString);
71 | using var inputStream = new MemoryStream(bytes);
72 | using var compressionStream = new DeflateStream(inputStream, CompressionMode.Decompress);
73 | using var reader = new StreamReader(compressionStream, Encoding.UTF8);
74 | var decodedJsonString = reader.ReadToEnd();
75 | return JsonConvert.DeserializeObject(decodedJsonString, _serializerSettings);
76 | }
77 | catch (Exception ex)
78 | {
79 | PluginLog.Error(ex.ToString());
80 | }
81 |
82 | return default;
83 | }
84 |
85 | public static LMeterConfig LoadConfig(string? path)
86 | {
87 | LMeterConfig? config = null;
88 |
89 | try
90 | {
91 | if (File.Exists(path))
92 | {
93 | config = JsonConvert.DeserializeObject(File.ReadAllText(path), _serializerSettings);
94 | }
95 | }
96 | catch (Exception ex)
97 | {
98 | PluginLog.Error(ex.ToString());
99 |
100 | var backupPath = $"{path}.bak";
101 | if (File.Exists(path))
102 | {
103 | try
104 | {
105 | File.Copy(path, backupPath);
106 | PluginLog.Information($"Backed up LMeter config to '{backupPath}'.");
107 | }
108 | catch
109 | {
110 | PluginLog.Warning($"Unable to back up LMeter config.");
111 | }
112 | }
113 | }
114 |
115 | return config ?? new LMeterConfig();
116 | }
117 |
118 | public static void SaveConfig(LMeterConfig config)
119 | {
120 | try
121 | {
122 | PluginLog.Verbose($"Writing out config file: {Plugin.ConfigFilePath}");
123 | var jsonString = JsonConvert.SerializeObject(config, Formatting.Indented, _serializerSettings);
124 | File.WriteAllText(Plugin.ConfigFilePath, jsonString);
125 | }
126 | catch (Exception ex)
127 | {
128 | PluginLog.Error(ex.ToString());
129 | }
130 | }
131 | }
132 |
133 | ///
134 | /// Because the game blocks the json serializer from loading assemblies at runtime, we define
135 | /// a custom SerializationBinder to ignore the assembly name for the types defined by this plugin.
136 | ///
137 | public class LMeterSerializationBinder : ISerializationBinder
138 | {
139 | // TODO: Make this automatic somehow?
140 | private static readonly List _configTypes = new ();
141 |
142 | private readonly Dictionary typeToName = new ();
143 | private readonly Dictionary nameToType = new ();
144 |
145 | public LMeterSerializationBinder()
146 | {
147 | foreach (var type in _configTypes)
148 | {
149 | if (type.FullName is not null)
150 | {
151 | this.typeToName.Add(type, type.FullName);
152 | this.nameToType.Add(type.FullName, type);
153 | }
154 | }
155 | }
156 |
157 | public void BindToName(Type serializedType, out string? assemblyName, out string? typeName)
158 | {
159 | if (this.typeToName.TryGetValue(serializedType, out string? name))
160 | {
161 | assemblyName = null;
162 | typeName = name;
163 | }
164 | else
165 | {
166 | assemblyName = serializedType.Assembly.FullName;
167 | typeName = serializedType.FullName;
168 | }
169 | }
170 |
171 | public Type BindToType(string? assemblyName, string? typeName)
172 | {
173 | if (typeName is not null && this.nameToType.TryGetValue(typeName, out Type? type)) return type;
174 |
175 | return
176 | Type.GetType($"{typeName}, {assemblyName}", true)
177 | ?? throw new TypeLoadException($"Unable to load type '{typeName}' from assembly '{assemblyName}'");
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/LMeter/src/Helpers/DrawChildScope.cs:
--------------------------------------------------------------------------------
1 | using ImGuiNET;
2 | using System;
3 | using System.Numerics;
4 |
5 |
6 | namespace LMeter.Helpers;
7 |
8 | public class DrawChildScope : IDisposable
9 | {
10 | public readonly bool Success;
11 |
12 | public DrawChildScope(string label, Vector2 size, bool border)
13 | {
14 | Success = ImGui.BeginChild(label, size, border);
15 | }
16 |
17 | public void Dispose()
18 | {
19 | ImGui.EndChild();
20 | GC.SuppressFinalize(this);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/LMeter/src/Helpers/DrawHelpers.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Interface.Internal.Notifications;
2 | using Dalamud.Interface;
3 | using ImGuiNET;
4 | using ImGuiScene;
5 | using System.Numerics;
6 | using System;
7 |
8 |
9 | namespace LMeter.Helpers;
10 |
11 | public class DrawHelpers
12 | {
13 | public static void DrawButton
14 | (
15 | string label,
16 | FontAwesomeIcon icon,
17 | Action clickAction,
18 | string? help = null,
19 | Vector2? size = null
20 | )
21 | {
22 | if (!string.IsNullOrEmpty(label))
23 | {
24 | ImGui.Text(label);
25 | ImGui.SameLine();
26 | }
27 |
28 | using (PluginManager.Instance.FontsManager.PushFont(UiBuilder.IconFont))
29 | {
30 | if (ImGui.Button(icon.ToIconString(), size ?? Vector2.Zero))
31 | {
32 | clickAction();
33 | }
34 | }
35 |
36 | if (!string.IsNullOrEmpty(help) && ImGui.IsItemHovered()) ImGui.SetTooltip(help);
37 | }
38 |
39 | public static void DrawNotification
40 | (
41 | string message,
42 | NotificationType type = NotificationType.Success,
43 | uint durationInMs = 3000,
44 | string title = "LMeter"
45 | ) =>
46 | PluginManager.Instance.PluginInterface.UiBuilder.AddNotification(message, title, type, durationInMs);
47 |
48 | public static void DrawNestIndicator(int depth)
49 | {
50 | // This draws the L shaped symbols and padding to the left of config items collapsible under a checkbox.
51 | // Shift cursor to the right to pad for children with depth more than 1.
52 | // 26 is an arbitrary value I found to be around half the width of a checkbox
53 | var oldCursor = ImGui.GetCursorPos();
54 | var offset = new Vector2(26 * Math.Max((depth - 1), 0), 2);
55 | ImGui.SetCursorPos(oldCursor + offset);
56 | ImGui.TextColored(new Vector4(229f / 255f, 57f / 255f, 57f / 255f, 1f), "\u2002\u2514");
57 | ImGui.SameLine();
58 | ImGui.SetCursorPosY(oldCursor.Y);
59 | }
60 |
61 | public static void DrawSpacing(int spacingSize)
62 | {
63 | for (var i = 0; i < spacingSize; i++)
64 | {
65 | ImGui.NewLine();
66 | }
67 | }
68 |
69 | public static void DrawIcon
70 | (
71 | uint iconId,
72 | Vector2 position,
73 | Vector2 size,
74 | ImDrawListPtr drawList
75 | )
76 | {
77 | var tex = PluginManager.Instance.TexCache.GetTextureFromIconId(iconId, 0, true);
78 |
79 | if (tex is null)
80 | {
81 | return;
82 | }
83 |
84 | drawList.AddImage(tex.ImGuiHandle, position, position + size, Vector2.Zero, Vector2.One);
85 | }
86 |
87 | public static void DrawIcon
88 | (
89 | uint iconId,
90 | Vector2 position,
91 | Vector2 size,
92 | bool cropIcon,
93 | int stackCount,
94 | bool desaturate,
95 | float opacity,
96 | ImDrawListPtr drawList
97 | )
98 | {
99 | var tex = PluginManager.Instance.TexCache.GetTextureFromIconId
100 | (
101 | iconId,
102 | (uint) stackCount,
103 | true,
104 | desaturate,
105 | opacity
106 | );
107 |
108 | if (tex is null) return;
109 |
110 | (var uv0, var uv1) = GetTexCoordinates(tex, cropIcon);
111 |
112 | drawList.AddImage(tex.ImGuiHandle, position, position + size, uv0, uv1);
113 | }
114 |
115 | public static (Vector2, Vector2) GetTexCoordinates(TextureWrap? texture, bool cropIcon = true)
116 | {
117 | if (texture == null) return (Vector2.Zero, Vector2.Zero);
118 |
119 | // Status = 24x32, show from 2,7 until 22,26
120 | //show from 0,0 until 24,32 for uncropped status icon
121 |
122 | var uv0x = cropIcon
123 | ? 4f
124 | : 1f;
125 | var uv0y = cropIcon
126 | ? 14f
127 | : 1f;
128 |
129 | var uv1x = cropIcon
130 | ? 4f
131 | : 1f;
132 | var uv1y = cropIcon
133 | ? 12f
134 | : 1f;
135 |
136 | var uv0 = new Vector2(uv0x / texture.Width, uv0y / texture.Height);
137 | var uv1 = new Vector2(1f - uv1x / texture.Width, 1f - uv1y / texture.Height);
138 |
139 | return (uv0, uv1);
140 | }
141 |
142 | public static void DrawInWindow
143 | (
144 | string name,
145 | Vector2 pos,
146 | Vector2 size,
147 | bool needsInput,
148 | bool setPosition,
149 | Action drawAction
150 | ) =>
151 | DrawInWindow(name, pos, size, needsInput, false, setPosition, drawAction);
152 |
153 | public static void DrawInWindow
154 | (
155 | string name,
156 | Vector2 pos,
157 | Vector2 size,
158 | bool needsInput,
159 | bool needsFocus,
160 | bool locked,
161 | Action drawAction,
162 | ImGuiWindowFlags extraFlags = ImGuiWindowFlags.None
163 | )
164 | {
165 | ImGuiWindowFlags windowFlags =
166 | ImGuiWindowFlags.NoSavedSettings |
167 | ImGuiWindowFlags.NoTitleBar |
168 | ImGuiWindowFlags.NoScrollbar |
169 | ImGuiWindowFlags.NoBackground |
170 | extraFlags;
171 |
172 | if (!needsInput) windowFlags |= ImGuiWindowFlags.NoInputs;
173 |
174 | if (!needsFocus) windowFlags |= ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBringToFrontOnFocus;
175 |
176 | if (locked)
177 | {
178 | windowFlags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize;
179 | ImGui.SetNextWindowSize(size);
180 | ImGui.SetNextWindowPos(pos);
181 | }
182 |
183 | ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0);
184 | ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0, 0));
185 | ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0);
186 |
187 | if (ImGui.Begin(name, windowFlags)) drawAction(ImGui.GetWindowDrawList());
188 |
189 | ImGui.PopStyleVar(3);
190 | ImGui.End();
191 | }
192 |
193 | public static void DrawText
194 | (
195 | ImDrawListPtr drawList,
196 | string text,
197 | Vector2 pos,
198 | uint color,
199 | bool outline,
200 | uint outlineColor = 0xFF000000,
201 | int thickness = 1
202 | )
203 | {
204 | // outline
205 | if (outline)
206 | {
207 | for (var i = 1; i < thickness + 1; i++)
208 | {
209 | drawList.AddText(new Vector2(pos.X - i, pos.Y + i), outlineColor, text);
210 | drawList.AddText(new Vector2(pos.X, pos.Y + i), outlineColor, text);
211 | drawList.AddText(new Vector2(pos.X + i, pos.Y + i), outlineColor, text);
212 | drawList.AddText(new Vector2(pos.X - i, pos.Y), outlineColor, text);
213 | drawList.AddText(new Vector2(pos.X + i, pos.Y), outlineColor, text);
214 | drawList.AddText(new Vector2(pos.X - i, pos.Y - i), outlineColor, text);
215 | drawList.AddText(new Vector2(pos.X, pos.Y - i), outlineColor, text);
216 | drawList.AddText(new Vector2(pos.X + i, pos.Y - i), outlineColor, text);
217 | }
218 | }
219 |
220 | // text
221 | drawList.AddText(new Vector2(pos.X, pos.Y), color, text);
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/LMeter/src/Helpers/Enums.cs:
--------------------------------------------------------------------------------
1 | namespace LMeter.Helpers;
2 |
3 | public enum Job
4 | {
5 | UKN = 0,
6 |
7 | GLA = 1,
8 | MRD = 3,
9 | PLD = 19,
10 | WAR = 21,
11 | DRK = 32,
12 | GNB = 37,
13 |
14 | CNJ = 6,
15 | WHM = 24,
16 | SCH = 28,
17 | AST = 33,
18 | SGE = 40,
19 |
20 | PGL = 2,
21 | LNC = 4,
22 | ROG = 29,
23 | MNK = 20,
24 | DRG = 22,
25 | NIN = 30,
26 | SAM = 34,
27 | RPR = 39,
28 |
29 | ARC = 5,
30 | BRD = 23,
31 | MCH = 31,
32 | DNC = 38,
33 |
34 | THM = 7,
35 | ACN = 26,
36 | BLM = 25,
37 | SMN = 27,
38 | RDM = 35,
39 | BLU = 36,
40 |
41 | CRP = 8,
42 | BSM = 9,
43 | ARM = 10,
44 | GSM = 11,
45 | LTW = 12,
46 | WVR = 13,
47 | ALC = 14,
48 | CUL = 15,
49 |
50 | MIN = 16,
51 | BOT = 17,
52 | FSH = 18
53 | }
54 |
55 | public enum JobType
56 | {
57 | All,
58 | Custom,
59 | Tanks,
60 | Casters,
61 | Melee,
62 | Ranged,
63 | Healers,
64 | DoW,
65 | DoM,
66 | Combat,
67 | Crafters,
68 | DoH,
69 | DoL
70 | }
71 |
72 | public enum DrawAnchor
73 | {
74 | Center = 0,
75 | Left = 1,
76 | Right = 2,
77 | Top = 3,
78 | TopLeft = 4,
79 | TopRight = 5,
80 | Bottom = 6,
81 | BottomLeft = 7,
82 | BottomRight = 8
83 | }
84 |
--------------------------------------------------------------------------------
/LMeter/src/Helpers/Extensions.cs:
--------------------------------------------------------------------------------
1 | using System.Numerics;
2 |
3 |
4 | namespace LMeter.Helpers;
5 |
6 | public static class Extensions
7 | {
8 | public static Vector2 AddX(this Vector2 v, float offset) =>
9 | new(v.X + offset, v.Y);
10 |
11 | public static Vector2 AddY(this Vector2 v, float offset) =>
12 | new(v.X, v.Y + offset);
13 |
14 | public static Vector4 AddTransparency(this Vector4 vec, float opacity) =>
15 | new(vec.X, vec.Y, vec.Z, vec.W * opacity);
16 |
17 | public static Vector4 AdjustColor(this Vector4 vec, float correctionFactor)
18 | {
19 | var red = vec.X;
20 | var green = vec.Y;
21 | var blue = vec.Z;
22 |
23 | if (correctionFactor < 0)
24 | {
25 | correctionFactor = 1 + correctionFactor;
26 | red *= correctionFactor;
27 | green *= correctionFactor;
28 | blue *= correctionFactor;
29 | }
30 | else
31 | {
32 | red = (1 - red) * correctionFactor + red;
33 | green = (1 - green) * correctionFactor + green;
34 | blue = (1 - blue) * correctionFactor + blue;
35 | }
36 |
37 | return new Vector4(red, green, blue, vec.W);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/LMeter/src/Helpers/FontsManager.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Interface;
2 | using Dalamud.Logging;
3 | using ImGuiNET;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Reflection;
7 | using System;
8 |
9 |
10 | namespace LMeter.Helpers
11 | {
12 | public struct FontData
13 | {
14 | public string Name;
15 | public int Size;
16 | public bool Chinese;
17 | public bool Korean;
18 |
19 | public FontData(string name, int size, bool chinese, bool korean)
20 | {
21 | Name = name;
22 | Size = size;
23 | Chinese = chinese;
24 | Korean = korean;
25 | }
26 | }
27 |
28 | public class FontScope : IDisposable
29 | {
30 | private readonly bool _fontPushed;
31 |
32 | public FontScope(bool fontPushed)
33 | {
34 | _fontPushed = fontPushed;
35 | }
36 |
37 | public void Dispose()
38 | {
39 | if (_fontPushed)
40 | {
41 | ImGui.PopFont();
42 | }
43 |
44 | GC.SuppressFinalize(this);
45 | }
46 | }
47 |
48 | public class FontsManager : IDisposable
49 | {
50 | private IEnumerable _fontData;
51 | private readonly Dictionary _imGuiFonts = new ();
52 | private string[] _fontList = new string[] { DalamudFontKey };
53 | private readonly UiBuilder _uiBuilder;
54 | public const string DalamudFontKey = "Dalamud Font";
55 | public static readonly List DefaultFontKeys =
56 | new ()
57 | {
58 | "Expressway_24",
59 | "Expressway_20",
60 | "Expressway_16"
61 | };
62 |
63 | public static string DefaultBigFontKey =>
64 | DefaultFontKeys[0];
65 | public static string DefaultMediumFontKey =>
66 | DefaultFontKeys[1];
67 | public static string DefaultSmallFontKey =>
68 | DefaultFontKeys[2];
69 |
70 | public FontsManager(UiBuilder uiBuilder, IEnumerable fonts)
71 | {
72 | _fontData = fonts;
73 | _uiBuilder = uiBuilder;
74 | _uiBuilder.BuildFonts += BuildFonts;
75 | _uiBuilder.RebuildFonts();
76 | }
77 |
78 | public void BuildFonts()
79 | {
80 | var fontDir = GetUserFontPath();
81 | if (string.IsNullOrEmpty(fontDir)) return;
82 |
83 | _imGuiFonts.Clear();
84 | var io = ImGui.GetIO();
85 |
86 | foreach (var font in _fontData)
87 | {
88 | var fontPath = $"{fontDir}{font.Name}.ttf";
89 | if (!File.Exists(fontPath)) continue;
90 |
91 | try
92 | {
93 | var ranges = this.GetCharacterRanges(font, io);
94 | var imFont = !ranges.HasValue
95 | ? io.Fonts.AddFontFromFileTTF(fontPath, font.Size)
96 | : io.Fonts.AddFontFromFileTTF(fontPath, font.Size, null, ranges.Value.Data);
97 |
98 | _imGuiFonts.Add(GetFontKey(font), imFont);
99 | }
100 | catch (Exception ex)
101 | {
102 | PluginLog.Error($"Failed to load font from path [{fontPath}]!");
103 | PluginLog.Error(ex.ToString());
104 | }
105 | }
106 |
107 | var fontList = new List() { DalamudFontKey };
108 | fontList.AddRange(_imGuiFonts.Keys);
109 | _fontList = fontList.ToArray();
110 | }
111 |
112 | public static bool ValidateFont(string[] fontOptions, int fontId, string fontKey) =>
113 | fontId < fontOptions.Length && fontOptions[fontId].Equals(fontKey);
114 |
115 | public FontScope PushFont(string fontKey)
116 | {
117 | if
118 | (
119 | string.IsNullOrEmpty(fontKey) ||
120 | fontKey.Equals(DalamudFontKey) ||
121 | !_imGuiFonts.ContainsKey(fontKey)
122 | )
123 | {
124 | return new FontScope(false);
125 | }
126 |
127 | ImGui.PushFont(this._imGuiFonts[fontKey]);
128 | return new FontScope(true);
129 | }
130 |
131 | public FontScope PushFont(ImFontPtr fontPtr)
132 | {
133 | ImGui.PushFont(fontPtr);
134 | return new FontScope(true);
135 | }
136 |
137 | public void UpdateFonts(IEnumerable fonts)
138 | {
139 | _fontData = fonts;
140 | _uiBuilder.RebuildFonts();
141 | }
142 |
143 | public string[] GetFontList() =>
144 | this._fontList;
145 |
146 | public int GetFontIndex(string fontKey)
147 | {
148 | for (var i = 0; i < _fontList.Length; i++)
149 | {
150 | if (_fontList[i].Equals(fontKey))
151 | {
152 | return i;
153 | }
154 | }
155 |
156 | return 0;
157 | }
158 |
159 | private unsafe ImVector? GetCharacterRanges(FontData font, ImGuiIOPtr io)
160 | {
161 | if (!font.Chinese && !font.Korean) return null;
162 |
163 | var builder = new ImFontGlyphRangesBuilderPtr
164 | (
165 | ImGuiNative.ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder()
166 | );
167 |
168 | if (font.Chinese)
169 | {
170 | // GetGlyphRangesChineseFull() includes Default + Hiragana, Katakana, Half-Width, Selection of 1946 Ideographs
171 | // https://skia.googlesource.com/external/github.com/ocornut/imgui/+/v1.53/extra_fonts/README.txt
172 | builder.AddRanges(io.Fonts.GetGlyphRangesChineseFull());
173 | }
174 |
175 | if (font.Korean)
176 | {
177 | builder.AddRanges(io.Fonts.GetGlyphRangesKorean());
178 | }
179 |
180 | builder.BuildRanges(out var ranges);
181 | return ranges;
182 | }
183 |
184 | public static string GetFontKey(FontData font) =>
185 | $"{font.Name}_{font.Size}" +
186 | (
187 | font.Chinese
188 | ? "_cnjp"
189 | : string.Empty
190 | ) +
191 | (
192 | font.Korean
193 | ? "_kr"
194 | : string.Empty
195 | );
196 |
197 | public static void CopyPluginFontsToUserPath()
198 | {
199 | var pluginFontPath = GetPluginFontPath();
200 | var userFontPath = GetUserFontPath();
201 |
202 | if (string.IsNullOrEmpty(pluginFontPath) || string.IsNullOrEmpty(userFontPath)) return;
203 |
204 | try
205 | {
206 | Directory.CreateDirectory(userFontPath);
207 | }
208 | catch (Exception ex)
209 | {
210 | PluginLog.Warning($"Failed to create User Font Directory {ex}");
211 | }
212 |
213 | if (!Directory.Exists(userFontPath)) return;
214 |
215 | string[] pluginFonts;
216 | try
217 | {
218 | pluginFonts = Directory.GetFiles(pluginFontPath, "*.ttf");
219 | }
220 | catch
221 | {
222 | pluginFonts = Array.Empty();
223 | }
224 |
225 | foreach (var font in pluginFonts)
226 | {
227 | try
228 | {
229 | if (!string.IsNullOrEmpty(font))
230 | {
231 | var fileName = font.Replace(pluginFontPath, string.Empty);
232 | var copyPath = Path.Combine(userFontPath, fileName);
233 | if (!File.Exists(copyPath))
234 | {
235 | File.Copy(font, copyPath, false);
236 | }
237 | }
238 | }
239 | catch (Exception ex)
240 | {
241 | PluginLog.Warning($"Failed to copy font {font} to User Font Directory: {ex}");
242 | }
243 | }
244 | }
245 |
246 | public static string GetPluginFontPath()
247 | {
248 | var path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
249 | if (path is not null)
250 | {
251 | return $"{path}\\Media\\Fonts\\";
252 | }
253 |
254 | return string.Empty;
255 | }
256 |
257 | public static string GetUserFontPath() =>
258 | $"{Plugin.ConfigFileDir}\\Fonts\\";
259 |
260 | public static string[] GetFontNamesFromPath(string? path)
261 | {
262 | if (string.IsNullOrEmpty(path)) return Array.Empty();
263 |
264 | string[] fonts;
265 | try
266 | {
267 | fonts = Directory.GetFiles(path, "*.ttf");
268 | }
269 | catch
270 | {
271 | fonts = Array.Empty();
272 | }
273 |
274 | for (var i = 0; i < fonts.Length; i++)
275 | {
276 | fonts[i] = fonts[i]
277 | .Replace(path, string.Empty)
278 | .Replace(".ttf", string.Empty, StringComparison.OrdinalIgnoreCase);
279 | }
280 |
281 | return fonts;
282 | }
283 |
284 | public void Dispose()
285 | {
286 | this.Dispose(true);
287 | GC.SuppressFinalize(this);
288 | }
289 |
290 | protected virtual void Dispose(bool disposing)
291 | {
292 | if (disposing)
293 | {
294 | _uiBuilder.BuildFonts -= BuildFonts;
295 | _imGuiFonts.Clear();
296 | }
297 | }
298 | }
299 | }
300 |
--------------------------------------------------------------------------------
/LMeter/src/Helpers/TexturesCache.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Data;
2 | using Dalamud.Interface;
3 | using Dalamud.Logging;
4 | using Dalamud.Plugin.Ipc;
5 | using Dalamud.Plugin;
6 | using Dalamud.Utility;
7 | using ImGuiScene;
8 | using Lumina.Data.Files;
9 | using Lumina.Data.Parsing.Tex;
10 | using System.Collections.Generic;
11 | using System.IO;
12 | using System.Runtime.CompilerServices;
13 | using System.Runtime.InteropServices;
14 | using System;
15 | using static Lumina.Data.Files.TexFile;
16 |
17 |
18 | namespace LMeter.Helpers;
19 |
20 | public class TexturesCache : IDisposable
21 | {
22 | private readonly Dictionary> _textureCache = new ();
23 | private readonly ICallGateSubscriber _penumbraPathResolver;
24 | private readonly DataManager _dataManager;
25 | private readonly UiBuilder _uiBuilder;
26 |
27 | public TexturesCache(DataManager dataManager, DalamudPluginInterface pluginInterface)
28 | {
29 | _penumbraPathResolver = pluginInterface.GetIpcSubscriber("Penumbra.ResolveDefaultPath");
30 | _dataManager = dataManager;
31 | _uiBuilder = pluginInterface.UiBuilder;
32 | }
33 |
34 | public TextureWrap? GetTextureFromIconId
35 | (
36 | uint iconId,
37 | uint stackCount = 0,
38 | bool hdIcon = true,
39 | bool greyScale = false,
40 | float opacity = 1f
41 | )
42 | {
43 | var key = $"{iconId}{(greyScale ? "_g" : string.Empty)}{(opacity != 1f ? "_t" : string.Empty)}";
44 | if (_textureCache.TryGetValue(key, out var tuple))
45 | {
46 | var (texture, cachedOpacity) = tuple;
47 | if (cachedOpacity == opacity) return texture;
48 |
49 | _textureCache.Remove(key);
50 | }
51 |
52 | var newTexture = this.LoadTexture(iconId + stackCount, hdIcon, greyScale, opacity);
53 | if (newTexture == null) return null;
54 |
55 | _textureCache.Add(key, new Tuple(newTexture, opacity));
56 | return newTexture;
57 | }
58 |
59 | private TextureWrap? LoadTexture(uint id, bool hdIcon, bool greyScale, float opacity = 1f)
60 | {
61 | var path = $"ui/icon/{id / 1000 * 1000:000000}/{id:000000}{(hdIcon ? "_hr1" : string.Empty)}.tex";
62 |
63 | try
64 | {
65 | var resolvedPath = _penumbraPathResolver.InvokeFunc(path);
66 |
67 | if (!string.IsNullOrEmpty(resolvedPath) && !resolvedPath.Equals(path))
68 | {
69 | return this.LoadPenumbraTexture(resolvedPath);
70 | }
71 | }
72 | catch { }
73 |
74 | try
75 | {
76 | var iconFile = _dataManager.GetFile(path);
77 | if (iconFile is null)
78 | {
79 | return null;
80 | }
81 |
82 | return GetTextureWrap(iconFile, greyScale, opacity);
83 | }
84 | catch (Exception ex)
85 | {
86 | PluginLog.Warning(ex.ToString());
87 | }
88 |
89 | return null;
90 | }
91 |
92 | private TextureWrap? LoadPenumbraTexture(string path)
93 | {
94 | try
95 | {
96 | var fileStream = new FileStream(path, FileMode.Open);
97 | var reader = new BinaryReader(fileStream);
98 |
99 | // read header
100 | int headerSize = Unsafe.SizeOf();
101 | var headerData = reader.ReadBytes(headerSize).AsSpan();
102 | var header = MemoryMarshal.Read(headerData);
103 |
104 | // read image data
105 | var rawImageData = reader.ReadBytes((int)fileStream.Length - headerSize);
106 | var imageData = new byte[header.Width * header.Height * 4];
107 |
108 | if (!ProcessTexture(header.Format, rawImageData, imageData, header.Width, header.Height)) return null;
109 |
110 | return _uiBuilder.LoadImageRaw(GetRgbaImageData(imageData), header.Width, header.Height, 4);
111 | }
112 | catch (Exception ex)
113 | {
114 | PluginLog.Error($"Error loading texture: {path} {ex}");
115 | }
116 |
117 | return null;
118 | }
119 |
120 | private static byte[] GetRgbaImageData(byte[] imageData)
121 | {
122 | var dst = new byte[imageData.Length];
123 |
124 | for (var i = 0; i < dst.Length; i += 4)
125 | {
126 | dst[i] = imageData[i + 2];
127 | dst[i + 1] = imageData[i + 1];
128 | dst[i + 2] = imageData[i];
129 | dst[i + 3] = imageData[i + 3];
130 | }
131 |
132 | return dst;
133 | }
134 |
135 | private static bool ProcessTexture(TextureFormat format, byte[] src, byte[] dst, int width, int height)
136 | {
137 | switch (format)
138 | {
139 | case TextureFormat.DXT1:
140 | {
141 | Decompress(SquishOptions.DXT1, src, dst, width, height);
142 | return true;
143 | }
144 | case TextureFormat.DXT3:
145 | {
146 | Decompress(SquishOptions.DXT3, src, dst, width, height);
147 | return true;
148 | }
149 | case TextureFormat.DXT5:
150 | {
151 | Decompress(SquishOptions.DXT5, src, dst, width, height);
152 | return true;
153 | }
154 | case TextureFormat.B5G5R5A1:
155 | {
156 | ProcessB5G5R5A1(src, dst, width, height);
157 | return true;
158 | }
159 | case TextureFormat.B4G4R4A4:
160 | {
161 | ProcessB4G4R4A4(src, dst, width, height);
162 | return true;
163 | }
164 | case TextureFormat.L8:
165 | {
166 | ProcessR3G3B2(src, dst, width, height);
167 | return true;
168 | }
169 | case TextureFormat.B8G8R8A8:
170 | {
171 | Array.Copy(src, dst, dst.Length);
172 | return true;
173 | }
174 | }
175 |
176 | return false;
177 | }
178 |
179 | private static void Decompress(SquishOptions squishOptions, byte[] src, byte[] dst, int width, int height) =>
180 | Array.Copy(Squish.DecompressImage(src, width, height, squishOptions), dst, dst.Length);
181 |
182 | private static void ProcessB5G5R5A1(Span src, byte[] dst, int width, int height)
183 | {
184 | for (var i = 0; (i + 2) <= 2 * width * height; i += 2)
185 | {
186 | var v = BitConverter.ToUInt16(src.Slice(i, sizeof(UInt16)).ToArray(), 0);
187 |
188 | var a = (uint) (v & 0x8000);
189 | var r = (uint) (v & 0x7C00);
190 | var g = (uint) (v & 0x03E0);
191 | var b = (uint) (v & 0x001F);
192 |
193 | var rgb = ((r << 9) | (g << 6) | (b << 3));
194 | var argbValue = (a * 0x1FE00 | rgb | ((rgb >> 5) & 0x070707));
195 |
196 | for (var j = 0; j < 4; ++j)
197 | {
198 | dst[i * 2 + j] = (byte) (argbValue >> (8 * j));
199 | }
200 | }
201 | }
202 |
203 | private static void ProcessB4G4R4A4(Span src, byte[] dst, int width, int height)
204 | {
205 | for (var i = 0; (i + 2) <= 2 * width * height; i += 2)
206 | {
207 | var v = BitConverter.ToUInt16(src.Slice(i, sizeof(UInt16)).ToArray(), 0);
208 |
209 | for (var j = 0; j < 4; ++j)
210 | {
211 | dst[i * 2 + j] = (byte)(((v >> (4 * j)) & 0x0F) << 4);
212 | }
213 | }
214 | }
215 |
216 | private static void ProcessR3G3B2(Span src, byte[] dst, int width, int height)
217 | {
218 | for (var i = 0; i < width * height; ++i)
219 | {
220 | var r = (uint) (src[i] & 0xE0);
221 | var g = (uint) (src[i] & 0x1C);
222 | var b = (uint) (src[i] & 0x03);
223 |
224 | dst[i * 4 + 0] = (byte) (b | (b << 2) | (b << 4) | (b << 6));
225 | dst[i * 4 + 1] = (byte) (g | (g << 3) | (g << 6));
226 | dst[i * 4 + 2] = (byte) (r | (r << 3) | (r << 6));
227 | dst[i * 4 + 3] = 0xFF;
228 | }
229 | }
230 |
231 | public void Dispose()
232 | {
233 | this.Dispose(true);
234 | GC.SuppressFinalize(this);
235 | }
236 |
237 | protected virtual void Dispose(bool disposing)
238 | {
239 | if (disposing)
240 | {
241 | foreach (var tuple in _textureCache.Values)
242 | {
243 | tuple.Item1.Dispose();
244 | }
245 |
246 | _textureCache.Clear();
247 | }
248 | }
249 |
250 | private TextureWrap GetTextureWrap(TexFile tex, bool greyScale, float opacity)
251 | {
252 | var bytes = tex.GetRgbaImageData();
253 |
254 | if (greyScale || opacity < 1f) ConvertBytes(ref bytes, greyScale, opacity);
255 |
256 | return _uiBuilder.LoadImageRaw(bytes, tex.Header.Width, tex.Header.Height, 4);
257 | }
258 |
259 | private static void ConvertBytes(ref byte[] bytes, bool greyScale, float opacity)
260 | {
261 | if (bytes.Length % 4 != 0 || opacity > 1 || opacity < 0) return;
262 |
263 | for (var i = 0; i < bytes.Length; i += 4)
264 | {
265 | if (greyScale)
266 | {
267 | int r = bytes[i] >> 2;
268 | int g = bytes[i + 1] >> 1;
269 | int b = bytes[i + 2] >> 3;
270 | byte lum = (byte) (r + g + b);
271 |
272 | bytes[i] = lum;
273 | bytes[i + 1] = lum;
274 | bytes[i + 2] = lum;
275 | }
276 |
277 | if (opacity != 1)
278 | {
279 | bytes[i + 3] = (byte) (bytes[i + 3] * opacity);
280 | }
281 | }
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/LMeter/src/Helpers/Utils.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Game.ClientState.Objects.Types;
2 | using Dalamud.Game.ClientState.Objects;
3 | using Dalamud.Logging;
4 | using System.Diagnostics;
5 | using System.Numerics;
6 | using System.Runtime.InteropServices;
7 | using System;
8 |
9 |
10 | namespace LMeter.Helpers;
11 |
12 | public static class Utils
13 | {
14 | public static Vector2 GetAnchoredPosition(Vector2 position, Vector2 size, DrawAnchor anchor) =>
15 | anchor switch
16 | {
17 | DrawAnchor.Center => position - size / 2f,
18 | DrawAnchor.Left => position + new Vector2(0, -size.Y / 2f),
19 | DrawAnchor.Right => position + new Vector2(-size.X, -size.Y / 2f),
20 | DrawAnchor.Top => position + new Vector2(-size.X / 2f, 0),
21 | DrawAnchor.TopLeft => position,
22 | DrawAnchor.TopRight => position + new Vector2(-size.X, 0),
23 | DrawAnchor.Bottom => position + new Vector2(-size.X / 2f, -size.Y),
24 | DrawAnchor.BottomLeft => position + new Vector2(0, -size.Y),
25 | DrawAnchor.BottomRight => position + new Vector2(-size.X, -size.Y),
26 | _ => position
27 | };
28 |
29 | public static GameObject? FindTargetOfTarget(ObjectTable objectTable, GameObject? player, GameObject? target)
30 | {
31 | if (target == null) return null;
32 |
33 | if (target.TargetObjectId == 0 && player != null && player.TargetObjectId == 0) return player;
34 |
35 | // only the first 200 elements in the array are relevant due to the order in which SE packs data into the array
36 | // we do a step of 2 because its always an actor followed by its companion
37 | for (var i = 0; i < 200; i += 2)
38 | {
39 | var actor = objectTable[i];
40 | if (actor?.ObjectId == target.TargetObjectId) return actor;
41 | }
42 |
43 | return null;
44 | }
45 |
46 | public static void OpenUrl(string url)
47 | {
48 | try
49 | {
50 | Process.Start(url);
51 | }
52 | catch
53 | {
54 | try
55 | {
56 | // hack because of this: https://github.com/dotnet/corefx/issues/10361
57 | if (RuntimeInformation.IsOSPlatform(osPlatform: OSPlatform.Windows))
58 | {
59 | url = url.Replace("&", "^&");
60 | Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true });
61 | }
62 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
63 | {
64 | Process.Start("xdg-open", url);
65 | }
66 | }
67 | catch (Exception e)
68 | {
69 | PluginLog.Error("Error trying to open url: " + e.Message);
70 | }
71 | }
72 | }
73 |
74 | public static string GetTagsTooltip(string[] textTags) =>
75 | $"""
76 | Available Text Tags:
77 |
78 | {string.Join("\n", textTags)}
79 |
80 | Append the characters ':k' to a numeric tag to kilo-format it.
81 | Append a '.' and a number to limit the number of characters,
82 | or the number of decimals when used with numeric values.
83 |
84 | Examples:
85 | [damagetotal] => 123,456
86 | [damagetotal:k] => 123k
87 | [damagetotal:k.1] => 123.4k
88 |
89 | [name] => Firstname Lastname
90 | [name_first.5] => First
91 | [name_last.1] => L
92 | """;
93 | }
94 |
--------------------------------------------------------------------------------
/LMeter/src/MagicValues.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Reflection;
4 |
5 |
6 | namespace LMeter;
7 |
8 | public static class MagicValues
9 | {
10 | public const string DefaultCactbotUrlQuery = "?OVERLAY_WS=ws://127.0.0.1:10501/ws";
11 | public const string DefaultCactbotUrl =
12 | "https://quisquous.github.io/cactbot/ui/raidboss/raidboss.html" + DefaultCactbotUrlQuery;
13 | public const string DiscordUrl =
14 | "https://discord.gg/C6fptVuFzZ";
15 | public const string GitRepoUrl =
16 | "https://github.com/joshua-software-dev/LMeter";
17 | public const string PatchedCryptographyDllUrl =
18 | "https://cdn.discordapp.com/attachments/1012241909403615313/1113368719834497104/System.Security.Cryptography.dll";
19 | public const string TotallyNotCefDownloadUrl =
20 | "https://github.com/joshua-software-dev/TotallyNotCef/releases/latest/download/TotallyNotCef.zip";
21 | public const string TotallyNotCefUpdateCheckUrl =
22 | "https://api.github.com/repos/joshua-software-dev/TotallyNotCef/tags";
23 | public static readonly string DllInstallLocation =
24 | Path.GetFullPath
25 | (
26 | Path.GetDirectoryName
27 | (
28 | Assembly.GetExecutingAssembly()?.Location ?? throw new NullReferenceException()
29 | ) ?? throw new NullReferenceException()
30 | );
31 | public static readonly string DefaultTotallyNotCefInstallLocation =
32 | Path.GetFullPath(Path.Join(DllInstallLocation, "../TotallyNotCef/"));
33 | }
34 |
--------------------------------------------------------------------------------
/LMeter/src/Plugin.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Data;
2 | using Dalamud.Game.ClientState.Conditions;
3 | using Dalamud.Game.ClientState;
4 | using Dalamud.Game.Command;
5 | using Dalamud.Game.Gui;
6 | using Dalamud.Interface;
7 | using Dalamud.Logging;
8 | using Dalamud.Plugin;
9 | using ImGuiScene;
10 | using LMeter.Act;
11 | using LMeter.Config;
12 | using LMeter.Helpers;
13 | using LMeter.Meter;
14 | using System.IO;
15 | using System.Reflection;
16 | using System;
17 |
18 |
19 | namespace LMeter;
20 |
21 | public class Plugin : IDalamudPlugin
22 | {
23 | private readonly PluginManager _pluginManager;
24 | public static string Changelog { get; private set; } = string.Empty;
25 | public static string ConfigFileDir { get; private set; } = string.Empty;
26 | public const string ConfigFileName = "LMeter.json";
27 | public static string ConfigFilePath { get; private set; } = string.Empty;
28 | public static string? GitHash { get; private set; }
29 | public static TextureWrap? IconTexture { get; private set; }
30 | public string Name => "LMeter";
31 | public static string? Version { get; private set; }
32 |
33 | public Plugin(
34 | ClientState clientState,
35 | CommandManager commandManager,
36 | Condition condition,
37 | DalamudPluginInterface pluginInterface,
38 | DataManager dataManager,
39 | ChatGui chatGui
40 | )
41 | {
42 | LoadVersion();
43 | Plugin.ConfigFileDir = pluginInterface.GetPluginConfigDirectory();
44 | Plugin.ConfigFilePath = Path.Combine(pluginInterface.GetPluginConfigDirectory(), Plugin.ConfigFileName);
45 |
46 | // Init TexturesCache
47 | var texCache = new TexturesCache(dataManager, pluginInterface);
48 |
49 | // Load Icon Texure
50 | Plugin.IconTexture = LoadIconTexture(pluginInterface.UiBuilder);
51 |
52 | // Load changelog
53 | Plugin.Changelog = LoadChangelog();
54 |
55 | // Load config
56 | FontsManager.CopyPluginFontsToUserPath();
57 | LMeterConfig config = ConfigHelpers.LoadConfig(Plugin.ConfigFilePath);
58 | config.FontConfig.RefreshFontList();
59 | config.ApplyConfig();
60 |
61 | // Initialize Fonts
62 | var fontsManager = new FontsManager(pluginInterface.UiBuilder, config.FontConfig.Fonts.Values);
63 |
64 | // Connect to ACT
65 | var actClient = new ActClient(chatGui, config.ActConfig, pluginInterface);
66 | actClient.Current.Start();
67 |
68 | // Start the plugin
69 | _pluginManager = new PluginManager
70 | (
71 | actClient,
72 | chatGui,
73 | clientState,
74 | commandManager,
75 | condition,
76 | config,
77 | dataManager,
78 | fontsManager,
79 | pluginInterface,
80 | texCache
81 | );
82 |
83 | // Create profile on first load
84 | if (config.FirstLoad && config.MeterList.Meters.Count == 0)
85 | {
86 | config.MeterList.Meters.Add(MeterWindow.GetDefaultMeter("Profile 1"));
87 | }
88 | config.FirstLoad = false;
89 | }
90 |
91 | private static TextureWrap? LoadIconTexture(UiBuilder uiBuilder)
92 | {
93 | var pluginPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
94 | if (string.IsNullOrEmpty(pluginPath)) return null;
95 |
96 | var iconPath = Path.Combine(pluginPath, "Media", "Images", "icon_small.png");
97 | if (!File.Exists(iconPath)) return null;
98 |
99 | try
100 | {
101 | return uiBuilder.LoadImage(iconPath);
102 | }
103 | catch (Exception ex)
104 | {
105 | PluginLog.Warning($"Failed to load LMeter Icon {ex}");
106 | }
107 |
108 | return null;
109 | }
110 |
111 | private static string LoadChangelog()
112 | {
113 | var pluginPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
114 | if (string.IsNullOrEmpty(pluginPath)) return string.Empty;
115 |
116 | var changelogPath = Path.Combine(pluginPath, "Media", "Text", "changelog.md");
117 | if (File.Exists(changelogPath))
118 | {
119 | try
120 | {
121 | return File.ReadAllText(changelogPath).Replace("%", "%%");
122 | }
123 | catch (Exception ex)
124 | {
125 | PluginLog.Warning($"Error loading changelog: {ex}");
126 | }
127 | }
128 |
129 | return string.Empty;
130 | }
131 |
132 | private static void LoadVersion()
133 | {
134 | var assemblyVersion = (AssemblyInformationalVersionAttribute) Assembly
135 | .GetExecutingAssembly()
136 | .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false)[0];
137 |
138 | (Plugin.Version, Plugin.GitHash) = assemblyVersion.InformationalVersion.Split("+") switch
139 | {
140 | [var versionNum, var gitHash] => (versionNum, gitHash),
141 | _ => throw new ArgumentException(nameof(assemblyVersion))
142 | };
143 | }
144 |
145 | public void Dispose()
146 | {
147 | this.Dispose(true);
148 | GC.SuppressFinalize(this);
149 | }
150 |
151 | protected virtual void Dispose(bool disposing)
152 | {
153 | if (disposing) _pluginManager.Dispose();
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/LMeter/src/PluginManager.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Data;
2 | using Dalamud.Game.ClientState.Conditions;
3 | using Dalamud.Game.ClientState;
4 | using Dalamud.Game.Command;
5 | using Dalamud.Game.Gui;
6 | using Dalamud.Interface.Windowing;
7 | using Dalamud.Interface;
8 | using Dalamud.Plugin;
9 | using ImGuiNET;
10 | using LMeter.Act;
11 | using LMeter.Cactbot;
12 | using LMeter.Config;
13 | using LMeter.Helpers;
14 | using LMeter.Meter;
15 | using LMeter.Windows;
16 | using System.Numerics;
17 | using System;
18 |
19 |
20 | namespace LMeter;
21 |
22 | public class PluginManager : IDisposable
23 | {
24 | private readonly Vector2 _origin = ImGui.GetMainViewport().Size / 2f;
25 | private readonly Vector2 _configSize = new (550, 550);
26 | private readonly ConfigWindow _configRoot;
27 | private readonly WindowSystem _windowSystem;
28 | private readonly ImGuiWindowFlags _mainWindowFlags =
29 | ImGuiWindowFlags.NoTitleBar |
30 | ImGuiWindowFlags.NoScrollbar |
31 | ImGuiWindowFlags.AlwaysAutoResize |
32 | ImGuiWindowFlags.NoBackground |
33 | ImGuiWindowFlags.NoInputs |
34 | ImGuiWindowFlags.NoBringToFrontOnFocus |
35 | ImGuiWindowFlags.NoSavedSettings;
36 |
37 | private readonly CommandManager _commandManager;
38 | private readonly LMeterConfig _config;
39 |
40 | public readonly ActClient ActClient;
41 | public readonly CactbotConfig CactbotConfig;
42 | public readonly ChatGui ChatGui;
43 | public readonly ClientState ClientState;
44 | public readonly Condition Condition;
45 | public readonly DataManager DataManager;
46 | public readonly FontsManager FontsManager;
47 | public readonly DalamudPluginInterface PluginInterface;
48 | public readonly TexturesCache TexCache;
49 |
50 | public static PluginManager Instance { get; private set; } = null!;
51 |
52 | public PluginManager
53 | (
54 | ActClient actClient,
55 | ChatGui chatGui,
56 | ClientState clientState,
57 | CommandManager commandManager,
58 | Condition condition,
59 | LMeterConfig config,
60 | DataManager dataManager,
61 | FontsManager fontsManager,
62 | DalamudPluginInterface pluginInterface,
63 | TexturesCache texCache
64 | )
65 | {
66 | PluginManager.Instance = this;
67 |
68 | ActClient = actClient;
69 | ChatGui = chatGui;
70 | ClientState = clientState;
71 | _commandManager = commandManager;
72 | Condition = condition;
73 | _config = config;
74 | DataManager = dataManager;
75 | FontsManager = fontsManager;
76 | PluginInterface = pluginInterface;
77 | TexCache = texCache;
78 |
79 | _configRoot = new ConfigWindow(_config, "ConfigRoot", _origin, _configSize);
80 | _windowSystem = new WindowSystem("LMeter");
81 | _windowSystem.AddWindow(_configRoot);
82 | CactbotConfig = _config.CactbotConfig;
83 |
84 | _commandManager.AddHandler(
85 | "/lm",
86 | new CommandInfo(PluginCommand)
87 | {
88 | HelpMessage =
89 | """
90 | Opens the LMeter configuration window.
91 | /lm end → Ends current ACT Encounter.
92 | /lm clear → Clears all ACT encounter log data.
93 | /lm ct → Toggles clickthrough status for the given profile.
94 | /lm toggle [on|off] → Toggles visibility for the given profile.
95 | """,
96 | ShowInHelp = true
97 | }
98 | );
99 |
100 | ClientState.Login += OnLogin;
101 | ClientState.Logout += OnLogout;
102 | PluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
103 | PluginInterface.UiBuilder.Draw += Draw;
104 | }
105 |
106 | private void Draw()
107 | {
108 | if (ClientState.IsLoggedIn && (ClientState.LocalPlayer == null || CharacterState.IsCharacterBusy())) return;
109 |
110 | _windowSystem.Draw();
111 |
112 | _config.ActConfig.TryReconnect();
113 | _config.ActConfig.TryEndEncounter();
114 |
115 | ImGuiHelpers.ForceNextWindowMainViewport();
116 | ImGui.SetNextWindowPos(Vector2.Zero);
117 | ImGui.SetNextWindowSize(ImGui.GetMainViewport().Size);
118 | if (ImGui.Begin("LMeter_Root", _mainWindowFlags))
119 | {
120 | foreach (var meter in _config.MeterList.Meters)
121 | {
122 | meter.Draw(_origin);
123 | }
124 |
125 | CactbotRaidbossWindows.Draw(_origin);
126 | }
127 |
128 | ImGui.End();
129 | }
130 |
131 | public void Clear()
132 | {
133 | ActClient.Current.Clear();
134 | foreach (var meter in _config.MeterList.Meters)
135 | {
136 | meter.Clear();
137 | }
138 | }
139 |
140 | public void Edit(IConfigurable configItem) =>
141 | _configRoot.PushConfig(configItem);
142 |
143 | public void ConfigureMeter(MeterWindow meter)
144 | {
145 | if (!_configRoot.IsOpen)
146 | {
147 | this.OpenConfigUi();
148 | this.Edit(meter);
149 | }
150 | }
151 |
152 | private void OpenConfigUi()
153 | {
154 | if (!_configRoot.IsOpen) _configRoot.PushConfig(_config);
155 | }
156 |
157 | private void OnLogin(object? sender, EventArgs? args)
158 | {
159 | if (_config.ActConfig.WaitForCharacterLogin) ActClient.Current.Start();
160 | }
161 |
162 | private void OnLogout(object? sender, EventArgs? args) =>
163 | ConfigHelpers.SaveConfig(_config);
164 |
165 | private void PluginCommand(string command, string arguments)
166 | {
167 | switch (arguments)
168 | {
169 | case "end":
170 | ActClient.Current.EndEncounter();
171 | break;
172 | case "clear":
173 | this.Clear();
174 | break;
175 | case { } argument when argument.StartsWith("toggle"):
176 | _config.MeterList.ToggleMeter(GetIntArg(argument) - 1, GetBoolArg(argument, 2));
177 | break;
178 | case { } argument when argument.StartsWith("ct"):
179 | _config.MeterList.ToggleClickThrough(GetIntArg(argument) - 1);
180 | break;
181 | default:
182 | this.ToggleWindow();
183 | break;
184 | }
185 | }
186 |
187 | private static int GetIntArg(string argument)
188 | {
189 | var args = argument.Split(" ");
190 | return
191 | args.Length > 1 &&
192 | int.TryParse(args[1], out var num)
193 | ? num
194 | : 0;
195 | }
196 |
197 | private static bool? GetBoolArg(string argument, int index = 1)
198 | {
199 | var args = argument.Split(" ");
200 | if (args.Length > index)
201 | {
202 | var arg = args[index].ToLower();
203 | return
204 | arg.Equals("on")
205 | ? true
206 | : arg.Equals("off")
207 | ? false
208 | : null;
209 | }
210 |
211 | return null;
212 | }
213 |
214 | private void ToggleWindow()
215 | {
216 | if (_configRoot.IsOpen)
217 | {
218 | _configRoot.IsOpen = false;
219 | }
220 | else
221 | {
222 | _configRoot.PushConfig(_config);
223 | }
224 | }
225 |
226 | public void Dispose()
227 | {
228 | this.Dispose(true);
229 | GC.SuppressFinalize(this);
230 | }
231 |
232 | protected virtual void Dispose(bool disposing)
233 | {
234 | if (disposing)
235 | {
236 | // Don't modify order
237 | PluginInterface.UiBuilder.Draw -= Draw;
238 | PluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi;
239 | ClientState.Login -= OnLogin;
240 | ClientState.Logout -= OnLogout;
241 | _commandManager.RemoveHandler("/lm");
242 | _windowSystem.RemoveAllWindows();
243 | this.CactbotConfig.Dispose();
244 |
245 | ActClient.Current.Dispose();
246 | _config.Dispose();
247 | FontsManager.Dispose();
248 | TexCache.Dispose();
249 | }
250 | }
251 | }
252 |
--------------------------------------------------------------------------------
/LMeter/src/Runtime/ProcessLauncher.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Logging;
2 | using System;
3 | using System.Diagnostics;
4 | using System.Linq;
5 |
6 |
7 | namespace LMeter.Runtime;
8 |
9 | public static class ProcessLauncher
10 | {
11 | public static void LaunchTotallyNotCef
12 | (
13 | string exePath,
14 | string cactbotUrl,
15 | ushort httpPort,
16 | bool enableAudio,
17 | bool bypassWebSocket
18 | )
19 | {
20 | if (Process.GetProcessesByName("TotallyNotCef").Any()) return;
21 |
22 | var process = new Process();
23 | process.EnableRaisingEvents = true;
24 | process.OutputDataReceived += new DataReceivedEventHandler(OnStdOutMessage);
25 | process.ErrorDataReceived += new DataReceivedEventHandler(OnStdErrMessage);
26 | process.Exited += (_, _) => PluginLog.Log($"{exePath} exited with code {process?.ExitCode}");
27 |
28 | process.StartInfo.FileName = exePath;
29 | process.StartInfo.Arguments =
30 | cactbotUrl + " " + httpPort + " " + (enableAudio ? 1 : 0) + " " + (bypassWebSocket ? 0 : 1);
31 |
32 | PluginLog.Log($"EXE : {process.StartInfo.FileName}");
33 | PluginLog.Log($"ARGS: {process.StartInfo.Arguments}");
34 |
35 | process.StartInfo.EnvironmentVariables["DOTNET_ROOT"] = Environment.GetEnvironmentVariable("DALAMUD_RUNTIME");
36 | process.StartInfo.EnvironmentVariables.Remove("DOTNET_BUNDLE_EXTRACT_BASE_DIR");
37 | process.StartInfo.CreateNoWindow = true;
38 | process.StartInfo.UseShellExecute = false;
39 | process.StartInfo.RedirectStandardError = true;
40 | process.StartInfo.RedirectStandardOutput = true;
41 |
42 | try
43 | {
44 | process.Start();
45 | process.BeginOutputReadLine();
46 | process.BeginErrorReadLine();
47 | }
48 | catch (Exception e)
49 | {
50 | // Prefer not crashing to not starting this process
51 | PluginLog.Log(e.ToString());
52 | }
53 | }
54 |
55 | public static void LaunchInstallFixDll(string winNewDllPath, string winOldDllPath)
56 | {
57 | var linNewDllPath = WineChecker.WindowsFullPathToLinuxPath(winNewDllPath);
58 | var linOldDllPath = WineChecker.WindowsFullPathToLinuxPath(winOldDllPath);
59 | if (linNewDllPath == null || linOldDllPath == null)
60 | {
61 | PluginLog.LogError("Could not install DLL fix.");
62 | }
63 |
64 | var process = new Process();
65 | process.EnableRaisingEvents = true;
66 | process.Exited += (_, _) => PluginLog.Log($"Process exited with code {process?.ExitCode}");
67 |
68 | process.StartInfo.FileName = "/usr/bin/env";
69 | process.StartInfo.Arguments = $"mv {linNewDllPath} {linOldDllPath}";
70 |
71 | process.StartInfo.CreateNoWindow = true;
72 | process.StartInfo.UseShellExecute = false;
73 | process.StartInfo.RedirectStandardError = false;
74 | process.StartInfo.RedirectStandardOutput = false;
75 |
76 | try
77 | {
78 | process.Start();
79 | }
80 | catch (Exception e)
81 | {
82 | // Prefer not crashing to not starting this process
83 | PluginLog.Log(e.ToString());
84 | }
85 | }
86 |
87 | private static void OnStdErrMessage(object? sender, DataReceivedEventArgs e) =>
88 | PluginLog.Debug($"STDERR: {e.Data}\n");
89 |
90 | private static void OnStdOutMessage(object? sender, DataReceivedEventArgs e) =>
91 | PluginLog.Verbose($"STDOUT: {e.Data}\n");
92 | }
93 |
--------------------------------------------------------------------------------
/LMeter/src/Runtime/ShaFixer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Net.Http;
5 | using System.Text;
6 | using System.Text.Json;
7 |
8 |
9 | namespace LMeter.Runtime;
10 |
11 | public static class ShaFixer
12 | {
13 | private static bool? _shaIsFunctional;
14 | public static bool ValidateSha1IsFunctional()
15 | {
16 | if (_shaIsFunctional != null) return _shaIsFunctional.Value;
17 |
18 | try
19 | {
20 | var bytes = new byte[128];
21 | System.Security.Cryptography.SHA1.TryHashData(bytes, bytes, out var _);
22 | _shaIsFunctional = true;
23 | return _shaIsFunctional.Value;
24 | }
25 | catch (AccessViolationException)
26 | {
27 | _shaIsFunctional = false;
28 | return _shaIsFunctional.Value;
29 | }
30 | }
31 |
32 | public static bool CanRuntimeBeFixed()
33 | {
34 | var originalDllPath = System.Reflection.Assembly.GetAssembly(typeof(System.Security.Cryptography.SHA1))?.Location;
35 | if (originalDllPath == null) return false;
36 |
37 | var dllDir = Path.GetDirectoryName(originalDllPath);
38 | if (dllDir == null) return false;
39 |
40 | return Path.Exists(Path.Join(dllDir, "..\\..\\..\\hashes-7.0.0.json"));
41 | }
42 |
43 | public static bool ModifyRuntimeWithShaFix()
44 | {
45 | var originalDllPath = System.Reflection.Assembly.GetAssembly(typeof(System.Security.Cryptography.SHA1))?.Location;
46 | if (originalDllPath == null) return false;
47 |
48 | var dllDir = Path.GetDirectoryName(originalDllPath);
49 | if (dllDir == null) return false;
50 | var newDllPath = Path.Join(dllDir, "System.Security.Cryptography2.dll");
51 | var jsonPath = Path.Join(dllDir, "..\\..\\..\\hashes-7.0.0.json");
52 | if (!Path.Exists(jsonPath)) return false;
53 |
54 | var hashJsonDict = JsonSerializer.Deserialize>(File.ReadAllText(jsonPath));
55 | if (hashJsonDict == null) return false;
56 |
57 | if (File.Exists(newDllPath)) File.Delete(newDllPath);
58 | using (var httpClient = new HttpClient())
59 | {
60 | using (var stream = httpClient.GetStreamAsync(MagicValues.PatchedCryptographyDllUrl).GetAwaiter().GetResult())
61 | {
62 | using (var fileStream = new FileStream(newDllPath, FileMode.CreateNew))
63 | {
64 | stream.CopyTo(fileStream);
65 | }
66 | }
67 | }
68 |
69 | var md5 = MonoMD5CryptoServiceProvider.Create();
70 | var hashed = md5.ComputeHash(File.ReadAllBytes(newDllPath));
71 | var sb = new StringBuilder();
72 | foreach (var bt in hashed)
73 | {
74 | sb.Append(bt.ToString("x2"));
75 | }
76 |
77 | hashJsonDict["shared\\Microsoft.NETCore.App\\7.0.0\\System.Security.Cryptography.dll"] = sb
78 | .ToString()
79 | .ToUpperInvariant();
80 | File.WriteAllText(jsonPath, JsonSerializer.Serialize(hashJsonDict));
81 |
82 | ProcessLauncher.LaunchInstallFixDll(newDllPath, originalDllPath);
83 | return true;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/LMeter/src/Runtime/WineChecker.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Win32;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Runtime.InteropServices;
7 |
8 |
9 | namespace LMeter.Runtime;
10 |
11 | public static class WineChecker
12 | {
13 | private static bool? _isRunningOnWine = null;
14 | public static bool IsRunningOnWine
15 | {
16 | get
17 | {
18 | if (_isRunningOnWine != null) return _isRunningOnWine.Value;
19 | if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
20 | {
21 | _isRunningOnWine = false;
22 | return _isRunningOnWine.Value;
23 | }
24 |
25 | using var registry = Registry.LocalMachine;
26 | _isRunningOnWine = registry.OpenSubKey("""Software\Wine""") != null;
27 | return _isRunningOnWine.Value;
28 | }
29 | }
30 |
31 | ///
32 | /// Split a directory in its components.
33 | /// Input e.g: a/b/c/d.
34 | /// Output: d, c, b, a.
35 | ///
36 | ///
37 | ///
38 | public static IEnumerable DirectorySplit(this DirectoryInfo Dir)
39 | {
40 | while (Dir != null)
41 | {
42 | yield return Dir.Name;
43 | Dir = Dir!.Parent!;
44 | }
45 | }
46 |
47 | private static string? _linuxPrefixPath = null;
48 | public static string? WindowsFullPathToLinuxPath(string? inputPath)
49 | {
50 | if (string.IsNullOrEmpty(inputPath) || !Path.Exists(inputPath)) return null;
51 |
52 | if (_linuxPrefixPath == null)
53 | {
54 | var winePrefixPath = Environment.GetEnvironmentVariable("WINEPREFIX");
55 | if (winePrefixPath == null) return null;
56 | _linuxPrefixPath = winePrefixPath + "/dosdevices/";
57 | }
58 |
59 | var dirList = DirectorySplit(new DirectoryInfo(inputPath)).ToList();
60 | if (dirList.Count < 1) return null;
61 | dirList.Reverse();
62 | dirList[0] = dirList[0].ToLowerInvariant().Replace("\\", string.Empty); // transforms `C:\` to `c:`
63 |
64 | return _linuxPrefixPath + string.Join('/', dirList);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/LMeter/src/Windows/ConfigWindow.cs:
--------------------------------------------------------------------------------
1 | using Dalamud.Interface.Windowing;
2 | using Dalamud.Interface;
3 | using ImGuiNET;
4 | using LMeter.Config;
5 | using LMeter.Helpers;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Numerics;
9 |
10 |
11 | namespace LMeter.Windows;
12 |
13 | public class ConfigWindow : Window
14 | {
15 | private const float NavBarHeight = 40;
16 | private bool _back = false;
17 | private bool _home = false;
18 | private string _name = string.Empty;
19 | private Vector2 _windowSize;
20 | private readonly Stack _configStack;
21 | private readonly LMeterConfig _config;
22 |
23 | public ConfigWindow(LMeterConfig config, string id, Vector2 position, Vector2 size) : base(id)
24 | {
25 | this.Flags =
26 | ImGuiWindowFlags.NoScrollbar |
27 | ImGuiWindowFlags.NoCollapse |
28 | ImGuiWindowFlags.NoScrollWithMouse |
29 | ImGuiWindowFlags.NoSavedSettings;
30 |
31 | this.Position = position - size / 2;
32 | this.PositionCondition = ImGuiCond.Appearing;
33 | this.SizeConstraints = new WindowSizeConstraints
34 | {
35 | MinimumSize = new (size.X, 400),
36 | MaximumSize = ImGui.GetMainViewport().Size
37 | };
38 |
39 | _windowSize = size;
40 | _configStack = new ();
41 | _config = config;
42 | }
43 |
44 | public void PushConfig(IConfigurable configItem)
45 | {
46 | _configStack.Push(configItem);
47 | _name = configItem.Name;
48 | this.IsOpen = true;
49 | }
50 |
51 | public override void PreDraw()
52 | {
53 | if (_configStack.Any())
54 | {
55 | this.WindowName = this.GetWindowTitle();
56 | ImGui.SetNextWindowSize(_windowSize);
57 | }
58 | }
59 |
60 | private string GetWindowTitle()
61 | {
62 | string title = string.Empty;
63 | title = string.Join(" > ", _configStack.Reverse().Select(c => c.Name));
64 | return title;
65 | }
66 |
67 | public override void Draw()
68 | {
69 | if (!_configStack.Any())
70 | {
71 | this.IsOpen = false;
72 | return;
73 | }
74 |
75 | var configItem = _configStack.Peek();
76 | var spacing = ImGui.GetStyle().ItemSpacing;
77 | var size = _windowSize - spacing * 2;
78 | var drawNavBar = _configStack.Count > 1;
79 |
80 | if (drawNavBar) size -= new Vector2(0, NavBarHeight + spacing.Y);
81 |
82 | IConfigPage? openPage = null;
83 | if (ImGui.BeginTabBar($"##{this.WindowName}"))
84 | {
85 | foreach (var page in configItem.GetConfigPages())
86 | {
87 | if (ImGui.BeginTabItem($"{page.Name}##{this.WindowName}"))
88 | {
89 | openPage = page;
90 | page.DrawConfig(size.AddY(-ImGui.GetCursorPosY()), spacing.X, spacing.Y);
91 | ImGui.EndTabItem();
92 | }
93 | }
94 |
95 | ImGui.EndTabBar();
96 | }
97 |
98 | if (drawNavBar) this.DrawNavBar(openPage, size, spacing.X);
99 |
100 | this.Position = ImGui.GetWindowPos();
101 | _windowSize = ImGui.GetWindowSize();
102 | }
103 |
104 | private void DrawNavBar(IConfigPage? openPage, Vector2 size, float padX)
105 | {
106 | if (!ImGui.BeginChild($"##{this.WindowName}_NavBar", new Vector2(size.X, NavBarHeight), true))
107 | {
108 | ImGui.EndChild();
109 | return;
110 | }
111 |
112 | var buttonSize = new Vector2(40, 0);
113 | var textInputWidth = 150f;
114 |
115 | DrawHelpers.DrawButton
116 | (
117 | string.Empty,
118 | FontAwesomeIcon.LongArrowAltLeft,
119 | () => _back = true,
120 | "Back",
121 | buttonSize
122 | );
123 | ImGui.SameLine();
124 |
125 | if (_configStack.Count > 2)
126 | {
127 | DrawHelpers.DrawButton(string.Empty, FontAwesomeIcon.Home, () => _home = true, "Home", buttonSize);
128 | ImGui.SameLine();
129 | }
130 | else
131 | {
132 | ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 40 + padX);
133 | }
134 |
135 | // calculate empty horizontal space based on size of buttons and text box
136 | var offset = size.X - buttonSize.X * 5 - textInputWidth - padX * 7;
137 |
138 | ImGui.SetCursorPosX(ImGui.GetCursorPosX() + offset);
139 |
140 | DrawHelpers.DrawButton
141 | (
142 | string.Empty,
143 | FontAwesomeIcon.UndoAlt,
144 | () => Reset(openPage),
145 | $"Reset {openPage?.Name} to Defaults",
146 | buttonSize
147 | );
148 | ImGui.SameLine();
149 |
150 | ImGui.PushItemWidth(textInputWidth);
151 | if (ImGui.InputText("##Input", ref _name, 64, ImGuiInputTextFlags.EnterReturnsTrue)) Rename(_name);
152 |
153 | if (ImGui.IsItemHovered()) ImGui.SetTooltip("Rename");
154 |
155 | ImGui.PopItemWidth();
156 | ImGui.SameLine();
157 |
158 | DrawHelpers.DrawButton
159 | (
160 | string.Empty,
161 | FontAwesomeIcon.Upload,
162 | () => Export(openPage),
163 | $"Export {openPage?.Name}",
164 | buttonSize
165 | );
166 | ImGui.SameLine();
167 |
168 | DrawHelpers.DrawButton
169 | (
170 | string.Empty,
171 | FontAwesomeIcon.Download,
172 | Import,
173 | $"Import {openPage?.Name}",
174 | buttonSize
175 | );
176 |
177 | ImGui.EndChild();
178 | }
179 |
180 | private void Reset(IConfigPage? openPage)
181 | {
182 | if (openPage is not null) _configStack.Peek().ImportPage(openPage.GetDefault());
183 | }
184 |
185 | private void Export(IConfigPage? openPage)
186 | {
187 | if (openPage is not null) ConfigHelpers.ExportToClipboard(openPage);
188 | }
189 |
190 | private void Import()
191 | {
192 | var importString = ImGui.GetClipboardText();
193 | var page = ConfigHelpers.GetFromImportString(importString);
194 |
195 | if (page is not null)
196 | {
197 | _configStack.Peek().ImportPage(page);
198 | }
199 | }
200 |
201 | private void Rename(string name)
202 | {
203 | if (_configStack.Any()) _configStack.Peek().Name = name;
204 | }
205 |
206 | public override void PostDraw()
207 | {
208 | if (_home)
209 | {
210 | while (_configStack.Count > 1)
211 | {
212 | _configStack.Pop();
213 | }
214 | }
215 | else if (_back)
216 | {
217 | _configStack.Pop();
218 | }
219 |
220 | if ((_home || _back) && _configStack.Count > 1)
221 | {
222 | _name = _configStack.Peek().Name;
223 | }
224 |
225 | _home = false;
226 | _back = false;
227 | }
228 |
229 | public override void OnClose()
230 | {
231 | ConfigHelpers.SaveConfig(_config);
232 | _configStack.Clear();
233 |
234 | foreach (var meter in _config.MeterList.Meters)
235 | {
236 | meter.GeneralConfig.Preview = false;
237 | }
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # THIS PROJECT HAS MOVED ITS REPOSITORY
2 | ## The most recent versions can be found here: https://gitlab.com/joshua.software.dev/LMeter
3 |
4 |
5 | # LMeter
6 |
7 | [](https://ko-fi.com/lichie)
8 |
9 | LMeter is a Dalamud plugin for displaying your ACT combat log data. The purpose of this plugin is to provide a highly customizable dps/hps meter without having to rely on clunky web-based overlays.
10 |
11 | ## Features
12 |
13 | * Customize meter information to your liking:
14 |
15 | 
16 | 
17 |
18 | * Hide meter from view based on many in game criteria:
19 |
20 | 
21 |
22 | * Optionally track encounters more closely using "In Combat" status
23 |
24 | 
25 |
26 | Track encounters more closely by automatically sending `/end` commands to ACT only when the "In Combat" status ends, rather than by time-out. This is optional.
27 |
28 | * IINACT IPC Support
29 |
30 | 
31 |
32 | Support for obtaining data from ACT using the WebSocket protocol, or using Dalamud's IPC (Inter-Plugin Communication) feature to directly communicate with [IINACT](https://github.com/marzent/IINACT), bypassing the WebSocket altogether. This makes setting up a parser in a restricted environment (ex. Linux, Steam Deck) much simpler. Connecting to IINACT using the WebSocket is also supported.
33 |
34 | ## Experimental Features
35 |
36 | * Cactbot Integration
37 |
38 | Display Cactbot timeline events and alerts using the same integrated dalamud rendering rather than a web browser overlay. More information on the limitations and how to enable and configure this feature [here](https://github.com/joshua-software-dev/LMeter/blob/master/Cactbot.md).
39 |
40 |
41 | ## How to Install
42 |
43 | LMeter is not available in the standard Dalamud plugin repository and must be installed from my third party repository.
44 |
45 | Here is the URL for my plugin repository: `https://raw.githubusercontent.com/joshua-software-dev/LMeter/master/repo.json`
46 |
--------------------------------------------------------------------------------
/Version/Version.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0.2.0.19
4 |
5 |
6 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | dotnet build -c Release
--------------------------------------------------------------------------------
/deps/fonts/Expressway.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/deps/fonts/Expressway.ttf
--------------------------------------------------------------------------------
/deps/fonts/Roboto-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/deps/fonts/Roboto-Black.ttf
--------------------------------------------------------------------------------
/deps/fonts/Roboto-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/deps/fonts/Roboto-Light.ttf
--------------------------------------------------------------------------------
/deps/fonts/big-noodle-too.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/deps/fonts/big-noodle-too.ttf
--------------------------------------------------------------------------------
/deps/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/deps/img/icon.png
--------------------------------------------------------------------------------
/deps/img/icon_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/deps/img/icon_small.png
--------------------------------------------------------------------------------
/deps/txt/changelog.md:
--------------------------------------------------------------------------------
1 | # Version 0.2.0.19
2 | - Fix occasional text doubling in cactbot popups (alarms, alerts, info)
3 |
4 | # Version 0.2.0.18
5 | - Add option to write all responses from background web browser to log file for
6 | debugging purposes.
7 |
8 | # Version 0.2.0.17
9 | - More aggressively catch errors during basic handshake to ensure plugin never
10 | crashes even when communicating with out of date background web browser
11 | versions.
12 |
13 | # Version 0.2.0.16
14 | - LMeter will now attempt a very basic handshake to determine if the responses
15 | it receives come from a program claiming to be the background web browser,
16 | and not some other program unexpectedly on the same port.
17 |
18 | # Version 0.2.0.15
19 | - Add fix for Cactbot WebSocket bypass option potentially not starting
20 | correctly if the plugin loaded before the player had finished logging in.
21 |
22 | # Version 0.2.0.14
23 | - Fix Cactbot URL potentially causing crashes on plugin load if the url was not
24 | valid.
25 |
26 | # Version 0.2.0.13
27 | - Add support for disabling WebSockets on the background web browser, instead
28 | sending data to the process obtained using Dalamud IPC (Requires IINACT)
29 | - Fix Cactbot URL not being editable if the URL exceeded 64 characters
30 |
31 | # Version 0.2.0.12
32 | - Fix reading from background browser install location for cactbot integration
33 | every frame when the config was open, rather than only when needed
34 | - Change UI to communicate more clearly when the background web browser
35 | connection is disabled or not active
36 | - Take measures to ensure the background web browser will not hang the game
37 | while the browser is starting
38 | - Improve management of background threads and background web browser to ensure
39 | the plugin will load faster, and block the game from rendering for less time
40 | on plugin load
41 | - Ensure background web browser process is always killed even when it is not
42 | responding when the plugin is given a chance to shutdown cleanly (sudden game
43 | crashes / hardware failure / bluescreens etc. prevent clean shutdowns)
44 |
45 | # Version 0.2.0.11
46 | - Fix cactbot timeline events not reading data correctly due to cactbot updates
47 |
48 | # Version 0.2.0.10
49 | - Ensure timeline events don't get left on screen should LMeter lose connection
50 | to the background web browser for any reason
51 | - Add button to reset background web browser install location back to default
52 | - Minor tweaks to Cactbot integration config presentation
53 |
54 | # Version 0.2.0.9
55 | - Add option to specify install location for background web browser for Cactbot
56 | integration
57 | - Rework Cactbot integration config screen to be easier to read and understand
58 | - Add better feedback into the state of the background web browser, to make
59 | debugging any unexpected errors easier
60 | - Add better feedback into the state of the background connection, to make
61 | debugging any unexpected errors easier
62 | - Add button to allow force killing of the background web browser
63 |
64 | # Version 0.2.0.8
65 | - Ensure plugin does not crash the game in the event of an unexpected issue
66 | launching the background web browser used for Cactbot integration
67 | - Ensure Cactbot timeline events are rendered more accurately
68 | - Cactbot timeline events now render using the same colors specified by Cactbot
69 |
70 | # Version 0.2.0.7
71 | - Add option to not automatically start background web browser, without forcing
72 | off Cactbot connections entirely
73 |
74 | # Version 0.2.0.6
75 | - Add option to disable audio prompts for Cactbot integration.
76 | - Add option to selectively enable and disable rendering of Alert message
77 | popups, Alarm message popups, Info message popups, and Timeline event popups
78 | for Cactbot integration.
79 | - Add option to render a text outline of user adjustable thickness around
80 | Alarm message popups, Alert message popups, and Info message popups to
81 | improve readability for Cactbot integration.
82 | - Add option to selectively enable and disable printing of Alert messages,
83 | Alarm messages, and Info messages to in game chat (with appropriate colors)
84 | for Cactbot integration.
85 | - Improve text rendering of Alarm messages, Alert messages, and Info messages
86 | to appear much less "pixel-ly" for Cactbot integration.
87 | - The background web browser process used for Cactbot integration now:
88 | - Auto updates correctly (this was broken last version, sorry)
89 | - Will avoid starting when already running
90 | - Has better communication with the plugin regarding its startup state,
91 | allowing for easier tracking of any issues that may arise.
92 | - Should no longer crash the game if the user rapidly requests it restart
93 | manually.
94 |
95 | # Version 0.2.0.5
96 | - Ensure that a console window does not briefly show when launching the
97 | background browser used for the Cactbot feature.
98 |
99 | # Version 0.2.0.4
100 | - Add experimental option to display Cactbot data (alerts, timeline) as well as
101 | play sound effects.
102 |
103 | # Version 0.2.0.3
104 | - Add option to show the meter at all times in a duty, even when "Hide Outside
105 | Combat" is enabled.
106 | - Add option to show meter in combat, even when "Hide Outside Duty" is enabled.
107 |
108 | # Version 0.2.0.2
109 | - Fix reading config file from pre 0.2.x.x incorrectly, hopefully for real this
110 | time
111 |
112 | # Version 0.2.0.1
113 | - Build against newest Dalamud to ensure patch 6.4 support (although there are
114 | no known issues with prior releases, and they may work fine.)
115 | - Otherwise, this only promotes the previous update from testing to general
116 | release channel.
117 |
118 | # Version 0.2.0.0 Release Candidate
119 | - [WARNING] BACKUP YOUR CONFIG BEFORE UPDATING!
120 |
121 | This is a testing release to ensure this update doesn't break people's custom
122 | LMeter configs before pushing to more users. This should be considered an
123 | alpha release. Errors loading configs are not expected to happen, but in an
124 | abundance of caution, this release is being held from general availability
125 | until more user testing is done.
126 |
127 | On Windows, the LMeter config can be found at:
128 | `%APPDATA%\XIVLauncher\pluginConfigs\LMeter\LMeter.json`
129 | and on Linux / Mac OS it can be found in:
130 | `~/.xlcore/pluginConfigs/LMeter/LMeter.json`
131 | - Add better connection status UI to make diagnosing errors during connection
132 | to ACT/IINACT easier to understand
133 | - Add option to delay connecting to ACT/IINACT until after logging into a
134 | character
135 | - Internal refactoring which should minorly improve performance, more to come
136 | in future updates.
137 |
138 | # Version 0.1.9.0
139 | - Add option to connect to IINACT using Dalamud IPC instead of using a
140 | WebSocket
141 | - Improve subscription process over pre-releases to give more info during
142 | failure states
143 | - Rename "Changelog" tab to "About / Changelog"
144 | - Add git commit info into plugin before distribution, visible from the
145 | "About / Changelog" page
146 | - Fix builds not being properly deterministic, aiding in transparency that the
147 | source code actually compiles to the build that users install.
148 | - New logo
149 |
150 | # Version 0.1.5.3
151 | - Fix bug that that caused removal of custom added fonts.
152 |
153 | # Version 0.1.5.2
154 | - Added new text tags: effectivehealing, overheal, overhealpct, maxhitname,
155 | maxhitvalue
156 | - Bars are now sorted by effective healing when the Healing sort mode is
157 | selected.
158 | - Added option to use Job color for bar text color
159 | - Fixed an issue with fonts on first time plugin load
160 |
161 | # Version 0.1.5.1
162 | - Fixed issue with auto-reconnect not working
163 | - Fixed issue with name text tags
164 | - Fixed issue with borders when Header is disabled
165 | - Fixed issue with 'Return to Current Data' option
166 | - Added new toggle option (/lm toggle [on|off])
167 |
168 | # Version 0.1.5.0
169 | - Added Encounter history right-click context menu
170 | - Added Rank text tag and Rank Text option under bar settings
171 | - Fix problem with name text tags when using your name instead of YOU
172 |
173 | # Version 0.1.4.3
174 | - Fix potential crash with certain text tags
175 | - Add position offsets for bar text
176 | - Add option for borders only around bars (not header)
177 |
178 | # Version 0.1.4.2
179 | - Fix issue with ACT data not appearing in certain dungeons
180 | - Improve logic for splitting encounters
181 |
182 | # Version 0.1.4.1
183 | - Fix potential plugin crash
184 | - Fix bug with lock/click through
185 | - Disable preview when config window is closed
186 | - Force show meter when previewing
187 |
188 | # Version 0.1.4.0
189 | - Added advanced text-tag formatting (kilo-format and decimal-format)
190 | - Text Format fields have been reset to default (please check out the new text
191 | tags!)
192 | - Added text command to show/hide Meters (/lm toggle )
193 | - Added text command to toggle click-though for Meters (/lm ct )
194 | - Added option to hide Meter if ACT is not connected
195 | - Added option to automatically attempt to reconnect to ACT
196 | - Added option to add gaps between bars
197 | - Added "Combat" job group to Visibility settings
198 | - Fixed various bugs and improved performance
199 |
200 | # Version 0.1.3.1
201 | - Make auto-end disabled by default
202 |
203 | # Version 0.1.3.0
204 | - Add options to end ACT encounter when combat ends
205 |
206 | # Version 0.1.2.0
207 | - Update for Endwalker/Dalamud api5
208 | - Add Reaper/Sage support
209 | - Add Scrolling
210 |
211 | # Version 0.1.1.0
212 | - Fix sorting
213 | - Fix bug with texture loading
214 | - Fix default websocket address
215 |
216 | # Version 0.1.0.0
217 | - Created Plugin
--------------------------------------------------------------------------------
/repo.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "Author": "Lichie, joshua.software.dev",
4 | "Name": "LMeter",
5 | "Description": "Plugin to display ACT combat log data.",
6 | "InternalName": "LMeter",
7 | "AssemblyVersion": "0.2.0.3",
8 | "TestingAssemblyVersion": "0.2.0.3",
9 | "RepoUrl": "https://github.com/joshua-software-dev/LMeter",
10 | "ApplicableVersion": "any",
11 | "DalamudApiLevel": 8,
12 | "IsHide": "False",
13 | "IsTestingExclusive": "False",
14 | "DownloadCount": 0,
15 | "LastUpdate": 0,
16 | "LoadPriority": 69420,
17 | "DownloadLinkInstall": "https://github.com/joshua-software-dev/LMeter/releases/download/v0.2.0.3/latest.zip",
18 | "DownloadLinkTesting": "https://github.com/joshua-software-dev/LMeter/releases/download/v0.2.0.3/latest.zip",
19 | "DownloadLinkUpdate": "https://github.com/joshua-software-dev/LMeter/releases/download/v0.2.0.3/latest.zip",
20 | "IconUrl": "https://raw.githubusercontent.com/joshua-software-dev/LMeter/master/deps/img/icon.png",
21 | "Changelog": "# Version 0.2.0.3\n- Add option to show the meter at all times in a duty, even when \"Hide Outside\n Combat\" is enabled.\n- Add option to show meter in combat, even when \"Hide Outside Duty\" is enabled.\n\n# Version 0.2.0.2\n- Fix reading config file from pre 0.2.x.x incorrectly, hopefully for real this\n time\n\n# Version 0.2.0.1\n- Build against newest Dalamud to ensure patch 6.4 support (although there are\n no known issues with prior releases, and they may work fine.)\n- Otherwise, this only promotes the previous update from testing to general\n release channel.\n\n# Version 0.2.0.0 Release Candidate\n- [WARNING] BACKUP YOUR CONFIG BEFORE UPDATING!\n\n This is a testing release to ensure this update doesn't break people's custom\n LMeter configs before pushing to more users. This should be considered an\n alpha release. Errors loading configs are not expected to happen, but in an\n abundance of caution, this release is being held from general availability\n until more user testing is done.\n\n On Windows, the LMeter config can be found at:\n `%APPDATA%\\XIVLauncher\\pluginConfigs\\LMeter\\LMeter.json`\n and on Linux / Mac OS it can be found in:\n `~/.xlcore/pluginConfigs/LMeter/LMeter.json`\n- Add better connection status UI to make diagnosing errors during connection\n to ACT/IINACT easier to understand\n- Add option to delay connecting to ACT/IINACT until after logging into a\n character\n- Internal refactoring which should minorly improve performance, more to come\n in future updates.\n\n# Version 0.1.9.0\n- Add option to connect to IINACT using Dalamud IPC instead of using a\n WebSocket\n- Improve subscription process over pre-releases to give more info during\n failure states\n- Rename \"Changelog\" tab to \"About / Changelog\"\n- Add git commit info into plugin before distribution, visible from the\n \"About / Changelog\" page\n- Fix builds not being properly deterministic, aiding in transparency that the\n source code actually compiles to the build that users install.\n- New logo\n\n# Version 0.1.5.3\n- Fix bug that that caused removal of custom added fonts.\n\n# Version 0.1.5.2\n- Added new text tags: effectivehealing, overheal, overhealpct, maxhitname,\n maxhitvalue\n- Bars are now sorted by effective healing when the Healing sort mode is\n selected.\n- Added option to use Job color for bar text color\n- Fixed an issue with fonts on first time plugin load\n\n# Version 0.1.5.1\n- Fixed issue with auto-reconnect not working\n- Fixed issue with name text tags\n- Fixed issue with borders when Header is disabled\n- Fixed issue with 'Return to Current Data' option\n- Added new toggle option (/lm toggle [on|off])\n\n# Version 0.1.5.0\n- Added Encounter history right-click context menu\n- Added Rank text tag and Rank Text option under bar settings\n- Fix problem with name text tags when using your name instead of YOU\n\n# Version 0.1.4.3\n- Fix potential crash with certain text tags\n- Add position offsets for bar text\n- Add option for borders only around bars (not header)\n\n# Version 0.1.4.2\n- Fix issue with ACT data not appearing in certain dungeons\n- Improve logic for splitting encounters\n\n# Version 0.1.4.1\n- Fix potential plugin crash\n- Fix bug with lock/click through\n- Disable preview when config window is closed\n- Force show meter when previewing\n\n# Version 0.1.4.0\n- Added advanced text-tag formatting (kilo-format and decimal-format)\n- Text Format fields have been reset to default (please check out the new text\n tags!)\n- Added text command to show/hide Meters (/lm toggle )\n- Added text command to toggle click-though for Meters (/lm ct )\n- Added option to hide Meter if ACT is not connected\n- Added option to automatically attempt to reconnect to ACT\n- Added option to add gaps between bars\n- Added \"Combat\" job group to Visibility settings\n- Fixed various bugs and improved performance\n\n# Version 0.1.3.1\n- Make auto-end disabled by default\n\n# Version 0.1.3.0\n- Add options to end ACT encounter when combat ends\n\n# Version 0.1.2.0\n- Update for Endwalker/Dalamud api5\n- Add Reaper/Sage support\n- Add Scrolling\n\n# Version 0.1.1.0\n- Fix sorting\n- Fix bug with texture loading\n- Fix default websocket address\n\n# Version 0.1.0.0\n- Created Plugin"
22 | },
23 | {
24 | "Author": "Lichie, joshua.software.dev",
25 | "Name": "LMeter",
26 | "Description": "Plugin to display ACT combat log data. Now with Cactbot integration!",
27 | "InternalName": "LMeter",
28 | "AssemblyVersion": "0.2.0.19",
29 | "TestingAssemblyVersion": "0.2.0.19",
30 | "RepoUrl": "https://github.com/joshua-software-dev/LMeter",
31 | "ApplicableVersion": "any",
32 | "DalamudApiLevel": 8,
33 | "IsHide": "False",
34 | "IsTestingExclusive": "True",
35 | "DownloadCount": 0,
36 | "LastUpdate": 0,
37 | "LoadPriority": 69420,
38 | "DownloadLinkInstall": "https://github.com/joshua-software-dev/LMeter/releases/download/v0.2.0.19/latest.zip",
39 | "DownloadLinkTesting": "https://github.com/joshua-software-dev/LMeter/releases/download/v0.2.0.19/latest.zip",
40 | "DownloadLinkUpdate": "https://github.com/joshua-software-dev/LMeter/releases/download/v0.2.0.19/latest.zip",
41 | "IconUrl": "https://raw.githubusercontent.com/joshua-software-dev/LMeter/master/deps/img/icon.png",
42 | "Changelog": "# Version 0.2.0.19\n- Fix occasional text doubling in cactbot popups (alarms, alerts, info)"
43 | }
44 | ]
45 |
--------------------------------------------------------------------------------
/repo/act_connection.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/repo/act_connection.png
--------------------------------------------------------------------------------
/repo/auto_hide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/repo/auto_hide.png
--------------------------------------------------------------------------------
/repo/cactbot_browser_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/repo/cactbot_browser_settings.png
--------------------------------------------------------------------------------
/repo/cactbot_connection_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/repo/cactbot_connection_settings.png
--------------------------------------------------------------------------------
/repo/cactbot_preview_positioning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/repo/cactbot_preview_positioning.png
--------------------------------------------------------------------------------
/repo/dalamud_settings_part1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/repo/dalamud_settings_part1.png
--------------------------------------------------------------------------------
/repo/dalamud_settings_part2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/repo/dalamud_settings_part2.png
--------------------------------------------------------------------------------
/repo/end_encounter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/repo/end_encounter.png
--------------------------------------------------------------------------------
/repo/meter_demo_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/repo/meter_demo_1.png
--------------------------------------------------------------------------------
/repo/meter_demo_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshua-software-dev/LMeter/e6507e1938ebd9fe25f9ead8dd597980c7e4fa88/repo/meter_demo_2.png
--------------------------------------------------------------------------------