├── .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 | [![ko-fi](https://img.shields.io/badge/donate-ko--fi-blue)](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 | ![](https://github.com/joshua-software-dev/LMeter/blob/master/repo/meter_demo_1.png) 16 | ![](https://github.com/joshua-software-dev/LMeter/blob/master/repo/meter_demo_2.png) 17 | 18 | * Hide meter from view based on many in game criteria: 19 | 20 | ![](https://github.com/joshua-software-dev/LMeter/blob/master/repo/auto_hide.png) 21 | 22 | * Optionally track encounters more closely using "In Combat" status 23 | 24 | ![](https://github.com/joshua-software-dev/LMeter/blob/master/repo/end_encounter.png) 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 | ![](https://github.com/joshua-software-dev/LMeter/blob/master/repo/act_connection.png) 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 --------------------------------------------------------------------------------