Compare commits

..

18 Commits

Author SHA1 Message Date
JonKazama-Hellion 960114c142 Force -p:Platform=x64 in workflow dotnet build steps
Security / scan (push) Successful in 17s
Build / Build (Release) (push) Successful in 26s
Release / Build and attach release ZIP (push) Successful in 34s
Project-level dotnet build defaults to AnyCPU and emits to
Craftimizer/bin/Release/, even though the csproj declares
<Platforms>x64</Platforms> and <RuntimeIdentifier>win-x64</RuntimeIdentifier>.
That mismatch silently broke the first v0.1.0 release run
(/JonKazama-Hellion/Craftimizer/actions/runs/387): build succeeded
but 'find Craftimizer/bin/x64/Release -name latest.zip' returned
'No such file or directory' so the release-action got nothing to attach.

Local builds via 'dotnet build Craftimizer.sln' work because solution-
level builds inherit the per-project Platforms list as the default,
which is a different code path from project-level builds.

Add -p:Platform=x64 to both build.yml and release.yml dotnet steps so
the CI build agrees with the find paths in the same workflow.

After this push, the v0.1.0 release can be re-triggered via Gitea's
workflow_dispatch on the tag (Actions UI -> Release -> Run workflow,
pick v0.1.0 from the Ref dropdown). The tag itself stays unchanged.
2026-05-26 20:54:26 +02:00
JonKazama-Hellion 356da24feb Extend .gitattributes: source files to eol=lf
Security / scan (push) Successful in 17s
Build / Build (Release) (push) Successful in 28s
Release / Build and attach release ZIP (push) Failing after 29s
csharpier check fails with 'different line endings than formatting would
result in' when .cs files are CRLF. Linux dev box and Gitea Actions
runners both work with LF natively. Extend the LF-override to .cs,
.csproj, .sln, .yml, .yaml, .json, .md.

Asriel's original CRLF default still applies to file types not listed,
to keep the diff against upstream minimal.
2026-05-26 20:25:32 +02:00
JonKazama-Hellion c7917afd67 Add LF override for shell scripts in .gitattributes
Upstream defaults all text files to CRLF (Windows-only dev). On Linux,
bash refuses to execute shell scripts with CRLF line endings, which
blocked the v0.1.0 pre-push hook with 'env: »bash\r«: nicht gefunden'.

Override the default for *.sh, .githooks/*, and scripts/* to eol=lf
so the hook + preflight runs cleanly on both Linux and Windows.
2026-05-26 20:24:02 +02:00
JonKazama-Hellion 862fb4782c Merge branch 'feature/v0.1.0-release-pipeline' 2026-05-26 20:22:05 +02:00
JonKazama-Hellion 614e97abf7 Add release pipeline, custom repo manifest, and pre-push linter
First release-ready cycle for Forgeimizer. Bumps the version to 0.1.0,
introduces the Hellion Forge custom repo distribution path, and wires
up the HellionChat-style pre-push lint gate.

Version:
- csproj <Version> 2.9.1.1 → 0.1.0. Forgeimizer adopts its own SemVer
  line starting at 0.1.0; the upstream Craftimizer 2.9.1.1 base is
  documented in the manifest description, CHANGELOG, README, and
  NOTICE for each release.

Release pipeline (.gitea/workflows/):
- build.yml on push/PR/dispatch: downloads Dalamud staging, dotnet
  restore + build of Craftimizer/Craftimizer.csproj. Fails the workflow
  if the plugin no longer compiles against current SDK 15.
- release.yml on v*-tag + dispatch: builds Release, locates the
  packager-produced latest.zip under Craftimizer/bin/x64/Release/,
  extracts the matching ## vX.Y.Z block from CHANGELOG.md via awk,
  appends .gitea/release-footer.md, and attaches everything to the
  Gitea release via gitea.com/actions/release-action.
- release-footer.md: install path with the custom-repo URL, conflict
  notice, attribution to Asriel Camora, MIT licence reference,
  Hellion Forge footer.
- security.yml: references the shared
  JonKazama-Hellion/security-workflows/security-scan.yml@main
  workflow (Semgrep + Trivy) on push/PR + weekly Monday schedule +
  dispatch.

Custom Dalamud repo manifest:
- repo.json at the repo root. AssemblyVersion 0.1.0.0,
  TestingAssemblyVersion 0.1.0.0, all three DownloadLink* pointing
  at releases/download/v0.1.0/latest.zip, embedded Changelog block,
  full description, tag list, icon and image URLs hosted off the
  Hellion Gitea. Subscribers add the raw URL of this file to
  Dalamud's Custom Plugin Repositories list.

Linter tooling:
- dotnet-tools.json with csharpier 1.2.6 as a local tool.
- .markdownlint.json copied from HellionChat (atx headers, dash
  bullets, underscore italics, asterisk strong) with MD060 disabled
  for our long-cell tables.
- renovate.json adapted from HellionChat: weekly schedule, grouped
  minor/patch updates per ecosystem, major updates get their own
  breaking-change PR, weekly lock file refresh, OSV vulnerability
  alerts.

Pre-push lint gate (.githooks/ + scripts/):
- .githooks/pre-push runs scripts/preflight.sh.
- preflight.sh: 4 blocks A (version consistency), B (dotnet build),
  C (dotnet csharpier check Craftimizer/), D (markdownlint via npx).
- verify-version-consistency.sh: csproj <Version> matches
  repo.json AssemblyVersion + TestingAssemblyVersion + a vX.Y.Z
  tag string is present in all three DownloadLink* URLs. Block A
  fails loud with a Fix: hint if anything drifts.
- setup-hooks.sh: sets core.hooksPath to .githooks, chmod +x the
  scripts. Run once after cloning.

CHANGELOG:
- New CHANGELOG.md at the repo root. First entry is v0.1.0 with the
  Hellion-style block (date, summary, bullet list, upstream-base
  attribution, full-history link). The release workflow extracts
  this entry verbatim as the Gitea release body.

Docs:
- README header updated with the new version badge, a release badge,
  and a build badge. New CHANGELOG reference, slash-command table,
  install section with both custom-repo and dev-install paths,
  conflict notice paragraph.

Plugin Version 0.1.0 stays in line with the verify-version-consistency
check; tag v0.1.0 is intentionally NOT pushed in this commit. Once the
user enables Gitea Actions on the repo, push the tag separately to
trigger the first release build.
2026-05-26 20:21:54 +02:00
JonKazama-Hellion b598c03e9e Apply csharpier reflow across source tree
Reformats the entire Craftimizer source tree with dotnet csharpier 1.2.6
to match the Hellion Forge house style (matches what HellionChat enforces
in its pre-push pipeline). Pure whitespace + using-block sorting; no
semantic changes.

This is a one-time noisy commit. Future code edits in this fork should
land csharpier-clean because the pre-push hook (introduced in the next
commit) runs `dotnet csharpier check Craftimizer/` as Block C of the
preflight gate.

Trade-off acknowledged: this widens the merge gap with upstream
Craftimizer should Asriel ever resume maintenance. Given the upstream
has been dormant since FFXIV 7.4 and the fork is light-rename only
(internal namespaces unchanged), the marginal cost is acceptable.
2026-05-26 20:21:21 +02:00
JonKazama-Hellion a52b9e8d76 Merge branch 'feature/forgeimizer-rebrand'
Build / build (push) Failing after 31s
Build / bench (push) Has been cancelled
2026-05-26 20:03:30 +02:00
JonKazama-Hellion 923d421e57 Rebrand fork to Forgeimizer with Craftimizer conflict detector
User-facing rebrand of the Hellion Forge maintenance fork so it installs
as a separate Dalamud plugin slot named Forgeimizer and refuses to load
in parallel with upstream Craftimizer. Internal namespaces stay as
Craftimizer.* — this is a deliberate light-rename so a future upstream
merge stays straightforward.

Manifest and build:
- csproj AssemblyName = Forgeimizer (DLL is now Forgeimizer.dll)
- csproj PackageProjectUrl points at the Hellion Gitea
- csproj Authors dual-credits Asriel Camora and Hellion Forge
- Craftimizer.json renamed to Forgeimizer.json with Name/InternalName/
  Punchline/Description/RepoUrl/IconUrl/ImageUrls/Tags updated to
  Forgeimizer + Hellion-hosted URLs

Conflict detector:
- New Utils/CraftimizerConflictDetector.cs checks
  pluginInterface.InstalledPlugins for an active InternalName=="Craftimizer"
  and throws InvalidOperationException with /xlplugins guidance if found
- Plugin.cs constructor calls the detector before Service.Initialize so
  no Dalamud state is touched on conflict

Other user-facing strings:
- WindowSystem name "Craftimizer" -> "Forgeimizer"
- Settings about-tab header "Craftimizer" -> "Forgeimizer", hyperlink
  retargeted to the Hellion Gitea repo (upstream attribution stays on
  the WorkingRobot author line below)
- Configuration.MacroMateName default "Craftimizer" -> "Forgeimizer"
- /craftimizer slash command gains a /forgeimizer alias (all other
  upstream commands stay unchanged for muscle-memory compatibility)

Docs:
- README rewritten with Forgeimizer title, install section, conflict
  notice, slash command table, dual-holder license footer
- COPYRIGHT title and source-code block updated to call out both the
  rebrand and the SDK 15 migration as Hellion Forge fork maintenance
- NOTICE.md direct-word-to-Asriel section now also covers the rebrand
  scope and the conflict detector language, with explicit invitation
  to flag anything that does not sit right

LICENSE remains unchanged — Asriel Camora's MIT notice is mandatory.
Plugin Version stays at 2.9.1.1; this is a rebrand cycle, not a
version bump.
2026-05-26 20:03:23 +02:00
JonKazama-Hellion 19c7c784f8 Add Hellion fork README, NOTICE, and COPYRIGHT
Build / build (push) Failing after 1m45s
Build / bench (push) Has been cancelled
Initial Hellion Forge documentation for the Craftimizer fork.

- Replace stub README with full project page (badges, fork scope table,
  tech stack, build instructions, project status, license block, FFXIV
  disclaimer, Hellion Forge footer)
- Add NOTICE.md with acknowledgement to Asriel Camora as the upstream
  author, direct word on fork intent, scope of changes, contact path,
  and explicit non-upstreaming rationale
- Add COPYRIGHT with dual-holder source code block (Asriel 2023 +
  Hellion Online Media 2026 for the SDK 15 maintenance), visual asset
  block (Hellion Forge logo by Florian Eck, all-rights-reserved), and
  bundled binary attribution (Raphael solver)
- Drop in the Hellion Forge logo under docs/images/

LICENSE remains unchanged — Asriel Camora's original MIT notice is
mandatory under MIT clause 3.
2026-05-26 19:45:24 +02:00
JonKazama-Hellion 531b103e8e Merge branch 'feature/dalamud-sdk-15-bump' 2026-05-26 19:35:53 +02:00
JonKazama-Hellion 8d6b603963 Bump Dalamud SDK 14.0.1 → 15.0.0 for FFXIV 7.5 API compatibility
- Update Dalamud.NET.Sdk version in csproj
- Regenerate packages.lock.json against SDK 15
- Migrate FFXIVClientStructs.FFXIV.Component.GUI.ValueType → AtkValueType
  (SynthesisValues.cs)
- Introduce local IEndObject interface in ImRaii2.cs (Dalamud's nested
  ImRaii.IEndObject was removed in SDK 15)
- Switch direct Dalamud ImRaii returns to typed disposables
  (ImRaii.TabItemDisposable, ImRaii.ColorDisposable)
- Replace `if (!panel)` / `if (plot)` with `.Success` checks for the
  local IEndObject helpers
- Fix ref-bool lifetime issue in Settings.TabItem by using the
  ImRaii.TabItem(label, flags) overload
2026-05-26 19:35:42 +02:00
Asriel Camora be349a29a4 Version 2.9.1.1
Signed-off-by: Asriel Camora <asriel@camora.dev>
2026-01-03 09:52:17 -08:00
Asriel Camora 9b69d1bc2a Use MinVer for versioning
Signed-off-by: Asriel Camora <asriel@camora.dev>
2026-01-03 08:02:45 -08:00
Asriel Camora e800bc4c75 Remove redundant check
Signed-off-by: Asriel Camora <asriel@camora.dev>
2026-01-03 07:56:07 -08:00
Asriel fdb31b5720 Merge pull request #58 from Narzaal/fix/requiredquality
fix: RequiredQuality for non expert recipes
2026-01-03 07:54:48 -08:00
Asriel Camora f26d958a5c Update Rapahel.Net
Signed-off-by: Asriel Camora <asriel@camora.dev>
2026-01-03 07:53:47 -08:00
Asriel Camora a041fc6ebf Update for 7.4 2025-12-21 23:55:42 -08:00
Narzaal 88a1be2738 Update RecipeData.cs
Allow quality calculation for recipes when RequiredQuality > 0 but not expert
2025-10-20 11:08:20 +02:00
62 changed files with 4358 additions and 1438 deletions
+24 -2
View File
@@ -1,3 +1,25 @@
# Auto detect text files and perform LF normalization # Asriel's upstream default: CRLF for text files (Windows-only dev workflow).
# Kept intact to minimise the diff against upstream Craftimizer.
* text eol=crlf * text eol=crlf
*.png binary
# Hellion Forge override: shell scripts and git hooks MUST be LF only,
# otherwise bash on Linux refuses to execute them with
# "env: »bash\r«: Datei nicht gefunden". This blocked the v0.1.0 release
# pipeline push on Linux before .gitattributes was extended.
*.sh text eol=lf
.githooks/* text eol=lf
scripts/* text eol=lf
# csharpier check fails with "different line endings than formatting would
# result in" when .cs files are CRLF. The Gitea Actions runners are Linux
# and the local dev box is Linux too, so force LF for the entire source
# tree and the supporting text manifests.
*.cs text eol=lf
*.csproj text eol=lf
*.sln text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.json text eol=lf
*.md text eol=lf
*.png binary
+37
View File
@@ -0,0 +1,37 @@
---
### Install
Add the Hellion Forge custom repository in Dalamud Settings
(`/xlsettings`**Experimental****Custom Plugin Repositories**):
```text
https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/raw/branch/main/repo.json
```
Save, then go to `/xlplugins`**All Plugins** → Refresh. Forgeimizer
shows up in the list — install it like any other plugin.
> ⚠️ **Conflict notice:** Forgeimizer refuses to load if the upstream
> Craftimizer plugin is active. Disable upstream Craftimizer in `/xlplugins`
> before enabling Forgeimizer.
### Licence and Attribution
MIT (same licence as upstream Craftimizer). Crafting logic, solver,
simulator, recipe data, synthesis hooks, and UI windows by
[Asriel Camora](https://github.com/WorkingRobot) — see
[NOTICE.md](https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/src/branch/main/NOTICE.md)
for the full acknowledgement and contact path.
Hellion Forge fork-maintenance (rebrand + SDK 15 migration + conflict
detector) by Jon Kazama / Hellion Online Media.
### Documentation
- [README.md](https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/src/branch/main/README.md)
- [CHANGELOG.md](https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/src/branch/main/CHANGELOG.md)
- [NOTICE.md](https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/src/branch/main/NOTICE.md)
- [COPYRIGHT](https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/src/branch/main/COPYRIGHT)
Maintained under **Hellion Forge** | [hellion-media.de](https://hellion-media.de)
+54
View File
@@ -0,0 +1,54 @@
name: Build
# Verifies that every push to main and every PR still builds against the
# current Dalamud staging branch. Does not produce release artefacts; the
# release workflow handles that on tag.
#
# Linux runner: gitea.com Cloud Actions provides ubuntu-latest. The plugin
# csproj targets net10.0-windows, but `dotnet build` cross-compiles on Linux
# as long as the Dalamud staging assemblies are present at the expected
# lookup path ($(HOME)/.xlcore/dalamud/Hooks/dev/, which Dalamud SDK 15 uses
# on Linux).
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
# Minimum permissions for a build-only workflow: read the repo, nothing else.
permissions:
contents: read
jobs:
build:
name: Build (Release)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup .NET 10
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5
with:
dotnet-version: 10.0.x
- name: Download Dalamud staging
run: |
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
mkdir -p "$hooks"
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
unzip -oq dalamud.zip -d "$hooks"
- name: Restore
run: dotnet restore Craftimizer/Craftimizer.csproj -p:Platform=x64
- name: Build (Release)
# -p:Platform=x64 is required: csproj declares <Platforms>x64</Platforms>
# and <RuntimeIdentifier>win-x64</RuntimeIdentifier>, but project-level
# dotnet build defaults to AnyCPU and emits to bin/Release/ instead of
# bin/x64/Release/. The release workflow's find step expects the latter.
run: dotnet build Craftimizer/Craftimizer.csproj --configuration Release --no-restore -p:Platform=x64
+143
View File
@@ -0,0 +1,143 @@
name: Release
# Triggered when a vX.Y.Z tag is pushed. Builds the plugin against the
# current Dalamud staging branch, locates the latest.zip produced by
# DalamudPackager, extracts the matching changelog block from CHANGELOG.md,
# and attaches everything to the Gitea Release.
#
# User-controlled inputs touched by this workflow:
# - the tag name (filtered by on.tags = v*, validated again at runtime
# against ^v\d+\.\d+\.\d+$ before being used in any string)
# All other values are either repo-controlled (paths under
# Craftimizer/bin/x64/Release derived from find) or pinned URLs to goatcorp
# / gitea. Nothing from a webhook event payload (issue/PR titles, commit
# messages, etc.) flows into a run-step.
on:
push:
tags:
- 'v*'
# Manual recovery trigger. Use Gitea's "Run workflow" UI and select the
# tag (e.g. v0.1.0) from the Ref dropdown - not main. The Validate tag
# ref step below hard-fails if a non-tag ref is selected, because
# release-action reads GITHUB_REF directly and rejects anything that
# does not start with refs/tags/.
workflow_dispatch:
permissions:
contents: write
jobs:
release:
name: Build and attach release ZIP
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Validate tag ref
run: |
if [[ "${GITHUB_REF}" != refs/tags/v* ]]; then
echo "::error::Release workflow must run on a v*.X.Y tag ref, got ${GITHUB_REF}"
echo "::error::Push a tag, or pick the tag (not main) in the workflow_dispatch Ref dropdown."
exit 1
fi
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Setup .NET 10
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5
with:
dotnet-version: 10.0.x
- name: Download Dalamud staging
run: |
hooks="$HOME/.xlcore/dalamud/Hooks/dev"
mkdir -p "$hooks"
curl -fsSL https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -o dalamud.zip
unzip -oq dalamud.zip -d "$hooks"
- name: Build (Release)
# -p:Platform=x64 is required: csproj declares <Platforms>x64</Platforms>
# and <RuntimeIdentifier>win-x64</RuntimeIdentifier>, but project-level
# dotnet build defaults to AnyCPU and emits to bin/Release/ instead of
# bin/x64/Release/. The Locate latest.zip step below expects the latter.
run: dotnet build Craftimizer/Craftimizer.csproj --configuration Release -p:Platform=x64
- name: Locate latest.zip
id: locate
run: |
zip="$(find Craftimizer/bin/x64/Release -name latest.zip -print -quit)"
if [ -z "$zip" ]; then
echo "latest.zip not found under Craftimizer/bin/x64/Release" >&2
exit 1
fi
echo "Found: $zip"
echo "path=$zip" >> "$GITHUB_OUTPUT"
# Extract the changelog block for the tagged version from CHANGELOG.md.
# Convention: each release block starts with `## vX.Y.Z` and ends at
# the next `## v` or the `---` trailer at the bottom. Fails the
# workflow if no block exists for the tagged version, which is the
# automated counterpart to the "csproj + repo.json + CHANGELOG kept
# in sync" rule.
- name: Generate release body
env:
TAG_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
tag="$TAG_NAME"
if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "::error::Refusing to generate release body for non-semver tag: $tag"
exit 1
fi
version="${tag#v}"
changelog="CHANGELOG.md"
# awk extracts the block starting at "## vX.Y.Z" and stops at the
# next "## v" or the "---" trailer.
block="$(awk -v ver="## v${version}" '
$0 ~ "^"ver" " { take=1; print; next }
take && /^## v[0-9]+\./ { exit }
take && /^---$/ { exit }
take { print }
' "$changelog")"
if [ -z "$block" ]; then
echo "::error::No changelog entry for version $version found in $changelog. Update the changelog before tagging a release."
exit 1
fi
footer=""
if [ -f .gitea/release-footer.md ]; then
footer="$(cat .gitea/release-footer.md)"
fi
{
echo "$block"
echo ""
echo "$footer"
} > release-body.md
echo "Generated release body for $tag:"
echo "----------------------------------------"
cat release-body.md
echo "----------------------------------------"
- name: Expose release body for release-action
id: body
shell: bash
run: |
{
echo 'content<<RELEASE_BODY_EOF'
cat release-body.md
echo 'RELEASE_BODY_EOF'
} >> "$GITHUB_OUTPUT"
- name: Attach to Gitea release
uses: https://gitea.com/actions/release-action@main
with:
files: ${{ steps.locate.outputs.path }}
body: ${{ steps.body.outputs.content }}
api_key: ${{ secrets.GITHUB_TOKEN }}
+12
View File
@@ -0,0 +1,12 @@
name: Security
on:
push:
branches: [main, master]
pull_request:
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:
jobs:
scan:
uses: JonKazama-Hellion/security-workflows/.gitea/workflows/security-scan.yml@main
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
# .githooks/pre-push — invokes preflight.sh (A=version, B=build, C=csharpier, D=markdownlint).
exec scripts/preflight.sh
+18
View File
@@ -0,0 +1,18 @@
{
"MD003": { "style": "atx" },
"MD004": { "style": "dash" },
"MD007": { "indent": 2 },
"MD009": { "br_spaces": 2, "strict": false, "list_item_empty_lines": false },
"MD013": false,
"MD024": { "siblings_only": true },
"MD029": false,
"MD033": false,
"MD036": false,
"MD040": true,
"MD041": false,
"MD046": { "style": "fenced" },
"MD048": { "style": "backtick" },
"MD049": { "style": "underscore" },
"MD050": { "style": "asterisk" },
"MD060": false
}
+5 -5
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
@@ -17,10 +17,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" /> <PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.dotTrace" Version="0.14.0" /> <PackageReference Include="BenchmarkDotNet.Diagnostics.dotTrace" Version="0.15.8" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.14.0" /> <PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.15.8" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.199"> <PackageReference Include="Meziantou.Analyzer" Version="2.0.264">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
+39
View File
@@ -0,0 +1,39 @@
# Changelog
## v0.1.0 — First Hellion fork release (2026-05-26)
Initial Hellion Forge maintenance release of the Craftimizer fork. Combines
the Dalamud SDK 15 migration, the rebrand to Forgeimizer, and the conflict
detector into one shippable plugin.
- **Dalamud SDK 14 → 15** for FFXIV 7.5+ compatibility. Migrates the call
sites the SDK 15 compiler refused: `FFXIVClientStructs.FFXIV.Component.GUI.ValueType`
renamed to `AtkValueType`; Dalamud `ImRaii.IEndObject` (removed in SDK 15)
replaced by a local `IEndObject` interface in `ImRaii2.cs`; direct Dalamud
ImRaii returns switched to typed disposables (`ImRaii.TabItemDisposable`,
`ImRaii.ColorDisposable`); `Settings.TabItem` ref-bool lifetime issue fixed
by switching to the `ImRaii.TabItem(label, flags)` overload.
- **Rebrand to Forgeimizer.** Assembly name, plugin manifest, `WindowSystem`
name, the About-tab header, the `MacroMate` default, and a new
`/forgeimizer` slash command alias all use the new name. The plugin
installs into its own `pluginConfigs/Forgeimizer/` slot and shows up
separately in `/xlplugins`.
- **Conflict detector.** Forgeimizer refuses to load if upstream Craftimizer
is active in the same Dalamud instance. Throws an `InvalidOperationException`
with a clear message pointing at `/xlplugins`. Both plugins hook
`UseAction` and `IsActionHighlighted`, so running them in parallel would
corrupt either's state.
- **Hellion Forge custom repo.** Friend-circle distribution via
`https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/raw/branch/main/repo.json`
for Dalamud's custom plugin repositories list.
Internal namespaces (`Craftimizer.*`) intentionally left alone. Crafting
logic, solver, simulator, recipe data layer, synthesis hooks, macro engine,
and all UI windows are unchanged from upstream Craftimizer 2.9.1.1.
Based on Craftimizer 2.9.1.1 (upstream WorkingRobot/Craftimizer, MIT).
---
Full history:
<https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/releases>
+61
View File
@@ -0,0 +1,61 @@
Forgeimizer — Hellion Forge fork of Craftimizer by Asriel Camora
═══════════════════════════════════════════════════════════════════
Source code
═══════════════════════════════════════════════════════════════════
Copyright (c) 2023 Asriel Camora
Original author of Craftimizer (https://github.com/WorkingRobot/Craftimizer).
The entire architecture, simulator, solver, recipe data layer,
synthesis hooks, macro engine, and every UI window are Asriel's
work. Forgeimizer would not exist without it.
Copyright (c) 2026 Hellion Online Media
Hellion Forge fork maintenance:
1. Rebrand to Forgeimizer (AssemblyName, InternalName, plugin
manifest, WindowSystem name, conflict detector, /forgeimizer
slash command alias).
2. Dalamud SDK 14 → 15 migration for the FFXIV 7.5 / Dalamud
API 15 cycle. Scope: the ValueType → AtkValueType rename in
FFXIVClientStructs, the local IEndObject interface that
replaces the removed Dalamud ImRaii.IEndObject, the typed
ImRaii disposable returns (TabItemDisposable, ColorDisposable),
and the ImRaii.TabItem ref-bool lifetime fix.
No functional changes to crafting logic, solver, or UI behaviour.
Source code is licensed under the MIT License. The full Licence text
lives in the LICENSE file at the root of this repository.
═══════════════════════════════════════════════════════════════════
Visual assets
═══════════════════════════════════════════════════════════════════
Copyright (c) 2026 Florian Eck
Designer of the Hellion Forge logo (docs/images/hellion-forge.png).
Exclusive usage and marketing rights licensed to Hellion Online
Media. This asset is NOT covered by the MIT source code licence
above and may not be reused, modified, or redistributed without
separate permission from the copyright holder.
Copyright (c) 2023 Asriel Camora
Designer of the original Craftimizer plugin icon (icon.png), the
embedded graphics (Craftimizer/Graphics/*.png), and the
documentation screenshots (Images/*.png). Forgeimizer continues
to use these assets unchanged as both attribution to the upstream
author and as the Dalamud plugin icon. Asset reuse follows the
MIT Licence above.
═══════════════════════════════════════════════════════════════════
Bundled binary assets
═══════════════════════════════════════════════════════════════════
Raphael solver (Craftimizer/raphael_bindings.dll, Raphael.Net.dll)
Upstream Rust + .NET binding crate bundled as a pre-built binary
by the original Craftimizer build. Licensing follows the upstream
Raphael project; see the upstream Craftimizer repository for the
attribution chain.
═══════════════════════════════════════════════════════════════════
Acknowledgements directed at the upstream author live in NOTICE.md.
+21 -14
View File
@@ -1,10 +1,10 @@
using Craftimizer.Simulator.Actions;
using Craftimizer.Solver;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Craftimizer.Simulator.Actions;
using Craftimizer.Solver;
namespace Craftimizer.Plugin; namespace Craftimizer.Plugin;
@@ -13,7 +13,8 @@ public class StoredActionTypeConverter : JsonConverter<ActionType[]>
public override ActionType[] Read( public override ActionType[] Read(
ref Utf8JsonReader reader, ref Utf8JsonReader reader,
Type typeToConvert, Type typeToConvert,
JsonSerializerOptions options) JsonSerializerOptions options
)
{ {
if (reader.TokenType != JsonTokenType.StartArray) if (reader.TokenType != JsonTokenType.StartArray)
throw new JsonException(); throw new JsonException();
@@ -87,7 +88,8 @@ public class StoredActionTypeConverter : JsonConverter<ActionType[]>
public override void Write( public override void Write(
Utf8JsonWriter writer, Utf8JsonWriter writer,
ActionType[] value, ActionType[] value,
JsonSerializerOptions options) JsonSerializerOptions options
)
{ {
writer.WriteStartArray(); writer.WriteStartArray();
foreach (var item in value) foreach (var item in value)
@@ -101,14 +103,18 @@ public class Macro
public static event Action<Macro>? OnMacroChanged; public static event Action<Macro>? OnMacroChanged;
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
[JsonInclude] [JsonPropertyName("Actions")]
[JsonInclude]
[JsonPropertyName("Actions")]
internal ActionType[] actions { get; set; } = []; internal ActionType[] actions { get; set; } = [];
[JsonIgnore] [JsonIgnore]
public IReadOnlyList<ActionType> Actions public IReadOnlyList<ActionType> Actions
{ {
get => actions; get => actions;
set => ActionEnumerable = value; set => ActionEnumerable = value;
} }
[JsonIgnore] [JsonIgnore]
public IEnumerable<ActionType> ActionEnumerable public IEnumerable<ActionType> ActionEnumerable
{ {
@@ -127,7 +133,7 @@ public class MacroCopyConfiguration
OpenWindow, // useful for big macros OpenWindow, // useful for big macros
CopyToMacro, // (add option for down or right) (max macro count; open copy-paste window if too much) CopyToMacro, // (add option for down or right) (max macro count; open copy-paste window if too much)
CopyToClipboard, CopyToClipboard,
CopyToMacroMate CopyToMacroMate,
} }
public CopyType Type { get; set; } = CopyType.OpenWindow; public CopyType Type { get; set; } = CopyType.OpenWindow;
@@ -139,7 +145,7 @@ public class MacroCopyConfiguration
public int MaxMacroCount { get; set; } = 5; public int MaxMacroCount { get; set; } = 5;
// CopyToMacroMate // CopyToMacroMate
public string MacroMateName { get; set; } = "Craftimizer"; public string MacroMateName { get; set; } = "Forgeimizer";
public string MacroMateParent { get; set; } = string.Empty; public string MacroMateParent { get; set; } = string.Empty;
// Add /nextmacro [down] // Add /nextmacro [down]
@@ -172,13 +178,15 @@ public partial class Configuration
{ {
Colorful, Colorful,
Simple, Simple,
None None,
} }
public static event Action? OnMacroListChanged; public static event Action? OnMacroListChanged;
[JsonInclude] [JsonPropertyName("Macros")] [JsonInclude]
[JsonPropertyName("Macros")]
internal List<Macro> macros { get; private set; } = []; internal List<Macro> macros { get; private set; } = [];
[JsonIgnore] [JsonIgnore]
public IReadOnlyList<Macro> Macros => macros; public IReadOnlyList<Macro> Macros => macros;
public int ReliabilitySimulationCount { get; set; } = 1000; public int ReliabilitySimulationCount { get; set; } = 1000;
@@ -244,10 +252,8 @@ public partial class Configuration
[JsonSerializable(typeof(Configuration))] [JsonSerializable(typeof(Configuration))]
internal sealed partial class JsonContext : JsonSerializerContext internal sealed partial class JsonContext : JsonSerializerContext
{ {
public static JsonSerializerOptions DeserializeOptions { get; } = new() public static JsonSerializerOptions DeserializeOptions { get; } =
{ new() { Converters = { new StoredActionTypeConverter() } };
Converters = { new StoredActionTypeConverter() }
};
} }
public void Save() public void Save()
@@ -265,7 +271,8 @@ public partial class Configuration
using var stream = f.OpenRead(); using var stream = f.OpenRead();
// System.InvalidOperationException: Setting init-only properties is not supported in source generation mode. // System.InvalidOperationException: Setting init-only properties is not supported in source generation mode.
return JsonSerializer.Deserialize<Configuration>(stream, JsonContext.DeserializeOptions) ?? new(); return JsonSerializer.Deserialize<Configuration>(stream, JsonContext.DeserializeOptions)
?? new();
} }
return new(); return new();
} }
+39 -39
View File
@@ -1,44 +1,44 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Dalamud.NET.Sdk/13.0.0"> <Project Sdk="Dalamud.NET.Sdk/15.0.0">
<PropertyGroup> <PropertyGroup>
<Authors>Asriel Camora</Authors> <Authors>Asriel Camora (original); Jon Kazama / Hellion Forge (fork)</Authors>
<Version>2.8.0.0</Version> <Version>0.1.0</Version>
<PackageProjectUrl>https://github.com/WorkingRobot/Craftimizer.git</PackageProjectUrl> <AssemblyName>Forgeimizer</AssemblyName>
<Configurations>Debug;Release</Configurations> <PackageProjectUrl>https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer</PackageProjectUrl>
</PropertyGroup> <Configurations>Debug;Release</Configurations>
</PropertyGroup>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0-windows7.0</TargetFramework> <TargetFramework>net10.0-windows</TargetFramework>
<Platforms>x64</Platforms> <Platforms>x64</Platforms>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly> <ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile> <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Graphics\icon.png" />
<EmbeddedResource Include="Graphics\horse_icon.png" />
<EmbeddedResource Include="Graphics\collectible_badge.png" />
<EmbeddedResource Include="Graphics\expert.png" />
<EmbeddedResource Include="Graphics\expert_badge.png" />
<EmbeddedResource Include="Graphics\no_manip.png" />
<EmbeddedResource Include="Graphics\specialist.png" />
<EmbeddedResource Include="Graphics\splendorous.png" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MathNet.Numerics" Version="5.0.0" /> <EmbeddedResource Include="Graphics\icon.png" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.199"> <EmbeddedResource Include="Graphics\horse_icon.png" />
<PrivateAssets>all</PrivateAssets> <EmbeddedResource Include="Graphics\collectible_badge.png" />
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <EmbeddedResource Include="Graphics\expert.png" />
</PackageReference> <EmbeddedResource Include="Graphics\expert_badge.png" />
<ProjectReference Include="..\Simulator\Craftimizer.Simulator.csproj" /> <EmbeddedResource Include="Graphics\no_manip.png" />
<ProjectReference Include="..\Solver\Craftimizer.Solver.csproj" /> <EmbeddedResource Include="Graphics\specialist.png" />
</ItemGroup> <EmbeddedResource Include="Graphics\splendorous.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.264">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<ProjectReference Include="..\Simulator\Craftimizer.Simulator.csproj" />
<ProjectReference Include="..\Solver\Craftimizer.Solver.csproj" />
</ItemGroup>
</Project> </Project>
-30
View File
@@ -1,30 +0,0 @@
{
"Author": "Asriel",
"Name": "Craftimizer",
"Punchline": "Simulate crafts, create computer-assisted macros, and get mid-craft suggestions from the comfort of your own game!",
"Description": "Allows you to generate macros and simulate all sorts of crafts without having to open another app. Open your crafting log to get started!",
"RepoUrl": "https://github.com/WorkingRobot/Craftimizer",
"InternalName": "Craftimizer",
"ApplicableVersion": "any",
"Tags": [
"crafting",
"doh",
"craft",
"macro",
"solver",
"generator",
"generate",
"simulate",
"sim",
"simulator"
],
"CategoryTags": [
"Jobs"
],
"IconUrl": "https://git.camora.dev/asriel/Craftimizer/raw/branch/main/icon.png",
"ImageUrls": [
"https://git.camora.dev/asriel/Craftimizer/raw/branch/main/Images/RecipeNote.png",
"https://git.camora.dev/asriel/Craftimizer/raw/branch/main/Images/SynthHelper.png",
"https://git.camora.dev/asriel/Craftimizer/raw/branch/main/Images/MacroEditor.png"
]
}
+32
View File
@@ -0,0 +1,32 @@
{
"Author": "Asriel Camora (original); Jon Kazama / Hellion Forge (fork)",
"Name": "Forgeimizer",
"Punchline": "A Hellion Forge plugin. Crafting simulator and macro solver for FFXIV, maintenance fork of Asriel Camora's Craftimizer kept current for Dalamud API 15+.",
"Description": "A Hellion Forge plugin — a maintenance fork of Craftimizer by Asriel Camora, brought back to life on Dalamud SDK 15 for FFXIV 7.5+.\n\nSimulate crafts, create computer-assisted macros, and get mid-craft suggestions from the comfort of your own game. Open your crafting log to get started.\n\nAll features below come from upstream Craftimizer, unchanged in this fork:\n- Crafting simulator with full action state tracking\n- Macro solver (Raphael Rust crate, bundled)\n- Recipe note overlay\n- Synthesis helper window\n- Macro editor and macro list\n\nThis fork:\n- Refuses to load if upstream Craftimizer is active (no parallel hooks)\n- No features added, no design changes vs upstream\n- Will archive if Asriel ships an official SDK 15 update upstream\n\nSource: https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer",
"RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer",
"InternalName": "Forgeimizer",
"ApplicableVersion": "any",
"Tags": [
"crafting",
"doh",
"craft",
"macro",
"solver",
"generator",
"generate",
"simulate",
"sim",
"simulator",
"hellion",
"forge"
],
"CategoryTags": [
"Jobs"
],
"IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/raw/branch/main/icon.png",
"ImageUrls": [
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/raw/branch/main/Images/RecipeNote.png",
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/raw/branch/main/Images/SynthHelper.png",
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/raw/branch/main/Images/MacroEditor.png"
]
}
+86 -20
View File
@@ -1,9 +1,9 @@
using Dalamud.Bindings.ImGui;
using System; using System;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Dalamud.Bindings.ImGui;
namespace Craftimizer.Plugin; namespace Craftimizer.Plugin;
@@ -11,22 +11,50 @@ internal static unsafe class ImGuiExtras
{ {
// https://github.com/ImGuiNET/ImGui.NET/blob/069363672fed940ebdaa02f9b032c282b66467c7/src/CodeGenerator/definitions/cimgui/definitions.json#L25394 // https://github.com/ImGuiNET/ImGui.NET/blob/069363672fed940ebdaa02f9b032c282b66467c7/src/CodeGenerator/definitions/cimgui/definitions.json#L25394
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
private static extern unsafe byte igInputTextEx(byte* label, byte* hint, byte* buf, int buf_size, Vector2 size, ImGuiInputTextFlags flags, ImGuiInputTextCallback? callback, void* user_data); private static extern unsafe byte igInputTextEx(
byte* label,
byte* hint,
byte* buf,
int buf_size,
Vector2 size,
ImGuiInputTextFlags flags,
ImGuiInputTextCallback? callback,
void* user_data
);
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
private static extern bool igItemAdd(Vector4 bb, uint id, Vector4* navBb, uint flags); private static extern bool igItemAdd(Vector4 bb, uint id, Vector4* navBb, uint flags);
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
private static extern bool igButtonBehavior(Vector4 bb, uint id, bool* outHovered, bool* outHeld, ImGuiButtonFlags flags); private static extern bool igButtonBehavior(
Vector4 bb,
uint id,
bool* outHovered,
bool* outHeld,
ImGuiButtonFlags flags
);
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
private static extern bool igItemSize_Vec2(Vector2 size, float text_baseline_y = -1.0f); private static extern bool igItemSize_Vec2(Vector2 size, float text_baseline_y = -1.0f);
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
private static extern void igRenderFrame(Vector2 p_min, Vector2 p_max, uint fill_col, bool border = true, float rounding = 0.0f); private static extern void igRenderFrame(
Vector2 p_min,
Vector2 p_max,
uint fill_col,
bool border = true,
float rounding = 0.0f
);
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
private static extern void igRenderRectFilledRangeH(ImDrawList* draw_list, Vector4* rect, uint col, float x_start_norm, float x_end_norm, float rounding); private static extern void igRenderRectFilledRangeH(
ImDrawList* draw_list,
Vector4* rect,
uint col,
float x_start_norm,
float x_end_norm,
float rounding
);
[DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)]
private static extern ImGuiItemFlags igGetItemFlags(); private static extern ImGuiItemFlags igGetItemFlags();
@@ -74,7 +102,16 @@ internal static unsafe class ImGuiExtras
#endregion #endregion
// Based off of code from InputTextWithHint: https://github.com/ImGuiNET/ImGui.NET/blob/069363672fed940ebdaa02f9b032c282b66467c7/src/ImGui.NET/ImGui.Manual.cs#L271 // Based off of code from InputTextWithHint: https://github.com/ImGuiNET/ImGui.NET/blob/069363672fed940ebdaa02f9b032c282b66467c7/src/ImGui.NET/ImGui.Manual.cs#L271
public static unsafe bool InputTextEx(string label, string hint, ref string input, int maxLength, Vector2 size, ImGuiInputTextFlags flags = ImGuiInputTextFlags.None, ImGuiInputTextCallback? callback = null, IntPtr user_data = default) public static unsafe bool InputTextEx(
string label,
string hint,
ref string input,
int maxLength,
Vector2 size,
ImGuiInputTextFlags flags = ImGuiInputTextFlags.None,
ImGuiInputTextCallback? callback = null,
IntPtr user_data = default
)
{ {
var utf8LabelByteCount = Encoding.UTF8.GetByteCount(label); var utf8LabelByteCount = Encoding.UTF8.GetByteCount(label);
byte* utf8LabelBytes; byte* utf8LabelBytes;
@@ -132,7 +169,8 @@ internal static unsafe class ImGuiExtras
size, size,
flags, flags,
callback, callback,
user_data.ToPointer()); user_data.ToPointer()
);
if (!AreStringsEqual(originalUtf8InputBytes, inputBufSize, utf8InputBytes)) if (!AreStringsEqual(originalUtf8InputBytes, inputBufSize, utf8InputBytes))
{ {
input = StringFromPtr(utf8InputBytes); input = StringFromPtr(utf8InputBytes);
@@ -155,8 +193,7 @@ internal static unsafe class ImGuiExtras
return result != 0; return result != 0;
} }
public static unsafe bool ItemAdd(Vector4 bb, uint id) => public static unsafe bool ItemAdd(Vector4 bb, uint id) => ItemAdd(bb, id, out _);
ItemAdd(bb, id, out _);
public static unsafe bool ItemAdd(Vector4 bb, uint id, out Vector4 navBb, uint flags = 0) public static unsafe bool ItemAdd(Vector4 bb, uint id, out Vector4 navBb, uint flags = 0)
{ {
@@ -166,7 +203,13 @@ internal static unsafe class ImGuiExtras
} }
} }
public static unsafe bool ButtonBehavior(Vector4 bb, uint id, out bool hovered, out bool held, ImGuiButtonFlags flags) public static unsafe bool ButtonBehavior(
Vector4 bb,
uint id,
out bool hovered,
out bool held,
ImGuiButtonFlags flags
)
{ {
fixed (bool* hoveredPtr = &hovered) fixed (bool* hoveredPtr = &hovered)
fixed (bool* heldPtr = &held) fixed (bool* heldPtr = &held)
@@ -175,19 +218,34 @@ internal static unsafe class ImGuiExtras
} }
} }
public static unsafe void RenderFrame(Vector2 p_min, Vector2 p_max, uint fill_col, bool border = true, float rounding = 0.0f) => public static unsafe void RenderFrame(
igRenderFrame(p_min, p_max, fill_col, border, rounding); Vector2 p_min,
Vector2 p_max,
uint fill_col,
bool border = true,
float rounding = 0.0f
) => igRenderFrame(p_min, p_max, fill_col, border, rounding);
public static unsafe void RenderRectFilledRangeH(ImDrawListPtr draw_list, Vector4 rect, uint col, float x_start_norm, float x_end_norm, float rounding) => public static unsafe void RenderRectFilledRangeH(
igRenderRectFilledRangeH(draw_list, &rect, col, x_start_norm, x_end_norm, rounding); ImDrawListPtr draw_list,
Vector4 rect,
uint col,
float x_start_norm,
float x_end_norm,
float rounding
) => igRenderRectFilledRangeH(draw_list, &rect, col, x_start_norm, x_end_norm, rounding);
public static unsafe bool ItemSize(Vector2 size, float text_baseline_y = -1.0f) => public static unsafe bool ItemSize(Vector2 size, float text_baseline_y = -1.0f) =>
igItemSize_Vec2(size, text_baseline_y); igItemSize_Vec2(size, text_baseline_y);
public static unsafe ImGuiItemFlags GetItemFlags() => public static unsafe ImGuiItemFlags GetItemFlags() => igGetItemFlags();
igGetItemFlags();
public static unsafe int? CalcWordWrapPositionA(this ImFontPtr font, float scale, ReadOnlySpan<char> text, float wrap_width) public static unsafe int? CalcWordWrapPositionA(
this ImFontPtr font,
float scale,
ReadOnlySpan<char> text,
float wrap_width
)
{ {
var utf8TextByteCount = Encoding.UTF8.GetByteCount(text); var utf8TextByteCount = Encoding.UTF8.GetByteCount(text);
byte* utf8TextBytes; byte* utf8TextBytes;
@@ -202,7 +260,13 @@ internal static unsafe class ImGuiExtras
} }
GetUtf8(text, utf8TextBytes, utf8TextByteCount); GetUtf8(text, utf8TextBytes, utf8TextByteCount);
var ret = ImGuiNative.CalcWordWrapPositionA(font, scale, utf8TextBytes, utf8TextBytes + utf8TextByteCount, wrap_width); var ret = ImGuiNative.CalcWordWrapPositionA(
font,
scale,
utf8TextBytes,
utf8TextBytes + utf8TextByteCount,
wrap_width
);
int? retVal = null; int? retVal = null;
if (utf8TextBytes <= ret && ret <= utf8TextBytes + utf8TextByteCount) if (utf8TextBytes <= ret && ret <= utf8TextBytes + utf8TextByteCount)
@@ -217,10 +281,12 @@ internal static unsafe class ImGuiExtras
return retVal; return retVal;
} }
public static unsafe bool SetDragDropPayload<T>(string type, T data) where T : unmanaged => public static unsafe bool SetDragDropPayload<T>(string type, T data)
where T : unmanaged =>
ImGui.SetDragDropPayload(type, MemoryMarshal.AsBytes(new ReadOnlySpan<T>(&data, 1))); ImGui.SetDragDropPayload(type, MemoryMarshal.AsBytes(new ReadOnlySpan<T>(&data, 1)));
public static unsafe bool AcceptDragDropPayload<T>(string type, out T data) where T : unmanaged public static unsafe bool AcceptDragDropPayload<T>(string type, out T data)
where T : unmanaged
{ {
var payload = ImGui.AcceptDragDropPayload(type); var payload = ImGui.AcceptDragDropPayload(type);
if (payload.IsNull || payload.DataSize != sizeof(T)) if (payload.IsNull || payload.DataSize != sizeof(T))
+262 -77
View File
@@ -1,11 +1,3 @@
using Craftimizer.Utils;
using Dalamud.Interface;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImPlot;
using MathNet.Numerics.Statistics;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
@@ -15,12 +7,24 @@ using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Craftimizer.Utils;
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImPlot;
using Dalamud.Interface;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using MathNet.Numerics.Statistics;
namespace Craftimizer.Plugin; namespace Craftimizer.Plugin;
internal static class ImGuiUtils internal static class ImGuiUtils
{ {
private static readonly Stack<(Vector2 Min, Vector2 Max, float TopPadding)> GroupPanelLabelStack = new(); private static readonly Stack<(
Vector2 Min,
Vector2 Max,
float TopPadding
)> GroupPanelLabelStack = new();
// Adapted from https://github.com/ocornut/imgui/issues/1496#issuecomment-655048353 // Adapted from https://github.com/ocornut/imgui/issues/1496#issuecomment-655048353
// width = -1 -> size to parent // width = -1 -> size to parent
@@ -62,7 +66,9 @@ internal static class ImGuiUtils
var textFrameHeight = ImGui.GetFrameHeight(); var textFrameHeight = ImGui.GetFrameHeight();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(name); ImGui.TextUnformatted(name);
GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), textFrameHeight / 2f)); // push rect to stack GroupPanelLabelStack.Push(
(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), textFrameHeight / 2f)
); // push rect to stack
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
ImGui.Dummy(new Vector2(0f, textFrameHeight + itemSpacing.Y)); // shifts content by fh + is.y ImGui.Dummy(new Vector2(0f, textFrameHeight + itemSpacing.Y)); // shifts content by fh + is.y
} }
@@ -112,18 +118,29 @@ internal static class ImGuiUtils
{ {
var (minClip, maxClip) = i switch var (minClip, maxClip) = i switch
{ {
0 => (new Vector2(float.NegativeInfinity), new Vector2(labelMin.X, float.PositiveInfinity)), 0 => (
1 => (new Vector2(labelMax.X, float.NegativeInfinity), new Vector2(float.PositiveInfinity)), new Vector2(float.NegativeInfinity),
2 => (new Vector2(labelMin.X, float.NegativeInfinity), new Vector2(labelMax.X, labelMin.Y)), new Vector2(labelMin.X, float.PositiveInfinity)
3 => (new Vector2(labelMin.X, labelMax.Y), new Vector2(labelMax.X, float.PositiveInfinity)), ),
_ => (Vector2.Zero, Vector2.Zero) 1 => (
new Vector2(labelMax.X, float.NegativeInfinity),
new Vector2(float.PositiveInfinity)
),
2 => (
new Vector2(labelMin.X, float.NegativeInfinity),
new Vector2(labelMax.X, labelMin.Y)
),
3 => (
new Vector2(labelMin.X, labelMax.Y),
new Vector2(labelMax.X, float.PositiveInfinity)
),
_ => (Vector2.Zero, Vector2.Zero),
}; };
ImGui.PushClipRect(minClip, maxClip, true); ImGui.PushClipRect(minClip, maxClip, true);
ImGui.GetWindowDrawList().AddRect( ImGui
innerMin, innerMax, .GetWindowDrawList()
ImGui.GetColorU32(ImGuiCol.Border), .AddRect(innerMin, innerMax, ImGui.GetColorU32(ImGuiCol.Border), itemSpacing.X);
itemSpacing.X);
ImGui.PopClipRect(); ImGui.PopClipRect();
} }
@@ -143,15 +160,23 @@ internal static class ImGuiUtils
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static float Lerp(float a, float b, float t) => private static float Lerp(float a, float b, float t) => MathF.FusedMultiplyAdd(b - a, t, a);
MathF.FusedMultiplyAdd(b - a, t, a);
private readonly record struct ArcEdge(float Angle, Vector2 Point) private readonly record struct ArcEdge(float Angle, Vector2 Point)
{ {
public ArcEdge(float angle) : this(angle, UnitCircle(angle)) { } public ArcEdge(float angle)
: this(angle, UnitCircle(angle)) { }
} }
private static void ArcSegment(Vector2 o, ArcEdge prev, ArcEdge cur, ArcEdge? next, float radius, float ratio, uint color) private static void ArcSegment(
Vector2 o,
ArcEdge prev,
ArcEdge cur,
ArcEdge? next,
float radius,
float ratio,
uint color
)
{ {
var d = ImGui.GetWindowDrawList(); var d = ImGui.GetWindowDrawList();
@@ -164,7 +189,15 @@ internal static class ImGuiUtils
d.PathFillConvex(color); d.PathFillConvex(color);
} }
public static void Arc(float startAngle, float endAngle, float radius, float ratio, uint backgroundColor, uint filledColor, bool addDummy = true) public static void Arc(
float startAngle,
float endAngle,
float radius,
float ratio,
uint backgroundColor,
uint filledColor,
bool addDummy = true
)
{ {
// Fix normals when drawing (for antialiasing) // Fix normals when drawing (for antialiasing)
if (startAngle > endAngle) if (startAngle > endAngle)
@@ -224,9 +257,16 @@ internal static class ImGuiUtils
ImGui.Dummy(new Vector2(radius * 2)); ImGui.Dummy(new Vector2(radius * 2));
} }
public static void ArcProgress(float value, float radius, float ratio, uint backgroundColor, uint filledColor) public static void ArcProgress(
float value,
float radius,
float ratio,
uint backgroundColor,
uint filledColor
)
{ {
float startAngle, endAngle; float startAngle,
endAngle;
// https://github.com/ocornut/imgui/commit/c895e987adf746a997b655c64a6a8916c549ff6f#diff-d750e175eb584ba76bc560b8e54cf113ccbb31dd33f75078c1588925e197a3afR1304-R1310 // https://github.com/ocornut/imgui/commit/c895e987adf746a997b655c64a6a8916c549ff6f#diff-d750e175eb584ba76bc560b8e54cf113ccbb31dd33f75078c1588925e197a3afR1304-R1310
if (value < 0) if (value < 0)
@@ -271,19 +311,34 @@ internal static class ImGuiUtils
bar_end = bar_begin + bar_fraction; bar_end = bar_begin + bar_fraction;
} }
ImGuiExtras.RenderFrame(bbMin, bbMax, ImGui.GetColorU32(ImGuiCol.FrameBg), true, style.FrameRounding); ImGuiExtras.RenderFrame(
bbMin,
bbMax,
ImGui.GetColorU32(ImGuiCol.FrameBg),
true,
style.FrameRounding
);
bbMin += new Vector2(style.FrameBorderSize); bbMin += new Vector2(style.FrameBorderSize);
bbMax -= new Vector2(style.FrameBorderSize); bbMax -= new Vector2(style.FrameBorderSize);
ImGuiExtras.RenderRectFilledRangeH(ImGui.GetWindowDrawList(), new(bbMin.X, bbMin.Y, bbMax.X, bbMax.Y), ImGui.GetColorU32(ImGuiCol.PlotHistogram), bar_begin, bar_end, style.FrameRounding); ImGuiExtras.RenderRectFilledRangeH(
ImGui.GetWindowDrawList(),
new(bbMin.X, bbMin.Y, bbMax.X, bbMax.Y),
ImGui.GetColorU32(ImGuiCol.PlotHistogram),
bar_begin,
bar_end,
style.FrameRounding
);
} }
public sealed class ViolinData public sealed class ViolinData
{ {
public struct Point(float x, float y, float y2) public struct Point(float x, float y, float y2)
{ {
public float X = x, Y = y, Y2 = y2; public float X = x,
Y = y,
Y2 = y2;
} }
public ReadOnlySpan<Point> Data => (DataArray ?? []).AsSpan(); public ReadOnlySpan<Point> Data => (DataArray ?? []).AsSpan();
@@ -291,7 +346,13 @@ internal static class ImGuiUtils
public readonly float Min; public readonly float Min;
public readonly float Max; public readonly float Max;
public ViolinData(IEnumerable<int> samples, float min, float max, int resolution, double bandwidth) public ViolinData(
IEnumerable<int> samples,
float min,
float max,
int resolution,
double bandwidth
)
{ {
Min = min; Min = min;
Max = max; Max = max;
@@ -300,9 +361,12 @@ internal static class ImGuiUtils
_ = Task.Run(() => _ = Task.Run(() =>
{ {
var s = Stopwatch.StartNew(); var s = Stopwatch.StartNew();
var data = ParallelEnumerable.Range(0, resolution + 1) var data = ParallelEnumerable
.Range(0, resolution + 1)
.Select(n => Lerp(min, max, n / (float)resolution)) .Select(n => Lerp(min, max, n / (float)resolution))
.Select(n => (n, (float)KernelDensity.EstimateGaussian(n, bandwidth, samplesList))) .Select(n =>
(n, (float)KernelDensity.EstimateGaussian(n, bandwidth, samplesList))
)
.Select(n => new Point(n.n, n.Item2, -n.Item2)); .Select(n => new Point(n.n, n.Item2, -n.Item2));
// ParallelQuery doesn't support [.. data] correctly. The plots look very wrong. // ParallelQuery doesn't support [.. data] correctly. The plots look very wrong.
#pragma warning disable IDE0305 // Simplify collection initialization #pragma warning disable IDE0305 // Simplify collection initialization
@@ -320,10 +384,22 @@ internal static class ImGuiUtils
using var plotBg = ImRaii2.PushColor(ImPlotCol.Bg, Vector4.Zero); using var plotBg = ImRaii2.PushColor(ImPlotCol.Bg, Vector4.Zero);
using var fill = ImRaii2.PushColor(ImPlotCol.Fill, Vector4.One.WithAlpha(.5f)); using var fill = ImRaii2.PushColor(ImPlotCol.Fill, Vector4.One.WithAlpha(.5f));
using var plot = ImRaii2.Plot("##violin", size, ImPlotFlags.CanvasOnly | ImPlotFlags.NoInputs | ImPlotFlags.NoChild | ImPlotFlags.NoFrame); using var plot = ImRaii2.Plot(
if (plot) "##violin",
size,
ImPlotFlags.CanvasOnly
| ImPlotFlags.NoInputs
| ImPlotFlags.NoChild
| ImPlotFlags.NoFrame
);
if (plot.Success)
{ {
ImPlot.SetupAxes([], [], ImPlotAxisFlags.NoDecorations, ImPlotAxisFlags.NoDecorations | ImPlotAxisFlags.AutoFit); ImPlot.SetupAxes(
[],
[],
ImPlotAxisFlags.NoDecorations,
ImPlotAxisFlags.NoDecorations | ImPlotAxisFlags.AutoFit
);
ImPlot.SetupAxisLimits(ImAxis.X1, data.Min, data.Max, ImPlotCond.Always); ImPlot.SetupAxisLimits(ImAxis.X1, data.Min, data.Max, ImPlotCond.Always);
ImPlot.SetupFinish(); ImPlot.SetupFinish();
@@ -334,14 +410,24 @@ internal static class ImGuiUtils
var label_id = stackalloc byte[] { (byte)'\0' }; var label_id = stackalloc byte[] { (byte)'\0' };
fixed (ViolinData.Point* p = points) fixed (ViolinData.Point* p = points)
{ {
ImPlot.PlotShaded(label_id, &p->X, &p->Y, &p->Y2, points.Length, ImPlotShadedFlags.None, 0, sizeof(ViolinData.Point)); ImPlot.PlotShaded(
label_id,
&p->X,
&p->Y,
&p->Y2,
points.Length,
ImPlotShadedFlags.None,
0,
sizeof(ViolinData.Point)
);
} }
} }
} }
} }
} }
private sealed class SearchableComboData<T> where T : IEquatable<T> private sealed class SearchableComboData<T>
where T : IEquatable<T>
{ {
public readonly ImmutableArray<T> items; public readonly ImmutableArray<T> items;
public List<T> filteredItems; public List<T> filteredItems;
@@ -381,20 +467,26 @@ internal static class ImGuiUtils
cts = new(); cts = new();
var token = cts.Token; var token = cts.Token;
task = Task.Run(() => FilterTask(inp, token), token) task = Task.Run(() => FilterTask(inp, token), token)
.ContinueWith(t => .ContinueWith(
{ t =>
if (cts.IsCancellationRequested) {
return; if (cts.IsCancellationRequested)
return;
try try
{ {
t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException); t.Exception!.Flatten()
} .Handle(ex =>
catch (Exception e) ex is TaskCanceledException or OperationCanceledException
{ );
Log.Error(e, "Filtering recipes failed"); }
} catch (Exception e)
}, TaskContinuationOptions.OnlyOnFaulted); {
Log.Error(e, "Filtering recipes failed");
}
},
TaskContinuationOptions.OnlyOnFaulted
);
} }
private void FilterTask(string input, CancellationToken token) private void FilterTask(string input, CancellationToken token)
@@ -405,7 +497,9 @@ internal static class ImGuiUtils
return; return;
} }
var matcher = new FuzzyMatcher(input.ToLowerInvariant(), MatchMode.FuzzyParts); var matcher = new FuzzyMatcher(input.ToLowerInvariant(), MatchMode.FuzzyParts);
var query = items.AsParallel().Select(i => (Item: i, Score: matcher.Matches(getString(i).ToLowerInvariant()))) var query = items
.AsParallel()
.Select(i => (Item: i, Score: matcher.Matches(getString(i).ToLowerInvariant())))
.Where(t => t.Score > 0) .Where(t => t.Score > 0)
.OrderByDescending(t => t.Score) .OrderByDescending(t => t.Score)
.Select(t => t.Item); .Select(t => t.Item);
@@ -413,16 +507,34 @@ internal static class ImGuiUtils
filteredItems = [.. query]; filteredItems = [.. query];
} }
} }
private static readonly Dictionary<uint, object> ComboData = []; private static readonly Dictionary<uint, object> ComboData = [];
private static SearchableComboData<T> GetComboData<T>(uint comboKey, IEnumerable<T> items, T selectedItem, Func<T, string> getString) where T : IEquatable<T> => private static SearchableComboData<T> GetComboData<T>(
uint comboKey,
IEnumerable<T> items,
T selectedItem,
Func<T, string> getString
)
where T : IEquatable<T> =>
(SearchableComboData<T>)( (SearchableComboData<T>)(
ComboData.TryGetValue(comboKey, out var data) ComboData.TryGetValue(comboKey, out var data)
? data ? data
: ComboData[comboKey] = new SearchableComboData<T>(items, selectedItem, getString)); : ComboData[comboKey] = new SearchableComboData<T>(items, selectedItem, getString)
);
// https://github.com/ocornut/imgui/issues/718#issuecomment-1563162222 // https://github.com/ocornut/imgui/issues/718#issuecomment-1563162222
public static bool SearchableCombo<T>(string id, ref T selectedItem, IEnumerable<T> items, ImFontPtr selectableFont, float width, Func<T, string> getString, Func<T, string> getId, Action<T> draw) where T : IEquatable<T> public static bool SearchableCombo<T>(
string id,
ref T selectedItem,
IEnumerable<T> items,
ImFontPtr selectableFont,
float width,
Func<T, string> getString,
Func<T, string> getId,
Action<T> draw
)
where T : IEquatable<T>
{ {
var comboKey = ImGui.GetID(id); var comboKey = ImGui.GetID(id);
var data = GetComboData(comboKey, items, selectedItem, getString); var data = GetComboData(comboKey, items, selectedItem, getString);
@@ -433,7 +545,12 @@ internal static class ImGuiUtils
width = width == 0 ? ImGui.GetContentRegionAvail().X : width; width = width == 0 ? ImGui.GetContentRegionAvail().X : width;
var availableSpace = Math.Min(ImGui.GetContentRegionAvail().X, width); var availableSpace = Math.Min(ImGui.GetContentRegionAvail().X, width);
ImGui.SetNextItemWidth(availableSpace); ImGui.SetNextItemWidth(availableSpace);
var isInputTextEnterPressed = ImGui.InputText("##input", ref data.input, 256, ImGuiInputTextFlags.EnterReturnsTrue); var isInputTextEnterPressed = ImGui.InputText(
"##input",
ref data.input,
256,
ImGuiInputTextFlags.EnterReturnsTrue
);
var min = ImGui.GetItemRectMin(); var min = ImGui.GetItemRectMin();
var size = ImGui.GetItemRectSize(); var size = ImGui.GetItemRectSize();
size.X = Math.Min(size.X, availableSpace); size.X = Math.Min(size.X, availableSpace);
@@ -447,7 +564,15 @@ internal static class ImGuiUtils
data.wasTextActive = false; data.wasTextActive = false;
} }
using (var popup = ImRaii.Popup("##popup", ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoSavedSettings)) using (
var popup = ImRaii.Popup(
"##popup",
ImGuiWindowFlags.NoMove
| ImGuiWindowFlags.NoResize
| ImGuiWindowFlags.AlwaysAutoResize
| ImGuiWindowFlags.NoSavedSettings
)
)
{ {
if (popup) if (popup)
{ {
@@ -466,25 +591,36 @@ internal static class ImGuiUtils
isInputTextEnterPressed = true; isInputTextEnterPressed = true;
data.wasTextActive = isActive; data.wasTextActive = isActive;
using (var scrollingRegion = ImRaii.Child("scrollingRegion", new Vector2(size.X, size.Y * 10), false, ImGuiWindowFlags.HorizontalScrollbar)) using (
var scrollingRegion = ImRaii.Child(
"scrollingRegion",
new Vector2(size.X, size.Y * 10),
false,
ImGuiWindowFlags.HorizontalScrollbar
)
)
{ {
T? _selectedItem = default; T? _selectedItem = default;
var height = ImGui.GetTextLineHeight(); var height = ImGui.GetTextLineHeight();
var r = ListClip(data.filteredItems, height, t => var r = ListClip(
{ data.filteredItems,
var name = getString(t); height,
using (var selectFont = ImRaii.PushFont(selectableFont)) t =>
{ {
if (ImGui.Selectable($"##{getId(t)}")) var name = getString(t);
using (var selectFont = ImRaii.PushFont(selectableFont))
{ {
_selectedItem = t; if (ImGui.Selectable($"##{getId(t)}"))
return true; {
_selectedItem = t;
return true;
}
} }
ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X / 2f);
draw(t);
return false;
} }
ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X / 2f); );
draw(t);
return false;
});
if (r) if (r)
{ {
selectedItem = _selectedItem!; selectedItem = _selectedItem!;
@@ -530,7 +666,11 @@ internal static class ImGuiUtils
imGuiListClipperPtr.Begin(data.Count, lineHeight); imGuiListClipperPtr.Begin(data.Count, lineHeight);
while (imGuiListClipperPtr.Step()) while (imGuiListClipperPtr.Step())
{ {
for (var i = imGuiListClipperPtr.DisplayStart; i <= imGuiListClipperPtr.DisplayEnd; i++) for (
var i = imGuiListClipperPtr.DisplayStart;
i <= imGuiListClipperPtr.DisplayEnd;
i++
)
{ {
if (i >= data.Count) if (i >= data.Count)
return false; return false;
@@ -551,10 +691,28 @@ internal static class ImGuiUtils
} }
} }
public static bool InputTextMultilineWithHint(string label, string hint, ref string input, int maxLength, Vector2 size, ImGuiInputTextFlags flags = ImGuiInputTextFlags.None, ImGuiInputTextCallback? callback = null, IntPtr user_data = default) public static bool InputTextMultilineWithHint(
string label,
string hint,
ref string input,
int maxLength,
Vector2 size,
ImGuiInputTextFlags flags = ImGuiInputTextFlags.None,
ImGuiInputTextCallback? callback = null,
IntPtr user_data = default
)
{ {
const ImGuiInputTextFlags Multiline = (ImGuiInputTextFlags)(1 << 26); const ImGuiInputTextFlags Multiline = (ImGuiInputTextFlags)(1 << 26);
return ImGuiExtras.InputTextEx(label, hint, ref input, maxLength, size, flags | Multiline, callback, user_data); return ImGuiExtras.InputTextEx(
label,
hint,
ref input,
maxLength,
size,
flags | Multiline,
callback,
user_data
);
} }
private static Vector2 GetIconSize(FontAwesomeIcon icon) private static Vector2 GetIconSize(FontAwesomeIcon icon)
@@ -563,7 +721,12 @@ internal static class ImGuiUtils
return ImGui.CalcTextSize(icon.ToIconString()); return ImGui.CalcTextSize(icon.ToIconString());
} }
private static void DrawCenteredIcon(FontAwesomeIcon icon, Vector2 offset, Vector2 size, bool isDisabled = false) private static void DrawCenteredIcon(
FontAwesomeIcon icon,
Vector2 offset,
Vector2 size,
bool isDisabled = false
)
{ {
var iconSize = GetIconSize(icon); var iconSize = GetIconSize(icon);
@@ -585,7 +748,15 @@ internal static class ImGuiUtils
iconOffset = Vector2.Zero; iconOffset = Vector2.Zero;
} }
ImGui.GetWindowDrawList().AddText(UiBuilder.IconFont, UiBuilder.IconFont.FontSize * ImGuiHelpers.GlobalScale * scale, offset + iconOffset, ImGui.GetColorU32(!isDisabled ? ImGuiCol.Text : ImGuiCol.TextDisabled), icon.ToIconString()); ImGui
.GetWindowDrawList()
.AddText(
UiBuilder.IconFont,
UiBuilder.IconFont.FontSize * ImGuiHelpers.GlobalScale * scale,
offset + iconOffset,
ImGui.GetColorU32(!isDisabled ? ImGuiCol.Text : ImGuiCol.TextDisabled),
icon.ToIconString()
);
} }
public static bool IconButtonSquare(FontAwesomeIcon icon, float size = -1) public static bool IconButtonSquare(FontAwesomeIcon icon, float size = -1)
@@ -627,7 +798,13 @@ internal static class ImGuiUtils
Dalamud.Utility.Util.OpenLink(url); Dalamud.Utility.Util.OpenLink(url);
var urlWithoutScheme = url; var urlWithoutScheme = url;
if (Uri.TryCreate(url, UriKind.Absolute, out var uri)) if (Uri.TryCreate(url, UriKind.Absolute, out var uri))
urlWithoutScheme = uri.Host + (string.Equals(uri.PathAndQuery, "/", StringComparison.Ordinal) ? string.Empty : uri.PathAndQuery); urlWithoutScheme =
uri.Host
+ (
string.Equals(uri.PathAndQuery, "/", StringComparison.Ordinal)
? string.Empty
: uri.PathAndQuery
);
Tooltip(urlWithoutScheme); Tooltip(urlWithoutScheme);
} }
} }
@@ -647,7 +824,11 @@ internal static class ImGuiUtils
ImGui.TextUnformatted(text); ImGui.TextUnformatted(text);
} }
public static void TextWrappedTo(string text, float wrapPosX = default, float basePosX = default) public static void TextWrappedTo(
string text,
float wrapPosX = default,
float basePosX = default
)
{ {
var font = ImGui.GetFont(); var font = ImGui.GetFont();
@@ -663,7 +844,11 @@ internal static class ImGuiUtils
currentWrapWidth = wrapPosX - currentPos; currentWrapWidth = wrapPosX - currentPos;
var textBuf = text.AsSpan(); var textBuf = text.AsSpan();
var lineSize = font.CalcWordWrapPositionA(ImGuiHelpers.GlobalScale, textBuf, currentWrapWidth); var lineSize = font.CalcWordWrapPositionA(
ImGuiHelpers.GlobalScale,
textBuf,
currentWrapWidth
);
if (lineSize == 0) if (lineSize == 0)
lineSize = textBuf.Length; lineSize = textBuf.Length;
var lineBuf = textBuf[..lineSize]; var lineBuf = textBuf[..lineSize];
+20 -12
View File
@@ -1,14 +1,19 @@
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImPlot;
using System; using System;
using System.Numerics; using System.Numerics;
using Dalamud.Bindings.ImGui;
using Dalamud.Bindings.ImPlot;
using Dalamud.Interface.Utility.Raii;
namespace Craftimizer.Plugin; namespace Craftimizer.Plugin;
public interface IEndObject : IDisposable
{
bool Success { get; }
}
public static class ImRaii2 public static class ImRaii2
{ {
private struct EndUnconditionally(Action endAction, bool success) : ImRaii.IEndObject, IDisposable private struct EndUnconditionally(Action endAction, bool success) : IEndObject
{ {
private Action EndAction { get; } = endAction; private Action EndAction { get; } = endAction;
@@ -26,7 +31,7 @@ public static class ImRaii2
} }
} }
private struct EndConditionally(Action endAction, bool success) : ImRaii.IEndObject, IDisposable private struct EndConditionally(Action endAction, bool success) : IEndObject
{ {
public bool Success { get; } = success; public bool Success { get; } = success;
@@ -48,36 +53,39 @@ public static class ImRaii2
} }
} }
public static ImRaii.IEndObject GroupPanel(string name, float width, out float internalWidth) public static IEndObject GroupPanel(string name, float width, out float internalWidth)
{ {
internalWidth = ImGuiUtils.BeginGroupPanel(name, width); internalWidth = ImGuiUtils.BeginGroupPanel(name, width);
return new EndUnconditionally(ImGuiUtils.EndGroupPanel, true); return new EndUnconditionally(ImGuiUtils.EndGroupPanel, true);
} }
public static ImRaii.IEndObject Plot(string title_id, Vector2 size, ImPlotFlags flags) public static IEndObject Plot(string title_id, Vector2 size, ImPlotFlags flags)
{ {
return new EndConditionally(new Action(ImPlot.EndPlot), ImPlot.BeginPlot(title_id, size, flags)); return new EndConditionally(
new Action(ImPlot.EndPlot),
ImPlot.BeginPlot(title_id, size, flags)
);
} }
public static ImRaii.IEndObject PushStyle(ImPlotStyleVar idx, Vector2 val) public static IEndObject PushStyle(ImPlotStyleVar idx, Vector2 val)
{ {
ImPlot.PushStyleVar(idx, val); ImPlot.PushStyleVar(idx, val);
return new EndUnconditionally(ImPlot.PopStyleVar, true); return new EndUnconditionally(ImPlot.PopStyleVar, true);
} }
public static ImRaii.IEndObject PushStyle(ImPlotStyleVar idx, float val) public static IEndObject PushStyle(ImPlotStyleVar idx, float val)
{ {
ImPlot.PushStyleVar(idx, val); ImPlot.PushStyleVar(idx, val);
return new EndUnconditionally(ImPlot.PopStyleVar, true); return new EndUnconditionally(ImPlot.PopStyleVar, true);
} }
public static ImRaii.IEndObject PushColor(ImPlotCol idx, Vector4 col) public static IEndObject PushColor(ImPlotCol idx, Vector4 col)
{ {
ImPlot.PushStyleColor(idx, col); ImPlot.PushStyleColor(idx, col);
return new EndUnconditionally(ImPlot.PopStyleColor, true); return new EndUnconditionally(ImPlot.PopStyleColor, true);
} }
public static ImRaii.IEndObject TextWrapPos(float wrap_local_pos_x) public static IEndObject TextWrapPos(float wrap_local_pos_x)
{ {
ImGui.PushTextWrapPos(wrap_local_pos_x); ImGui.PushTextWrapPos(wrap_local_pos_x);
return new EndUnconditionally(ImGui.PopTextWrapPos, true); return new EndUnconditionally(ImGui.PopTextWrapPos, true);
+11 -5
View File
@@ -10,18 +10,24 @@ public static class LuminaSheets
public static readonly ExcelSheet<Recipe> RecipeSheet = Module.GetSheet<Recipe>(); public static readonly ExcelSheet<Recipe> RecipeSheet = Module.GetSheet<Recipe>();
public static readonly ExcelSheet<Action> ActionSheet = Module.GetSheet<Action>(); public static readonly ExcelSheet<Action> ActionSheet = Module.GetSheet<Action>();
public static readonly ExcelSheet<CraftAction> CraftActionSheet = Module.GetSheet<CraftAction>(); public static readonly ExcelSheet<CraftAction> CraftActionSheet =
Module.GetSheet<CraftAction>();
public static readonly ExcelSheet<Status> StatusSheet = Module.GetSheet<Status>(); public static readonly ExcelSheet<Status> StatusSheet = Module.GetSheet<Status>();
public static readonly ExcelSheet<Addon> AddonSheet = Module.GetSheet<Addon>(); public static readonly ExcelSheet<Addon> AddonSheet = Module.GetSheet<Addon>();
public static readonly ExcelSheet<ClassJob> ClassJobSheet = Module.GetSheet<ClassJob>(); public static readonly ExcelSheet<ClassJob> ClassJobSheet = Module.GetSheet<ClassJob>();
public static readonly ExcelSheet<Item> ItemSheet = Module.GetSheet<Item>(); public static readonly ExcelSheet<Item> ItemSheet = Module.GetSheet<Item>();
public static readonly ExcelSheet<Item> ItemSheetEnglish = Module.GetSheet<Item>(Language.English)!; public static readonly ExcelSheet<Item> ItemSheetEnglish = Module.GetSheet<Item>(
Language.English
)!;
public static readonly ExcelSheet<Level> LevelSheet = Module.GetSheet<Level>(); public static readonly ExcelSheet<Level> LevelSheet = Module.GetSheet<Level>();
public static readonly ExcelSheet<Quest> QuestSheet = Module.GetSheet<Quest>(); public static readonly ExcelSheet<Quest> QuestSheet = Module.GetSheet<Quest>();
public static readonly ExcelSheet<Materia> MateriaSheet = Module.GetSheet<Materia>(); public static readonly ExcelSheet<Materia> MateriaSheet = Module.GetSheet<Materia>();
public static readonly ExcelSheet<BaseParam> BaseParamSheet = Module.GetSheet<BaseParam>(); public static readonly ExcelSheet<BaseParam> BaseParamSheet = Module.GetSheet<BaseParam>();
public static readonly ExcelSheet<ItemFood> ItemFoodSheet = Module.GetSheet<ItemFood>(); public static readonly ExcelSheet<ItemFood> ItemFoodSheet = Module.GetSheet<ItemFood>();
public static readonly ExcelSheet<WKSMissionToDoEvalutionRefin> WKSMissionToDoEvalutionRefinSheet = Module.GetSheet<WKSMissionToDoEvalutionRefin>(); public static readonly ExcelSheet<WKSMissionToDoEvalutionRefin> WKSMissionToDoEvalutionRefinSheet =
public static readonly ExcelSheet<RecipeLevelTable> RecipeLevelTableSheet = Module.GetSheet<RecipeLevelTable>(); Module.GetSheet<WKSMissionToDoEvalutionRefin>();
public static readonly ExcelSheet<GathererCrafterLvAdjustTable> GathererCrafterLvAdjustTableSheet = Module.GetSheet<GathererCrafterLvAdjustTable>(); public static readonly ExcelSheet<RecipeLevelTable> RecipeLevelTableSheet =
Module.GetSheet<RecipeLevelTable>();
public static readonly ExcelSheet<GathererCrafterLvAdjustTable> GathererCrafterLvAdjustTableSheet =
Module.GetSheet<GathererCrafterLvAdjustTable>();
} }
+87 -41
View File
@@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Craftimizer.Plugin.Windows; using Craftimizer.Plugin.Windows;
using Craftimizer.Simulator; using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions; using Craftimizer.Simulator.Actions;
@@ -6,9 +9,6 @@ using Craftimizer.Windows;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Plugin; using Dalamud.Plugin;
using System;
using System.Collections.Generic;
using System.Reflection;
namespace Craftimizer.Plugin; namespace Craftimizer.Plugin;
@@ -37,9 +37,11 @@ public sealed class Plugin : IDalamudPlugin
public Plugin(IDalamudPluginInterface pluginInterface) public Plugin(IDalamudPluginInterface pluginInterface)
{ {
CraftimizerConflictDetector.ThrowIfUpstreamLoaded(pluginInterface);
Service.Initialize(this, pluginInterface); Service.Initialize(this, pluginInterface);
WindowSystem = new("Craftimizer"); WindowSystem = new("Forgeimizer");
Configuration = Configuration.Load(); Configuration = Configuration.Load();
IconManager = new(); IconManager = new();
Hooks = new(); Hooks = new();
@@ -48,9 +50,13 @@ public sealed class Plugin : IDalamudPlugin
AttributeCommandManager = new(); AttributeCommandManager = new();
var assembly = Assembly.GetExecutingAssembly(); var assembly = Assembly.GetExecutingAssembly();
Version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion.Split('+')[0]; Version = assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!
.InformationalVersion.Split('+')[0];
Author = assembly.GetCustomAttribute<AssemblyCompanyAttribute>()!.Company; Author = assembly.GetCustomAttribute<AssemblyCompanyAttribute>()!.Company;
BuildConfiguration = assembly.GetCustomAttribute<AssemblyConfigurationAttribute>()!.Configuration; BuildConfiguration = assembly
.GetCustomAttribute<AssemblyConfigurationAttribute>()!
.Configuration;
if (DateTime.Now is { Day: 1, Month: 4 }) if (DateTime.Now is { Day: 1, Month: 4 })
Icon = IconManager.GetAssemblyTexture("Graphics.horse_icon.png"); Icon = IconManager.GetAssemblyTexture("Graphics.horse_icon.png");
else else
@@ -70,59 +76,93 @@ public sealed class Plugin : IDalamudPlugin
Service.PluginInterface.UiBuilder.OpenMainUi += OpenCraftingLog; Service.PluginInterface.UiBuilder.OpenMainUi += OpenCraftingLog;
} }
public (CharacterStats? Character, RecipeData? Recipe, MacroEditor.CrafterBuffs? Buffs) GetOpenedStats() public (
CharacterStats? Character,
RecipeData? Recipe,
MacroEditor.CrafterBuffs? Buffs
) GetOpenedStats()
{ {
var editorWindow = (EditorWindow?.IsOpen ?? false) ? EditorWindow : null; var editorWindow = (EditorWindow?.IsOpen ?? false) ? EditorWindow : null;
var recipeData = editorWindow?.RecipeData ?? Service.Plugin.RecipeNoteWindow.RecipeData; var recipeData = editorWindow?.RecipeData ?? Service.Plugin.RecipeNoteWindow.RecipeData;
var characterStats = editorWindow?.CharacterStats ?? Service.Plugin.RecipeNoteWindow.CharacterStats; var characterStats =
var buffs = editorWindow?.Buffs ?? (RecipeNoteWindow.CharacterStats != null ? new(Service.ClientState.LocalPlayer?.StatusList) : null); editorWindow?.CharacterStats ?? Service.Plugin.RecipeNoteWindow.CharacterStats;
var buffs =
editorWindow?.Buffs
?? (
RecipeNoteWindow.CharacterStats != null
? new(Service.Objects.LocalPlayer?.StatusList)
: null
);
return (characterStats, recipeData, buffs); return (characterStats, recipeData, buffs);
} }
public (CharacterStats Character, RecipeData Recipe, MacroEditor.CrafterBuffs Buffs) GetDefaultStats() public (
CharacterStats Character,
RecipeData Recipe,
MacroEditor.CrafterBuffs Buffs
) GetDefaultStats()
{ {
var stats = GetOpenedStats(); var stats = GetOpenedStats();
return ( return (
stats.Character ?? new() stats.Character
{ ?? new()
Craftsmanship = 100, {
Control = 100, Craftsmanship = 100,
CP = 200, Control = 100,
Level = 10, CP = 200,
CanUseManipulation = false, Level = 10,
HasSplendorousBuff = false, CanUseManipulation = false,
IsSpecialist = false, HasSplendorousBuff = false,
}, IsSpecialist = false,
},
stats.Recipe ?? new(1023), stats.Recipe ?? new(1023),
stats.Buffs ?? new(null) stats.Buffs ?? new(null)
); );
} }
[Command(name: "/crafteditor", aliases: "/macroeditor", description: "Open the crafting macro editor.")] [Command(
name: "/crafteditor",
aliases: "/macroeditor",
description: "Open the crafting macro editor."
)]
public void OpenEmptyMacroEditor() public void OpenEmptyMacroEditor()
{ {
var stats = GetDefaultStats(); var stats = GetDefaultStats();
OpenMacroEditor(stats.Character, stats.Recipe, stats.Buffs, null, [], null); OpenMacroEditor(stats.Character, stats.Recipe, stats.Buffs, null, [], null);
} }
public void OpenMacroEditor(CharacterStats characterStats, RecipeData recipeData, MacroEditor.CrafterBuffs buffs, IEnumerable<int>? ingredientHqCounts, IEnumerable<ActionType> actions, Action<IEnumerable<ActionType>>? setter) public void OpenMacroEditor(
CharacterStats characterStats,
RecipeData recipeData,
MacroEditor.CrafterBuffs buffs,
IEnumerable<int>? ingredientHqCounts,
IEnumerable<ActionType> actions,
Action<IEnumerable<ActionType>>? setter
)
{ {
EditorWindow?.Dispose(); EditorWindow?.Dispose();
EditorWindow = new(characterStats, recipeData, buffs, ingredientHqCounts, actions, setter); EditorWindow = new(characterStats, recipeData, buffs, ingredientHqCounts, actions, setter);
} }
[Command(name: "/craftaction", description: "Execute the suggested action in the synthesis helper. Can also be run inside a macro. This command is useful for controller players.")] [Command(
public void ExecuteSuggestedSynthHelperAction() => name: "/craftaction",
SynthHelperWindow.ExecuteNextAction(); description: "Execute the suggested action in the synthesis helper. Can also be run inside a macro. This command is useful for controller players."
)]
public void ExecuteSuggestedSynthHelperAction() => SynthHelperWindow.ExecuteNextAction();
[Command(name: "/craftretry", description: "Clicks \"Retry\" in the synthesis helper. Can also be run inside a macro. This command is useful for controller players.")] [Command(
public void ExecuteRetrySynthHelper() => name: "/craftretry",
SynthHelperWindow.AttemptRetry(); description: "Clicks \"Retry\" in the synthesis helper. Can also be run inside a macro. This command is useful for controller players."
)]
public void ExecuteRetrySynthHelper() => SynthHelperWindow.AttemptRetry();
[Command(name: "/craftimizer", description: "Open the settings window.")] [Command(
private void OpenSettingsWindowForced() => name: "/craftimizer",
OpenSettingsWindow(true); aliases: "/forgeimizer",
description: "Open the settings window."
)]
private void OpenSettingsWindowForced() => OpenSettingsWindow(true);
public void OpenSettingsWindow(bool force = false) public void OpenSettingsWindow(bool force = false)
{ {
@@ -136,14 +176,18 @@ public sealed class Plugin : IDalamudPlugin
SettingsWindow.SelectTab(selectedTabLabel); SettingsWindow.SelectTab(selectedTabLabel);
} }
[Command(name: "/craftmacros", aliases: "/macrolist", description: "Open the crafting macros window.")] [Command(
name: "/craftmacros",
aliases: "/macrolist",
description: "Open the crafting macros window."
)]
public void OpenMacroListWindow() public void OpenMacroListWindow()
{ {
ListWindow.IsOpen = true; ListWindow.IsOpen = true;
ListWindow.BringToFront(); ListWindow.BringToFront();
} }
public void OpenCraftingLog() public static void OpenCraftingLog()
{ {
Chat.SendMessage("/craftinglog"); Chat.SendMessage("/craftinglog");
} }
@@ -154,15 +198,17 @@ public sealed class Plugin : IDalamudPlugin
ClipboardWindow = new(macros); ClipboardWindow = new(macros);
} }
public IActiveNotification DisplaySolverWarning(string text) => public static IActiveNotification DisplaySolverWarning(string text) =>
DisplayNotification(new() DisplayNotification(
{ new()
Content = text, {
Title = "Solver Warning", Content = text,
Type = NotificationType.Warning Title = "Solver Warning",
}); Type = NotificationType.Warning,
}
);
public IActiveNotification DisplayNotification(Notification notification) public static IActiveNotification DisplayNotification(Notification notification)
{ {
var ret = Service.NotificationManager.AddNotification(notification); var ret = Service.NotificationManager.AddNotification(notification);
// ret.SetIconTexture(Icon.RentAsync().ContinueWith(t => (IDalamudTextureWrap?)t)); // ret.SetIconTexture(Icon.RentAsync().ContinueWith(t => (IDalamudTextureWrap?)t));
+47 -16
View File
@@ -14,22 +14,53 @@ namespace Craftimizer.Plugin;
public sealed class Service public sealed class Service
{ {
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
[PluginService] public static IDalamudPluginInterface PluginInterface { get; private set; } [PluginService]
[PluginService] public static ICommandManager CommandManager { get; private set; } public static IDalamudPluginInterface PluginInterface { get; private set; }
[PluginService] public static IObjectTable Objects { get; private set; }
[PluginService] public static ISigScanner SigScanner { get; private set; } [PluginService]
[PluginService] public static IGameGui GameGui { get; private set; } public static ICommandManager CommandManager { get; private set; }
[PluginService] public static IClientState ClientState { get; private set; }
[PluginService] public static IDataManager DataManager { get; private set; } [PluginService]
[PluginService] public static ITextureProvider TextureProvider { get; private set; } public static IObjectTable Objects { get; private set; }
[PluginService] public static IDalamudAssetManager DalamudAssetManager { get; private set; }
[PluginService] public static ITargetManager TargetManager { get; private set; } [PluginService]
[PluginService] public static ICondition Condition { get; private set; } public static ISigScanner SigScanner { get; private set; }
[PluginService] public static IFramework Framework { get; private set; }
[PluginService] public static IPluginLog PluginLog { get; private set; } [PluginService]
[PluginService] public static IGameInteropProvider GameInteropProvider { get; private set; } public static IGameGui GameGui { get; private set; }
[PluginService] public static INotificationManager NotificationManager { get; private set; }
[PluginService] public static ISeStringEvaluator SeStringEvaluator { get; private set; } [PluginService]
public static IClientState ClientState { get; private set; }
[PluginService]
public static IDataManager DataManager { get; private set; }
[PluginService]
public static ITextureProvider TextureProvider { get; private set; }
[PluginService]
public static IDalamudAssetManager DalamudAssetManager { get; private set; }
[PluginService]
public static ITargetManager TargetManager { get; private set; }
[PluginService]
public static ICondition Condition { get; private set; }
[PluginService]
public static IFramework Framework { get; private set; }
[PluginService]
public static IPluginLog PluginLog { get; private set; }
[PluginService]
public static IGameInteropProvider GameInteropProvider { get; private set; }
[PluginService]
public static INotificationManager NotificationManager { get; private set; }
[PluginService]
public static ISeStringEvaluator SeStringEvaluator { get; private set; }
public static Plugin Plugin { get; private set; } public static Plugin Plugin { get; private set; }
public static Configuration Configuration => Plugin.Configuration; public static Configuration Configuration => Plugin.Configuration;
+94 -46
View File
@@ -1,21 +1,21 @@
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using System; using System;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using System.Text; using System.Text;
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Craftimizer.Utils;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Lumina.Excel.Sheets;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
using Action = Lumina.Excel.Sheets.Action; using Action = Lumina.Excel.Sheets.Action;
using ActionType = Craftimizer.Simulator.Actions.ActionType; using ActionType = Craftimizer.Simulator.Actions.ActionType;
using ClassJob = Craftimizer.Simulator.ClassJob; using ClassJob = Craftimizer.Simulator.ClassJob;
using Condition = Craftimizer.Simulator.Condition; using Condition = Craftimizer.Simulator.Condition;
using Status = Lumina.Excel.Sheets.Status; using Status = Lumina.Excel.Sheets.Status;
using Craftimizer.Utils;
using Lumina.Text.ReadOnly;
using Lumina.Text.Payloads;
using Lumina.Excel.Sheets;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Event;
namespace Craftimizer.Plugin; namespace Craftimizer.Plugin;
@@ -27,7 +27,10 @@ internal static class ActionUtils
{ {
var actionTypes = Enum.GetValues<ActionType>(); var actionTypes = Enum.GetValues<ActionType>();
var classJobs = Enum.GetValues<ClassJob>(); var classJobs = Enum.GetValues<ClassJob>();
ActionRows = new (CraftAction? CraftAction, Action? Action)[actionTypes.Length, classJobs.Length]; ActionRows = new (CraftAction? CraftAction, Action? Action)[
actionTypes.Length,
classJobs.Length
];
foreach (var actionType in actionTypes) foreach (var actionType in actionTypes)
{ {
var actionId = actionType.Base().ActionId; var actionId = actionType.Base().ActionId;
@@ -35,37 +38,48 @@ internal static class ActionUtils
{ {
foreach (var classJob in classJobs) foreach (var classJob in classJobs)
{ {
ActionRows[(int)actionType, (int)classJob] = (classJob switch ActionRows[(int)actionType, (int)classJob] = (
{ classJob switch
ClassJob.Carpenter => baseCraftAction.CRP.Value, {
ClassJob.Blacksmith => baseCraftAction.BSM.Value, ClassJob.Carpenter => baseCraftAction.CRP.Value,
ClassJob.Armorer => baseCraftAction.ARM.Value, ClassJob.Blacksmith => baseCraftAction.BSM.Value,
ClassJob.Goldsmith => baseCraftAction.GSM.Value, ClassJob.Armorer => baseCraftAction.ARM.Value,
ClassJob.Leatherworker => baseCraftAction.LTW.Value, ClassJob.Goldsmith => baseCraftAction.GSM.Value,
ClassJob.Weaver => baseCraftAction.WVR.Value, ClassJob.Leatherworker => baseCraftAction.LTW.Value,
ClassJob.Alchemist => baseCraftAction.ALC.Value, ClassJob.Weaver => baseCraftAction.WVR.Value,
ClassJob.Culinarian => baseCraftAction.CUL.Value, ClassJob.Alchemist => baseCraftAction.ALC.Value,
_ => baseCraftAction ClassJob.Culinarian => baseCraftAction.CUL.Value,
}, null); _ => baseCraftAction,
},
null
);
} }
} }
if (LuminaSheets.ActionSheet.GetRowOrDefault(actionId) is { } baseAction) if (LuminaSheets.ActionSheet.GetRowOrDefault(actionId) is { } baseAction)
{ {
var possibleActions = LuminaSheets.ActionSheet.Where(r => var possibleActions = LuminaSheets.ActionSheet.Where(r =>
r.Icon == baseAction.Icon && r.Icon == baseAction.Icon
r.ActionCategory.RowId == baseAction.ActionCategory.RowId && && r.ActionCategory.RowId == baseAction.ActionCategory.RowId
r.Name.Equals(baseAction.Name)); && r.Name.Equals(baseAction.Name)
);
foreach (var classJob in classJobs) foreach (var classJob in classJobs)
ActionRows[(int)actionType, (int)classJob] = (null, possibleActions.First(r => r.ClassJobCategory.ValueNullable?.IsClassJob(classJob) ?? false)); ActionRows[(int)actionType, (int)classJob] = (
null,
possibleActions.First(r =>
r.ClassJobCategory.ValueNullable?.IsClassJob(classJob) ?? false
)
);
} }
} }
} }
public static void Initialize() { } public static void Initialize() { }
public static (CraftAction? CraftAction, Action? Action) GetActionRow(this ActionType me, ClassJob classJob) => public static (CraftAction? CraftAction, Action? Action) GetActionRow(
ActionRows[(int)me, (int)classJob]; this ActionType me,
ClassJob classJob
) => ActionRows[(int)me, (int)classJob];
public static uint GetId(this ActionType me, ClassJob classJob) public static uint GetId(this ActionType me, ClassJob classJob)
{ {
@@ -86,7 +100,11 @@ internal static class ActionUtils
return Service.IconManager.GetIconCached(craftAction?.Icon ?? action?.Icon ?? 1953); return Service.IconManager.GetIconCached(craftAction?.Icon ?? action?.Icon ?? 1953);
} }
public static ActionType? GetActionTypeFromId(uint actionId, ClassJob classJob, bool isCraftAction) public static ActionType? GetActionTypeFromId(
uint actionId,
ClassJob classJob,
bool isCraftAction
)
{ {
foreach (var action in Enum.GetValues<ActionType>()) foreach (var action in Enum.GetValues<ActionType>())
{ {
@@ -119,7 +137,7 @@ internal static class ClassJobUtils
ClassJob.Weaver => 13, ClassJob.Weaver => 13,
ClassJob.Alchemist => 14, ClassJob.Alchemist => 14,
ClassJob.Culinarian => 15, ClassJob.Culinarian => 15,
_ => 0 _ => 0,
}; };
public static ClassJob? GetClassJobFromIdx(byte classJobIdx) => public static ClassJob? GetClassJobFromIdx(byte classJobIdx) =>
@@ -133,7 +151,7 @@ internal static class ClassJobUtils
13 => ClassJob.Weaver, 13 => ClassJob.Weaver,
14 => ClassJob.Alchemist, 14 => ClassJob.Alchemist,
15 => ClassJob.Culinarian, 15 => ClassJob.Culinarian,
_ => null _ => null,
}; };
public static sbyte GetExpArrayIdx(this ClassJob me) => public static sbyte GetExpArrayIdx(this ClassJob me) =>
@@ -158,7 +176,11 @@ internal static class ClassJobUtils
} }
public static unsafe bool CanPlayerUseManipulation(this ClassJob me) => public static unsafe bool CanPlayerUseManipulation(this ClassJob me) =>
UIState.Instance()->IsUnlockLinkUnlockedOrQuestCompleted(ActionType.Manipulation.GetActionRow(me).Action!.Value.UnlockLink.RowId); UIState
.Instance()
->IsUnlockLinkUnlockedOrQuestCompleted(
ActionType.Manipulation.GetActionRow(me).Action!.Value.UnlockLink.RowId
);
public static string GetName(this ClassJob me) public static string GetName(this ClassJob me)
{ {
@@ -186,8 +208,7 @@ internal static class ClassJobUtils
public static Quest GetUnlockQuest(this ClassJob me) => public static Quest GetUnlockQuest(this ClassJob me) =>
LuminaSheets.QuestSheet.GetRow(65720 + (uint)me); LuminaSheets.QuestSheet.GetRow(65720 + (uint)me);
public static ushort GetIconId(this ClassJob me) => public static ushort GetIconId(this ClassJob me) => (ushort)(62000 + me.GetClassJobIndex());
(ushort)(62000 + me.GetClassJobIndex());
public static bool IsClassJob(this ClassJobCategory me, ClassJob classJob) => public static bool IsClassJob(this ClassJobCategory me, ClassJob classJob) =>
classJob switch classJob switch
@@ -200,7 +221,7 @@ internal static class ClassJobUtils
ClassJob.Weaver => me.WVR, ClassJob.Weaver => me.WVR,
ClassJob.Alchemist => me.ALC, ClassJob.Alchemist => me.ALC,
ClassJob.Culinarian => me.CUL, ClassJob.Culinarian => me.CUL,
_ => false _ => false,
}; };
} }
@@ -219,7 +240,7 @@ internal static class ConditionUtils
Condition.Malleable => (13455, 14208), Condition.Malleable => (13455, 14208),
Condition.Primed => (13454, 14207), Condition.Primed => (13454, 14207),
Condition.GoodOmen => (14214, 14215), Condition.GoodOmen => (14214, 14215),
_ => (226, 14200) // Unknown _ => (226, 14200), // Unknown
}; };
private static Vector3 AddRGB(this Condition me) => private static Vector3 AddRGB(this Condition me) =>
@@ -235,10 +256,11 @@ internal static class ConditionUtils
Condition.Malleable => new(-80, -40, 180), Condition.Malleable => new(-80, -40, 180),
Condition.Primed => new(30, -155, 200), Condition.Primed => new(30, -155, 200),
Condition.GoodOmen => new(100, 20, 0), Condition.GoodOmen => new(100, 20, 0),
_ => Vector3.Zero // Unknown _ => Vector3.Zero, // Unknown
}; };
private const float ConditionCyclePeriod = 19 / 30f; private const float ConditionCyclePeriod = 19 / 30f;
// The real period of all condition color cycles are 0.633... (19/30) seconds // The real period of all condition color cycles are 0.633... (19/30) seconds
// Interp accepts 0-1 // Interp accepts 0-1
public static Vector4 GetColor(this Condition me, float interp) public static Vector4 GetColor(this Condition me, float interp)
@@ -252,12 +274,32 @@ internal static class ConditionUtils
addRgb = interp switch addRgb = interp switch
{ {
< 0.155f => Vector3.Lerp(new(128, 0, 0), new(128, 80, 0), (interp - 0) / 0.155f), < 0.155f => Vector3.Lerp(new(128, 0, 0), new(128, 80, 0), (interp - 0) / 0.155f),
< 0.315f => Vector3.Lerp(new(128, 80, 0), new(128, 128, 0), (interp - 0.155f) / 0.16f), < 0.315f => Vector3.Lerp(
< 0.475f => Vector3.Lerp(new(128, 128, 0), new(0, 64, 0), (interp - 0.315f) / 0.16f), new(128, 80, 0),
< 0.630f => Vector3.Lerp(new(0, 64, 0), new(0, 128, 128), (interp - 0.475f) / 0.155f), new(128, 128, 0),
< 0.790f => Vector3.Lerp(new(0, 128, 128), new(0, 0, 128), (interp - 0.630f) / 0.16f), (interp - 0.155f) / 0.16f
< 0.945f => Vector3.Lerp(new(0, 0, 128), new(64, 0, 64), (interp - 0.790f) / 0.155f), ),
_ => new(64, 0, 64) < 0.475f => Vector3.Lerp(
new(128, 128, 0),
new(0, 64, 0),
(interp - 0.315f) / 0.16f
),
< 0.630f => Vector3.Lerp(
new(0, 64, 0),
new(0, 128, 128),
(interp - 0.475f) / 0.155f
),
< 0.790f => Vector3.Lerp(
new(0, 128, 128),
new(0, 0, 128),
(interp - 0.630f) / 0.16f
),
< 0.945f => Vector3.Lerp(
new(0, 0, 128),
new(64, 0, 64),
(interp - 0.790f) / 0.155f
),
_ => new(64, 0, 64),
}; };
} }
// Period is twice as fast so we oscillate at twice that speed // Period is twice as fast so we oscillate at twice that speed
@@ -285,7 +327,7 @@ internal static class ConditionUtils
Condition.Pliant => Vector3.Lerp(new(0, 150, 0), new(0, 249, 0), interp), Condition.Pliant => Vector3.Lerp(new(0, 150, 0), new(0, 249, 0), interp),
Condition.Primed => Vector3.Lerp(new(-30, -255, 50), new(29, -156, 199), interp), Condition.Primed => Vector3.Lerp(new(-30, -255, 50), new(29, -156, 199), interp),
Condition.GoodOmen => Vector3.Lerp(new(100, 20, 0), new(100, 99, 99), interp), Condition.GoodOmen => Vector3.Lerp(new(100, 20, 0), new(100, 99, 99), interp),
_ => default _ => default,
}; };
} }
@@ -294,7 +336,9 @@ internal static class ConditionUtils
public static Vector4 GetColor(this Condition me, TimeSpan time) public static Vector4 GetColor(this Condition me, TimeSpan time)
{ {
return me.GetColor((float)(time.TotalSeconds % ConditionCyclePeriod / ConditionCyclePeriod)); return me.GetColor(
(float)(time.TotalSeconds % ConditionCyclePeriod / ConditionCyclePeriod)
);
} }
public static string Name(this Condition me) => public static string Name(this Condition me) =>
@@ -310,7 +354,11 @@ internal static class ConditionUtils
foreach (var payload in text) foreach (var payload in text)
{ {
if (payload is { Type: ReadOnlySePayloadType.Macro, MacroCode: MacroCode.Float }) if (payload is { Type: ReadOnlySePayloadType.Macro, MacroCode: MacroCode.Float })
finalText += new ReadOnlySePayload(ReadOnlySePayloadType.Text, default, Encoding.UTF8.GetBytes(isRelic ? "1.75" : "1.5")); finalText += new ReadOnlySePayload(
ReadOnlySePayloadType.Text,
default,
Encoding.UTF8.GetBytes(isRelic ? "1.75" : "1.5")
);
else else
finalText += payload; finalText += payload;
} }
+22 -7
View File
@@ -1,13 +1,18 @@
using Craftimizer.Plugin;
using Dalamud.Game.Command;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using Craftimizer.Plugin;
using Dalamud.Game.Command;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
[AttributeUsage(AttributeTargets.Method)] [AttributeUsage(AttributeTargets.Method)]
public sealed class CommandAttribute(string name, string description, bool hidden = false, params string[] aliases) : Attribute public sealed class CommandAttribute(
string name,
string description,
bool hidden = false,
params string[] aliases
) : Attribute
{ {
public string Name { get; } = name; public string Name { get; } = name;
public string Description { get; } = description; public string Description { get; } = description;
@@ -22,7 +27,11 @@ public sealed class AttributeCommandManager : IDisposable
public AttributeCommandManager() public AttributeCommandManager()
{ {
var target = Service.Plugin; var target = Service.Plugin;
foreach (var method in target.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) foreach (
var method in target
.GetType()
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
)
{ {
if (method.GetCustomAttribute<CommandAttribute>() is not { } command) if (method.GetCustomAttribute<CommandAttribute>() is not { } command)
continue; continue;
@@ -51,15 +60,21 @@ public sealed class AttributeCommandManager : IDisposable
}; };
if (!RegisteredCommands.Add(command.Name)) if (!RegisteredCommands.Add(command.Name))
throw new InvalidOperationException($"Command '{command.Name}' is already registered."); throw new InvalidOperationException(
$"Command '{command.Name}' is already registered."
);
if (!Service.CommandManager.AddHandler(command.Name, info)) if (!Service.CommandManager.AddHandler(command.Name, info))
throw new InvalidOperationException($"Failed to register command '{command.Name}'."); throw new InvalidOperationException(
$"Failed to register command '{command.Name}'."
);
foreach (var alias in command.Aliases) foreach (var alias in command.Aliases)
{ {
if (!RegisteredCommands.Add(alias)) if (!RegisteredCommands.Add(alias))
throw new InvalidOperationException($"Command '{alias}' is already registered."); throw new InvalidOperationException(
$"Command '{alias}' is already registered."
);
if (!Service.CommandManager.AddHandler(alias, aliasInfo)) if (!Service.CommandManager.AddHandler(alias, aliasInfo))
throw new InvalidOperationException($"Failed to register command '{alias}'."); throw new InvalidOperationException($"Failed to register command '{alias}'.");
+22 -19
View File
@@ -4,7 +4,8 @@ using System.Threading.Tasks;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
public sealed class BackgroundTask<T>(Func<CancellationToken, T> func) : IDisposable where T : struct public sealed class BackgroundTask<T>(Func<CancellationToken, T> func) : IDisposable
where T : struct
{ {
public T? Result { get; private set; } public T? Result { get; private set; }
public Exception? Exception { get; private set; } public Exception? Exception { get; private set; }
@@ -19,26 +20,28 @@ public sealed class BackgroundTask<T>(Func<CancellationToken, T> func) : IDispos
var token = TokenSource.Token; var token = TokenSource.Token;
var task = Task.Run(() => Result = Func(token), token); var task = Task.Run(() => Result = Func(token), token);
_ = task.ContinueWith(t => Completed = true); _ = task.ContinueWith(t => Completed = true);
_ = task.ContinueWith(t => _ = task.ContinueWith(
{ t =>
if (token.IsCancellationRequested) {
return; if (token.IsCancellationRequested)
return;
try try
{ {
t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException); t.Exception!.Flatten()
} .Handle(ex => ex is TaskCanceledException or OperationCanceledException);
catch (AggregateException e) }
{ catch (AggregateException e)
Exception = e; {
Log.Error(e, "Background task failed"); Exception = e;
} Log.Error(e, "Background task failed");
}, TaskContinuationOptions.OnlyOnFaulted); }
},
TaskContinuationOptions.OnlyOnFaulted
);
} }
public void Cancel() => public void Cancel() => TokenSource.Cancel();
TokenSource.Cancel();
public void Dispose() => public void Dispose() => Cancel();
Cancel();
} }
+3 -2
View File
@@ -1,13 +1,14 @@
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.System.String;
using System.Runtime.InteropServices;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
[StructLayout(LayoutKind.Explicit, Size = 2880)] [StructLayout(LayoutKind.Explicit, Size = 2880)]
public unsafe struct CSRecipeNote public unsafe struct CSRecipeNote
{ {
[FieldOffset(0x118)] public ushort ActiveCraftRecipeId; [FieldOffset(0x118)]
public ushort ActiveCraftRecipeId;
public static CSRecipeNote* Instance() public static CSRecipeNote* Instance()
{ {
+23 -11
View File
@@ -1,6 +1,6 @@
using System;
using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using System;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
@@ -19,16 +19,28 @@ public static unsafe class Chat
var unsanitizedLength = str->Length; var unsanitizedLength = str->Length;
str->SanitizeString( str->SanitizeString(
AllowedEntities.Unknown9 | // 200 AllowedEntities.Unknown9
AllowedEntities.Payloads | // 40 | // 200
AllowedEntities.OtherCharacters | // 20 AllowedEntities.Payloads
AllowedEntities.CharacterList | // 10 | // 40
AllowedEntities.SpecialCharacters | // 8 AllowedEntities.OtherCharacters
AllowedEntities.Numbers | // 4 | // 20
AllowedEntities.LowercaseLetters | // 2 AllowedEntities.CharacterList
AllowedEntities.UppercaseLetters, // 1 | // 10
null); AllowedEntities.SpecialCharacters
ArgumentOutOfRangeException.ThrowIfNotEqual(unsanitizedLength, str->Length, nameof(message)); | // 8
AllowedEntities.Numbers
| // 4
AllowedEntities.LowercaseLetters
| // 2
AllowedEntities.UppercaseLetters, // 1
null
);
ArgumentOutOfRangeException.ThrowIfNotEqual(
unsanitizedLength,
str->Length,
nameof(message)
);
UIModule.Instance()->ProcessChatBoxEntry(str); UIModule.Instance()->ProcessChatBoxEntry(str);
} }
+6 -5
View File
@@ -1,8 +1,8 @@
using Craftimizer.Plugin;
using Dalamud.Interface.Colors;
using Dalamud.Bindings.ImGui;
using System; using System;
using System.Numerics; using System.Numerics;
using Craftimizer.Plugin;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Colors;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
@@ -15,7 +15,8 @@ public static class Colors
public static readonly Vector4 Collectability = new(0.99f, 0.56f, 0.57f, 1f); public static readonly Vector4 Collectability = new(0.99f, 0.56f, 0.57f, 1f);
public static readonly Vector4 CP = new(0.63f, 0.37f, 0.75f, 1f); public static readonly Vector4 CP = new(0.63f, 0.37f, 0.75f, 1f);
private static Vector4 SolverProgressBg => ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.TableBorderLight)); private static Vector4 SolverProgressBg =>
ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.TableBorderLight));
private static Vector4 SolverProgressFgBland => ImGuiColors.DalamudWhite2; private static Vector4 SolverProgressFgBland => ImGuiColors.DalamudWhite2;
private static readonly Vector4[] SolverProgressFgColorful = private static readonly Vector4[] SolverProgressFgColorful =
@@ -52,7 +53,7 @@ public static class Colors
{ {
Configuration.ProgressBarType.Colorful => SolverProgressFgColorful, Configuration.ProgressBarType.Colorful => SolverProgressFgColorful,
Configuration.ProgressBarType.Simple => SolverProgressFgMonochromatic, Configuration.ProgressBarType.Simple => SolverProgressFgMonochromatic,
_ => throw new InvalidOperationException("No progress bar should be visible") _ => throw new InvalidOperationException("No progress bar should be visible"),
}; };
if (stageValue is not { } stage) if (stageValue is not { } stage)
+105 -55
View File
@@ -1,18 +1,18 @@
using Dalamud.Networking.Http;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Text.Json.Serialization;
using System.Net.Http.Json;
using System.Text.Json;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Craftimizer.Simulator.Actions; using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Craftimizer.Simulator; using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Craftimizer.Solver; using Craftimizer.Solver;
using Dalamud.Networking.Http;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
@@ -40,7 +40,9 @@ public sealed class CommunityMacros
{ {
[JsonPropertyName("integerValue")] [JsonPropertyName("integerValue")]
[JsonRequired] [JsonRequired]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)] [JsonNumberHandling(
JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString
)]
public required int Value { get; set; } public required int Value { get; set; }
public static implicit operator int(IntegerValue v) => v.Value; public static implicit operator int(IntegerValue v) => v.Value;
@@ -59,6 +61,7 @@ public sealed class CommunityMacros
public required ValueData Data { get; set; } public required ValueData Data { get; set; }
public T Value => Data.Fields; public T Value => Data.Fields;
public static implicit operator T(MapValue<T> v) => v.Value; public static implicit operator T(MapValue<T> v) => v.Value;
} }
@@ -74,6 +77,7 @@ public sealed class CommunityMacros
public required ValueData Data { get; set; } public required ValueData Data { get; set; }
public T[] Value => Data.Values ?? []; public T[] Value => Data.Values ?? [];
public static implicit operator T[](ArrayValue<T> v) => v.Value; public static implicit operator T[](ArrayValue<T> v) => v.Value;
} }
@@ -88,8 +92,10 @@ public sealed class CommunityMacros
{ {
[JsonRequired] [JsonRequired]
public required List<CollectionSelector> From { get; set; } public required List<CollectionSelector> From { get; set; }
[JsonRequired] [JsonRequired]
public required Filter Where { get; set; } public required Filter Where { get; set; }
[JsonRequired] [JsonRequired]
public required List<Order> OrderBy { get; set; } public required List<Order> OrderBy { get; set; }
} }
@@ -109,6 +115,7 @@ public sealed class CommunityMacros
{ {
[JsonRequired] [JsonRequired]
public required List<Filter> Filters { get; set; } public required List<Filter> Filters { get; set; }
[JsonRequired] [JsonRequired]
public required CompositeOperator Op { get; set; } public required CompositeOperator Op { get; set; }
} }
@@ -117,13 +124,14 @@ public sealed class CommunityMacros
{ {
OPERATOR_UNSPECIFIED, OPERATOR_UNSPECIFIED,
AND, AND,
OR OR,
} }
public sealed record FieldFilter public sealed record FieldFilter
{ {
[JsonRequired] [JsonRequired]
public required FieldReference Field { get; set; } public required FieldReference Field { get; set; }
[JsonRequired] [JsonRequired]
public required FieldOperator Op { get; set; } public required FieldOperator Op { get; set; }
public object? Value { get; set; } public object? Value { get; set; }
@@ -141,13 +149,14 @@ public sealed class CommunityMacros
ARRAY_CONTAINS, ARRAY_CONTAINS,
IN, IN,
ARRAY_CONTAINS_ANY, ARRAY_CONTAINS_ANY,
NOT_IN NOT_IN,
} }
public sealed record Order public sealed record Order
{ {
[JsonRequired] [JsonRequired]
public required FieldReference Field { get; set; } public required FieldReference Field { get; set; }
[JsonRequired] [JsonRequired]
public required Direction Direction { get; set; } public required Direction Direction { get; set; }
} }
@@ -162,7 +171,7 @@ public sealed class CommunityMacros
{ {
DIRECTION_UNSPECIFIED, DIRECTION_UNSPECIFIED,
ASCENDING, ASCENDING,
DESCENDING DESCENDING,
} }
private sealed record RunQueryRequest private sealed record RunQueryRequest
@@ -178,6 +187,7 @@ public sealed class CommunityMacros
[JsonRequired] [JsonRequired]
[JsonPropertyName("rlvl")] [JsonPropertyName("rlvl")]
public required IntegerValue RLvl { get; set; } public required IntegerValue RLvl { get; set; }
[JsonRequired] [JsonRequired]
public required IntegerValue Durability { get; set; } public required IntegerValue Durability { get; set; }
} }
@@ -185,6 +195,7 @@ public sealed class CommunityMacros
public sealed record FieldData public sealed record FieldData
{ {
public StringValue? Name { get; set; } public StringValue? Name { get; set; }
[JsonRequired] [JsonRequired]
public required ArrayValue<StringValue> Rotation { get; set; } public required ArrayValue<StringValue> Rotation { get; set; }
public MapValue<RecipeFieldData>? Recipe { get; set; } public MapValue<RecipeFieldData>? Recipe { get; set; }
@@ -212,6 +223,7 @@ public sealed class CommunityMacros
public string? Slug { get; set; } public string? Slug { get; set; }
public string? Version { get; set; } public string? Version { get; set; }
public string? Job { get; set; } public string? Job { get; set; }
[JsonPropertyName("job_level")] [JsonPropertyName("job_level")]
public int JobLevel { get; set; } public int JobLevel { get; set; }
public int Craftsmanship { get; set; } public int Craftsmanship { get; set; }
@@ -219,11 +231,14 @@ public sealed class CommunityMacros
public int CP { get; set; } public int CP { get; set; }
public string? Food { get; set; } public string? Food { get; set; }
public string? Potion { get; set; } public string? Potion { get; set; }
[JsonPropertyName("recipe_job_level")] [JsonPropertyName("recipe_job_level")]
public int RecipeJobLevel { get; set; } public int RecipeJobLevel { get; set; }
public string? Recipe { get; set; } public string? Recipe { get; set; }
// HqIngredients // HqIngredients
public string? Actions { get; set; } public string? Actions { get; set; }
[JsonPropertyName("created_at")] [JsonPropertyName("created_at")]
public long CreatedAt { get; set; } public long CreatedAt { get; set; }
public string? Error { get; set; } public string? Error { get; set; }
@@ -241,10 +256,13 @@ public sealed class CommunityMacros
throw new Exception($"Internal error; No fields were returned"); throw new Exception($"Internal error; No fields were returned");
// https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/67f453041c6b2b31d32fcf6e1fd53aa38ed7a12b/apps/client/src/app/model/other/crafting-rotation.ts#L49 // https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/67f453041c6b2b31d32fcf6e1fd53aa38ed7a12b/apps/client/src/app/model/other/crafting-rotation.ts#L49
Name = rotation.Name?.Value ?? Name =
(rotation.Recipe is { Value: var recipe } ? rotation.Name?.Value
$"rlvl{recipe.RLvl.Value} - {rotation.Rotation.Value.Length} steps, {recipe.Durability.Value} dur" : ?? (
"New Teamcraft Rotation"); rotation.Recipe is { Value: var recipe }
? $"rlvl{recipe.RLvl.Value} - {rotation.Rotation.Value.Length} steps, {recipe.Durability.Value} dur"
: "New Teamcraft Rotation"
);
var actions = new List<ActionType>(); var actions = new List<ActionType>();
foreach (var action in rotation.Rotation.Value) foreach (var action in rotation.Rotation.Value)
@@ -290,7 +308,7 @@ public sealed class CommunityMacros
"RemoveFinalAppraisal" => null, "RemoveFinalAppraisal" => null,
// Old actions? // Old actions?
_ => null _ => null,
}; };
if (actionType.HasValue) if (actionType.HasValue)
actions.Add(actionType.Value); actions.Add(actionType.Value);
@@ -353,7 +371,7 @@ public sealed class CommunityMacros
"DelicateSynthesisTraited" => ActionType.DelicateSynthesis, "DelicateSynthesisTraited" => ActionType.DelicateSynthesis,
// Old actions? // Old actions?
_ => null _ => null,
}; };
if (actionType.HasValue) if (actionType.HasValue)
actions.Add(actionType.Value); actions.Add(actionType.Value);
@@ -361,16 +379,30 @@ public sealed class CommunityMacros
Actions = actions; Actions = actions;
} }
public (float Score, SimulationState FinalState) CalculateScore(SimulatorNoRandom simulator, in SimulationState startingState, in MCTSConfig mctsConfig) public (float Score, SimulationState FinalState) CalculateScore(
SimulatorNoRandom simulator,
in SimulationState startingState,
in MCTSConfig mctsConfig
)
{ {
return CalculateScore(Actions, simulator, startingState, mctsConfig); return CalculateScore(Actions, simulator, startingState, mctsConfig);
} }
public static (float Score, SimulationState FinalState) CalculateScore(IReadOnlyCollection<ActionType> actions, SimulatorNoRandom simulator, in SimulationState startingState, in MCTSConfig mctsConfig) public static (float Score, SimulationState FinalState) CalculateScore(
IReadOnlyCollection<ActionType> actions,
SimulatorNoRandom simulator,
in SimulationState startingState,
in MCTSConfig mctsConfig
)
{ {
var (resp, outState, failedIdx) = simulator.ExecuteMultiple(startingState, actions); var (resp, outState, failedIdx) = simulator.ExecuteMultiple(startingState, actions);
outState.ActionCount = actions.Count; outState.ActionCount = actions.Count;
var score = SimulationNode.CalculateScoreForState(outState, simulator.CompletionState, mctsConfig) ?? 0; var score =
SimulationNode.CalculateScoreForState(
outState,
simulator.CompletionState,
mctsConfig
) ?? 0;
if (resp != ActionResponse.SimulationComplete) if (resp != ActionResponse.SimulationComplete)
{ {
if (failedIdx != -1) if (failedIdx != -1)
@@ -382,7 +414,10 @@ public sealed class CommunityMacros
private Dictionary<int, List<CommunityMacro>> CachedRotations { get; } = []; private Dictionary<int, List<CommunityMacro>> CachedRotations { get; } = [];
public async Task<IReadOnlyList<CommunityMacro>> RetrieveRotations(int rlvl, CancellationToken token) public async Task<IReadOnlyList<CommunityMacro>> RetrieveRotations(
int rlvl,
CancellationToken token
)
{ {
lock (CachedRotations) lock (CachedRotations)
{ {
@@ -397,23 +432,25 @@ public sealed class CommunityMacros
return macros; return macros;
} }
private static async Task<List<TeamcraftMacro>> RetrieveRotationsInternal(int rlvl, CancellationToken token) private static async Task<List<TeamcraftMacro>> RetrieveRotationsInternal(
int rlvl,
CancellationToken token
)
{ {
using var heCallback = new HappyEyeballsCallback(); using var heCallback = new HappyEyeballsCallback();
using var client = new HttpClient(new SocketsHttpHandler using var client = new HttpClient(
{ new SocketsHttpHandler
AutomaticDecompression = DecompressionMethods.All, {
ConnectCallback = heCallback.ConnectCallback, AutomaticDecompression = DecompressionMethods.All,
}); ConnectCallback = heCallback.ConnectCallback,
}
);
var request = new RunQueryRequest var request = new RunQueryRequest
{ {
StructuredQuery = new StructuredQuery StructuredQuery = new StructuredQuery
{ {
From = From = [new() { CollectionId = "rotations" }],
[
new() { CollectionId = "rotations" }
],
Where = new Filter Where = new Filter
{ {
CompositeFilter = new CompositeFilter CompositeFilter = new CompositeFilter
@@ -427,8 +464,8 @@ public sealed class CommunityMacros
{ {
Field = new FieldReference { FieldPath = "public" }, Field = new FieldReference { FieldPath = "public" },
Op = FieldOperator.EQUAL, Op = FieldOperator.EQUAL,
Value = new BooleanValue { Value = true } Value = new BooleanValue { Value = true },
} },
}, },
new() new()
{ {
@@ -436,10 +473,10 @@ public sealed class CommunityMacros
{ {
Field = new FieldReference { FieldPath = "community.rlvl" }, Field = new FieldReference { FieldPath = "community.rlvl" },
Op = FieldOperator.EQUAL, Op = FieldOperator.EQUAL,
Value = new IntegerValue { Value = rlvl } Value = new IntegerValue { Value = rlvl },
} },
} },
] ],
}, },
}, },
OrderBy = OrderBy =
@@ -447,39 +484,50 @@ public sealed class CommunityMacros
new() new()
{ {
Field = new FieldReference { FieldPath = "xivVersion" }, Field = new FieldReference { FieldPath = "xivVersion" },
Direction = Direction.DESCENDING Direction = Direction.DESCENDING,
}, },
new() new()
{ {
Field = new FieldReference { FieldPath = "__name__" }, Field = new FieldReference { FieldPath = "__name__" },
Direction = Direction.DESCENDING Direction = Direction.DESCENDING,
} },
] ],
}, },
}; };
var resp = await PostFromJsonAsync<RunQueryRequest, List<QueriedTeamcraftMacro>>( var resp = await PostFromJsonAsync<RunQueryRequest, List<QueriedTeamcraftMacro>>(
client, client,
$"https://firestore.googleapis.com/v1beta1/projects/ffxivteamcraft/databases/(default)/documents:runQuery", $"https://firestore.googleapis.com/v1beta1/projects/ffxivteamcraft/databases/(default)/documents:runQuery",
request, new JsonSerializerOptions request,
{ new JsonSerializerOptions
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, {
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
}, token). PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
ConfigureAwait(false); },
token
)
.ConfigureAwait(false);
if (resp is null) if (resp is null)
throw new Exception("Internal server error; failed to retrieve macro"); throw new Exception("Internal server error; failed to retrieve macro");
foreach(var macro in resp) foreach (var macro in resp)
{ {
if (macro.Error is { } error) if (macro.Error is { } error)
throw new Exception($"Internal server error ({error.Status}); {error.Message}"); throw new Exception($"Internal server error ({error.Status}); {error.Message}");
} }
return resp.Where(macro => macro.Document is not null).Select(macro => macro.Document!).ToList(); return resp.Where(macro => macro.Document is not null)
.Select(macro => macro.Document!)
.ToList();
} }
private static async Task<TResponse?> PostFromJsonAsync<TRequest, TResponse>(HttpClient client, [StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, TRequest value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) private static async Task<TResponse?> PostFromJsonAsync<TRequest, TResponse>(
HttpClient client,
[StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri,
TRequest value,
JsonSerializerOptions? options = null,
CancellationToken cancellationToken = default
)
{ {
ArgumentNullException.ThrowIfNull(client); ArgumentNullException.ThrowIfNull(client);
@@ -487,6 +535,8 @@ public sealed class CommunityMacros
using var message = await resp.ConfigureAwait(false); using var message = await resp.ConfigureAwait(false);
message.EnsureSuccessStatusCode(); message.EnsureSuccessStatusCode();
return await message.Content!.ReadFromJsonAsync<TResponse>(options, cancellationToken).ConfigureAwait(false); return await message
.Content!.ReadFromJsonAsync<TResponse>(options, cancellationToken)
.ConfigureAwait(false);
} }
} }
@@ -0,0 +1,27 @@
using System.Linq;
using Dalamud.Plugin;
namespace Craftimizer.Utils;
internal static class CraftimizerConflictDetector
{
private const string UpstreamInternalName = "Craftimizer";
public static void ThrowIfUpstreamLoaded(IDalamudPluginInterface pluginInterface)
{
var conflict = pluginInterface.InstalledPlugins.FirstOrDefault(p =>
p.InternalName == UpstreamInternalName && p.IsLoaded
);
if (conflict is null)
return;
throw new System.InvalidOperationException(
"Forgeimizer cannot load while the upstream Craftimizer plugin is active.\n\n"
+ "Both plugins register the same Dalamud hooks (UseAction, IsActionHighlighted) "
+ "and would conflict if loaded together.\n\n"
+ "Action: open /xlplugins, disable the upstream 'Craftimizer' plugin, "
+ "then re-enable Forgeimizer."
);
}
}
+65 -37
View File
@@ -1,28 +1,34 @@
using Craftimizer.Plugin;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
using System.Collections.Generic;
using System; using System;
using System.Numerics; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Utility.Numerics; using System.Numerics;
using Craftimizer.Plugin;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Utility.Numerics;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
internal static class DynamicBars internal static class DynamicBars
{ {
public readonly record struct BarData(string Name, Vector4 Color, SimulatedMacro.Reliablity.Param? Reliability, float Value, float Max, IReadOnlyList<int?>? Collectability = null, string? Caption = null, string? DefaultCaptionSizeText = null, Action<DrawerParams>? CustomDrawer = null) public readonly record struct BarData(
string Name,
Vector4 Color,
SimulatedMacro.Reliablity.Param? Reliability,
float Value,
float Max,
IReadOnlyList<int?>? Collectability = null,
string? Caption = null,
string? DefaultCaptionSizeText = null,
Action<DrawerParams>? CustomDrawer = null
)
{ {
public BarData(string name, Action<DrawerParams> customDrawer) : this(name, default, null, 0, 0, null, null, null, customDrawer) public BarData(string name, Action<DrawerParams> customDrawer)
{ : this(name, default, null, 0, 0, null, null, null, customDrawer) { }
} public BarData(string name, Vector4 color, float value, float max)
: this(name, color, null, value, max, null, null, null) { }
public BarData(string name, Vector4 color, float value, float max) : this(name, color, null, value, max, null, null, null)
{
}
} }
public readonly record struct DrawerParams(float TotalSize, float Spacing); public readonly record struct DrawerParams(float TotalSize, float Spacing);
@@ -39,13 +45,19 @@ internal static class DynamicBars
return Math.Max(ImGui.CalcTextSize(caption).X, defaultSize); return Math.Max(ImGui.CalcTextSize(caption).X, defaultSize);
// max (sp/2) "/" (sp/2) max // max (sp/2) "/" (sp/2) max
return Math.Max( return Math.Max(
Math.Max(ImGui.CalcTextSize($"{b.Value:0}").X, ImGui.CalcTextSize($"{b.Max:0}").X) * 2 Math.Max(ImGui.CalcTextSize($"{b.Value:0}").X, ImGui.CalcTextSize($"{b.Max:0}").X)
+ ImGui.GetStyle().ItemSpacing.X * 2
+ ImGui.CalcTextSize("/").X, + ImGui.GetStyle().ItemSpacing.X
defaultSize); + ImGui.CalcTextSize("/").X,
defaultSize
);
}); });
private static ImRaii.Color? PushCollectableColor(this in BarData bar, float collectability, bool colorUnmetThreshold = true) private static ImRaii.ColorDisposable? PushCollectableColor(
this in BarData bar,
float collectability,
bool colorUnmetThreshold = true
)
{ {
if (bar.Collectability is not { } collectabilities) if (bar.Collectability is not { } collectabilities)
return null; return null;
@@ -88,7 +100,10 @@ internal static class DynamicBars
var pos = ImGui.GetCursorPos(); var pos = ImGui.GetCursorPos();
var screenPos = ImGui.GetCursorScreenPos(); var screenPos = ImGui.GetCursorScreenPos();
using (var color = ImRaii.PushColor(ImGuiCol.PlotHistogram, bar.Color)) using (var color = ImRaii.PushColor(ImGuiCol.PlotHistogram, bar.Color))
ImGuiUtils.ProgressBar(Math.Clamp(bar.Value / bar.Max, 0, 1), new(barSize, ImGui.GetFrameHeight())); ImGuiUtils.ProgressBar(
Math.Clamp(bar.Value / bar.Max, 0, 1),
new(barSize, ImGui.GetFrameHeight())
);
if (bar.Collectability is { } collectability) if (bar.Collectability is { } collectability)
{ {
var i = 0; var i = 0;
@@ -101,27 +116,36 @@ internal static class DynamicBars
continue; continue;
var offset = barSize * threshold / bar.Max; var offset = barSize * threshold / bar.Max;
var isLast = i == collectability.Count; var isLast = i == collectability.Count;
var offsetNext = isLast ? barSize : barSize * collectability[i]!.Value / bar.Max; var offsetNext = isLast
? barSize
: barSize * collectability[i]!.Value / bar.Max;
var passedThreshold = bar.Value >= threshold; var passedThreshold = bar.Value >= threshold;
ImGui.GetWindowDrawList().AddRectFilled( ImGui
screenPos + new Vector2(offset, 0), .GetWindowDrawList()
screenPos + new Vector2(offsetNext, height), .AddRectFilled(
ImGui.GetColorU32(color.WithW(passedThreshold ? 0.6f : 0.2f)), screenPos + new Vector2(offset, 0),
isLast ? rounding : 0 screenPos + new Vector2(offsetNext, height),
); ImGui.GetColorU32(color.WithW(passedThreshold ? 0.6f : 0.2f)),
ImGui.GetWindowDrawList().AddLine( isLast ? rounding : 0
screenPos + new Vector2(offset, 0), );
screenPos + new Vector2(offset, height), ImGui
ImGui.GetColorU32(color), .GetWindowDrawList()
Math.Max(passedThreshold ? 3 : 1.5f, rounding / 2f) .AddLine(
); screenPos + new Vector2(offset, 0),
screenPos + new Vector2(offset, height),
ImGui.GetColorU32(color),
Math.Max(passedThreshold ? 3 : 1.5f, rounding / 2f)
);
} }
} }
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenOverlapped)) if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenOverlapped))
{ {
if (bar.Reliability is { } reliability) if (bar.Reliability is { } reliability)
{ {
if (reliability.GetViolinData(bar.Max, (int)(barSize / 5), 0.02) is { } violinData) if (
reliability.GetViolinData(bar.Max, (int)(barSize / 5), 0.02) is
{ } violinData
)
{ {
ImGui.SetCursorPos(pos); ImGui.SetCursorPos(pos);
ImGuiUtils.ViolinPlot(violinData, new(barSize, ImGui.GetFrameHeight())); ImGuiUtils.ViolinPlot(violinData, new(barSize, ImGui.GetFrameHeight()));
@@ -129,7 +153,10 @@ internal static class DynamicBars
{ {
using var _font = ImRaii.PushFont(UiBuilder.DefaultFont); using var _font = ImRaii.PushFont(UiBuilder.DefaultFont);
using var _tooltip = ImRaii.Tooltip(); using var _tooltip = ImRaii.Tooltip();
using var _ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); using var _ = ImRaii.PushStyle(
ImGuiStyleVar.ItemSpacing,
Vector2.Zero
);
ImGui.TextUnformatted("Min: "); ImGui.TextUnformatted("Min: ");
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
@@ -221,7 +248,8 @@ internal static class DynamicBars
{ {
tooltip = $"Solver Progress: {solver.ProgressValue:N0} / {solver.ProgressMax:N0}"; tooltip = $"Solver Progress: {solver.ProgressValue:N0} / {solver.ProgressMax:N0}";
if (solver.ProgressValue > solver.ProgressMax) if (solver.ProgressValue > solver.ProgressMax)
tooltip += $"\n\nThis is taking longer than expected. Check to see if your gear stats are good and the solver settings are adequate."; tooltip +=
$"\n\nThis is taking longer than expected. Check to see if your gear stats are good and the solver settings are adequate.";
} }
ImGuiUtils.TooltipWrapped(tooltip); ImGuiUtils.TooltipWrapped(tooltip);
} }
+51 -15
View File
@@ -1,9 +1,9 @@
using Craftimizer.Plugin;
using Lumina.Excel.Sheets;
using System.Collections.Frozen; using System.Collections.Frozen;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using Craftimizer.Plugin;
using Lumina.Excel.Sheets;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
@@ -15,8 +15,20 @@ public static class FoodStatus
private static readonly ImmutableArray<uint> FoodOrder; private static readonly ImmutableArray<uint> FoodOrder;
private static readonly ImmutableArray<uint> MedicineOrder; private static readonly ImmutableArray<uint> MedicineOrder;
public readonly record struct FoodStat(bool IsRelative, int Value, int Max, int ValueHQ, int MaxHQ); public readonly record struct FoodStat(
public readonly record struct Food(Item Item, FoodStat? Craftsmanship, FoodStat? Control, FoodStat? CP); bool IsRelative,
int Value,
int Max,
int ValueHQ,
int MaxHQ
);
public readonly record struct Food(
Item Item,
FoodStat? Craftsmanship,
FoodStat? Control,
FoodStat? CP
);
static FoodStatus() static FoodStatus()
{ {
@@ -33,24 +45,39 @@ public static class FoodStatus
if (item.ItemAction.ValueNullable is not { } itemAction) if (item.ItemAction.ValueNullable is not { } itemAction)
continue; continue;
if (itemAction.Type is not (844 or 845 or 846)) if (itemAction.Action.RowId is not (844 or 845 or 846))
continue; continue;
if (LuminaSheets.ItemFoodSheet.GetRowOrDefault(itemAction.Data[1]) is not { } itemFood) if (LuminaSheets.ItemFoodSheet.GetRowOrDefault(itemAction.Data[1]) is not { } itemFood)
continue; continue;
FoodStat? craftsmanship = null, control = null, cp = null; FoodStat? craftsmanship = null,
control = null,
cp = null;
foreach (var stat in itemFood.Params) foreach (var stat in itemFood.Params)
{ {
if (stat.BaseParam.RowId == 0) if (stat.BaseParam.RowId == 0)
continue; continue;
var foodStat = new FoodStat(stat.IsRelative, stat.Value, stat.Max, stat.ValueHQ, stat.MaxHQ); var foodStat = new FoodStat(
stat.IsRelative,
stat.Value,
stat.Max,
stat.ValueHQ,
stat.MaxHQ
);
switch (stat.BaseParam.RowId) switch (stat.BaseParam.RowId)
{ {
case Gearsets.ParamCraftsmanship: craftsmanship = foodStat; break; case Gearsets.ParamCraftsmanship:
case Gearsets.ParamControl: control = foodStat; break; craftsmanship = foodStat;
case Gearsets.ParamCP: cp = foodStat; break; break;
default: continue; case Gearsets.ParamControl:
control = foodStat;
break;
case Gearsets.ParamCP:
cp = foodStat;
break;
default:
continue;
} }
} }
@@ -66,18 +93,27 @@ public static class FoodStatus
lut.TryAdd(itemFood.RowId, item.RowId); lut.TryAdd(itemFood.RowId, item.RowId);
} }
ItemFoodToItemLUT = lut.ToFrozenDictionary(); ItemFoodToItemLUT = lut.ToFrozenDictionary();
FoodItems = foods.ToFrozenDictionary(); FoodItems = foods.ToFrozenDictionary();
MedicineItems = medicines.ToFrozenDictionary(); MedicineItems = medicines.ToFrozenDictionary();
FoodOrder = FoodItems.OrderByDescending(a => a.Value.Item.LevelItem.RowId).Select(a => a.Key).ToImmutableArray(); FoodOrder =
MedicineOrder = MedicineItems.OrderByDescending(a => a.Value.Item.LevelItem.RowId).Select(a => a.Key).ToImmutableArray(); [
.. FoodItems.OrderByDescending(a => a.Value.Item.LevelItem.RowId).Select(a => a.Key),
];
MedicineOrder =
[
.. MedicineItems
.OrderByDescending(a => a.Value.Item.LevelItem.RowId)
.Select(a => a.Key),
];
} }
public static void Initialize() { } public static void Initialize() { }
public static IEnumerable<Food> OrderedFoods => FoodOrder.Select(id => FoodItems[id]); public static IEnumerable<Food> OrderedFoods => FoodOrder.Select(id => FoodItems[id]);
public static IEnumerable<Food> OrderedMedicines => MedicineOrder.Select(id => MedicineItems[id]); public static IEnumerable<Food> OrderedMedicines =>
MedicineOrder.Select(id => MedicineItems[id]);
public static (uint ItemId, bool IsHQ)? ResolveFoodParam(ushort param) public static (uint ItemId, bool IsHQ)? ResolveFoodParam(ushort param)
{ {
+40 -14
View File
@@ -24,7 +24,11 @@ internal readonly struct FuzzyMatcher
{ {
MatchMode.FuzzyParts => FindNeedleSegments(needleString), MatchMode.FuzzyParts => FindNeedleSegments(needleString),
MatchMode.Fuzzy or MatchMode.Simple => EmptySegArray, MatchMode.Fuzzy or MatchMode.Simple => EmptySegArray,
_ => throw new ArgumentOutOfRangeException(nameof(matchMode), matchMode, "Invalid match mode"), _ => throw new ArgumentOutOfRangeException(
nameof(matchMode),
matchMode,
"Invalid match mode"
),
}; };
} }
@@ -59,7 +63,9 @@ internal readonly struct FuzzyMatcher
return 0; return 0;
if (mode == MatchMode.Simple) if (mode == MatchMode.Simple)
return value.Contains(needleString, StringComparison.InvariantCultureIgnoreCase) ? 1 : 0; return value.Contains(needleString, StringComparison.InvariantCultureIgnoreCase)
? 1
: 0;
if (mode == MatchMode.Fuzzy) if (mode == MatchMode.Fuzzy)
return GetRawScore(value, 0, needleFinalPosition); return GetRawScore(value, 0, needleFinalPosition);
@@ -101,7 +107,11 @@ internal readonly struct FuzzyMatcher
private int GetRawScore(ReadOnlySpan<char> haystack, int needleStart, int needleEnd) private int GetRawScore(ReadOnlySpan<char> haystack, int needleStart, int needleEnd)
{ {
var (startPos, gaps, consecutive, borderMatches, endPos) = FindForward(haystack, needleStart, needleEnd); var (startPos, gaps, consecutive, borderMatches, endPos) = FindForward(
haystack,
needleStart,
needleEnd
);
if (startPos < 0) if (startPos < 0)
return 0; return 0;
@@ -109,28 +119,40 @@ internal readonly struct FuzzyMatcher
var score = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches); var score = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches);
(startPos, gaps, consecutive, borderMatches) = FindReverse(haystack, endPos, needleStart, needleEnd); (startPos, gaps, consecutive, borderMatches) = FindReverse(
haystack,
endPos,
needleStart,
needleEnd
);
var revScore = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches); var revScore = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches);
return int.Max(score, revScore); return int.Max(score, revScore);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int CalculateRawScore(int needleSize, int startPos, int gaps, int consecutive, int borderMatches) private static int CalculateRawScore(
int needleSize,
int startPos,
int gaps,
int consecutive,
int borderMatches
)
{ {
var score = 100 var score =
+ needleSize * 3 100 + needleSize * 3 + borderMatches * 3 + consecutive * 5 - startPos - gaps * 2;
+ borderMatches * 3
+ consecutive * 5
- startPos
- gaps * 2;
if (startPos == 0) if (startPos == 0)
score += 5; score += 5;
return score < 1 ? 1 : score; return score < 1 ? 1 : score;
} }
private (int StartPos, int Gaps, int Consecutive, int BorderMatches, int HaystackIndex) FindForward( private (
ReadOnlySpan<char> haystack, int needleStart, int needleEnd) int StartPos,
int Gaps,
int Consecutive,
int BorderMatches,
int HaystackIndex
) FindForward(ReadOnlySpan<char> haystack, int needleStart, int needleEnd)
{ {
var needleIndex = needleStart; var needleIndex = needleStart;
var lastMatchIndex = -10; var lastMatchIndex = -10;
@@ -176,7 +198,11 @@ internal readonly struct FuzzyMatcher
} }
private (int StartPos, int Gaps, int Consecutive, int BorderMatches) FindReverse( private (int StartPos, int Gaps, int Consecutive, int BorderMatches) FindReverse(
ReadOnlySpan<char> haystack, int haystackLastMatchIndex, int needleStart, int needleEnd) ReadOnlySpan<char> haystack,
int haystackLastMatchIndex,
int needleStart,
int needleEnd
)
{ {
var needleIndex = needleEnd; var needleIndex = needleEnd;
var revLastMatchIndex = haystack.Length + 10; var revLastMatchIndex = haystack.Length + 10;
+61 -20
View File
@@ -1,18 +1,20 @@
using System;
using System.Linq;
using Craftimizer.Plugin;
using Craftimizer.Simulator; using Craftimizer.Simulator;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using System;
using System.Linq;
using Craftimizer.Plugin;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
public static unsafe class Gearsets public static unsafe class Gearsets
{ {
public record struct GearsetStats(int CP, int Craftsmanship, int Control); public record struct GearsetStats(int CP, int Craftsmanship, int Control);
public record struct GearsetMateria(ushort Type, ushort Grade); public record struct GearsetMateria(ushort Type, ushort Grade);
public record struct GearsetItem(uint ItemId, bool IsHq, GearsetMateria[] Materia); public record struct GearsetItem(uint ItemId, bool IsHq, GearsetMateria[] Materia);
private static readonly GearsetStats BaseStats = new(180, 0, 0); private static readonly GearsetStats BaseStats = new(180, 0, 0);
@@ -27,7 +29,11 @@ public static unsafe class Gearsets
for (var i = 0; i < container->Size; ++i) for (var i = 0; i < container->Size; ++i)
{ {
var item = container->Items[i]; var item = container->Items[i];
items[i] = new(item.ItemId, item.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality), GetMaterias(item.Materia, item.MateriaGrades)); items[i] = new(
item.ItemId,
item.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality),
GetMaterias(item.Materia, item.MateriaGrades)
);
} }
return items; return items;
} }
@@ -39,7 +45,11 @@ public static unsafe class Gearsets
for (var i = 0; i < 14; ++i) for (var i = 0; i < 14; ++i)
{ {
var item = gearsetItems[i]; var item = gearsetItems[i];
items[i] = new(item.ItemId % 1000000, item.ItemId > 1000000, GetMaterias(item.Materia, item.MateriaGrades)); items[i] = new(
item.ItemId % 1000000,
item.ItemId > 1000000,
GetMaterias(item.Materia, item.MateriaGrades)
);
} }
return items; return items;
} }
@@ -48,7 +58,9 @@ public static unsafe class Gearsets
{ {
var item = LuminaSheets.ItemSheet.GetRow(gearsetItem.ItemId)!; var item = LuminaSheets.ItemSheet.GetRow(gearsetItem.ItemId)!;
int cp = 0, craftsmanship = 0, control = 0; int cp = 0,
craftsmanship = 0,
control = 0;
void IncreaseStat(uint baseParam, int amount) void IncreaseStat(uint baseParam, int amount)
{ {
@@ -83,7 +95,12 @@ public static unsafe class Gearsets
} }
public static GearsetStats CalculateGearsetStats(GearsetItem[] gearsetItems) => public static GearsetStats CalculateGearsetStats(GearsetItem[] gearsetItems) =>
gearsetItems.Select(CalculateGearsetItemStats).Aggregate(BaseStats, (a, b) => new(a.CP + b.CP, a.Craftsmanship + b.Craftsmanship, a.Control + b.Control)); gearsetItems
.Select(CalculateGearsetItemStats)
.Aggregate(
BaseStats,
(a, b) => new(a.CP + b.CP, a.Craftsmanship + b.Craftsmanship, a.Control + b.Control)
);
public static GearsetStats CalculateGearsetCurrentStats() public static GearsetStats CalculateGearsetCurrentStats()
{ {
@@ -97,10 +114,24 @@ public static unsafe class Gearsets
}; };
} }
public static CharacterStats CalculateCharacterStats(GearsetItem[] gearsetItems, int characterLevel, bool canUseManipulation) => public static CharacterStats CalculateCharacterStats(
CalculateCharacterStats(CalculateGearsetStats(gearsetItems), gearsetItems, characterLevel, canUseManipulation); GearsetItem[] gearsetItems,
int characterLevel,
bool canUseManipulation
) =>
CalculateCharacterStats(
CalculateGearsetStats(gearsetItems),
gearsetItems,
characterLevel,
canUseManipulation
);
public static CharacterStats CalculateCharacterStats(GearsetStats gearsetStats, GearsetItem[] gearsetItems, int characterLevel, bool canUseManipulation) => public static CharacterStats CalculateCharacterStats(
GearsetStats gearsetStats,
GearsetItem[] gearsetItems,
int characterLevel,
bool canUseManipulation
) =>
new() new()
{ {
CP = gearsetStats.CP, CP = gearsetStats.CP,
@@ -115,8 +146,7 @@ public static unsafe class Gearsets
public static bool HasDelineations() => public static bool HasDelineations() =>
InventoryManager.Instance()->GetInventoryItemCount(28724) > 0; InventoryManager.Instance()->GetInventoryItemCount(28724) > 0;
public static bool IsItem(GearsetItem item, uint itemId) => public static bool IsItem(GearsetItem item, uint itemId) => item.ItemId == itemId;
item.ItemId == itemId;
public static bool IsSpecialistSoulCrystal(GearsetItem item) public static bool IsSpecialistSoulCrystal(GearsetItem item)
{ {
@@ -125,11 +155,18 @@ public static unsafe class Gearsets
var luminaItem = LuminaSheets.ItemSheet.GetRow(item.ItemId)!; var luminaItem = LuminaSheets.ItemSheet.GetRow(item.ItemId)!;
// Soul Crystal ItemUICategory DoH Category // Soul Crystal ItemUICategory DoH Category
return luminaItem.ItemUICategory.RowId == 62 && luminaItem.ClassJobUse.Value.ClassJobCategory.RowId == 33; return luminaItem.ItemUICategory.RowId == 62
&& luminaItem.ClassJobUse.Value.ClassJobCategory.RowId == 33;
} }
public static bool IsSplendorousTool(GearsetItem item) => public static bool IsSplendorousTool(GearsetItem item) =>
LuminaSheets.ItemSheetEnglish.GetRow(item.ItemId).Description.ToString().Contains("Increases to quality are 1.75 times higher than normal when material condition is Good.", StringComparison.Ordinal); LuminaSheets
.ItemSheetEnglish.GetRow(item.ItemId)
.Description.ToString()
.Contains(
"Increases to quality are 1.75 times higher than normal when material condition is Good.",
StringComparison.Ordinal
);
// https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/24d0db2d9676f264edf53651b21005305267c84c/apps/client/src/app/modules/gearsets/materia.service.ts#L265 // https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/24d0db2d9676f264edf53651b21005305267c84c/apps/client/src/app/modules/gearsets/materia.service.ts#L265
private static int CalculateParamCap(Item item, uint paramId) private static int CalculateParamCap(Item item, uint paramId)
@@ -142,14 +179,14 @@ public static unsafe class Gearsets
ParamCP => ilvl.CP, ParamCP => ilvl.CP,
ParamCraftsmanship => ilvl.Craftsmanship, ParamCraftsmanship => ilvl.Craftsmanship,
ParamControl => ilvl.Control, ParamControl => ilvl.Control,
_ => 0 _ => 0,
}; };
// https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/24d0db2d9676f264edf53651b21005305267c84c/apps/data-extraction/src/extractors/items.extractor.ts#L6 // https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/24d0db2d9676f264edf53651b21005305267c84c/apps/data-extraction/src/extractors/items.extractor.ts#L6
var slotMod = item.EquipSlotCategory.RowId switch var slotMod = item.EquipSlotCategory.RowId switch
{ {
1 => param.OneHandWeaponPercent, // column 4 1 => param.OneHandWeaponPercent, // column 4
2 => param.OffHandPercent, // column 5 2 => param.OffHandPercent, // column 5
3 => param.HeadPercent, // ... 3 => param.HeadPercent, // ...
4 => param.ChestPercent, 4 => param.ChestPercent,
5 => param.HandsPercent, 5 => param.HandsPercent,
6 => param.WaistPercent, 6 => param.WaistPercent,
@@ -168,16 +205,20 @@ public static unsafe class Gearsets
19 => param.HeadChestHandsLegsFeetPercent, 19 => param.HeadChestHandsLegsFeetPercent,
20 => param.ChestLegsGlovesPercent, 20 => param.ChestLegsGlovesPercent,
21 => param.ChestLegsFeetPercent, 21 => param.ChestLegsFeetPercent,
_ => 0 _ => 0,
}; };
var roleMod = param.MeldParam[item.BaseParamModifier]; var roleMod = param.MeldParam[item.BaseParamModifier];
// https://github.com/Caraxi/SimpleTweaksPlugin/pull/595 // https://github.com/Caraxi/SimpleTweaksPlugin/pull/595
var cap = (int)Math.Round((float)baseValue * slotMod / (roleMod * 10f), MidpointRounding.AwayFromZero); var cap = (int)
Math.Round((float)baseValue * slotMod / (roleMod * 10f), MidpointRounding.AwayFromZero);
return cap == 0 ? int.MaxValue : cap; return cap == 0 ? int.MaxValue : cap;
} }
private static GearsetMateria[] GetMaterias(ReadOnlySpan<ushort> types, ReadOnlySpan<byte> grades) private static GearsetMateria[] GetMaterias(
ReadOnlySpan<ushort> types,
ReadOnlySpan<byte> grades
)
{ {
var materia = new GearsetMateria[5]; var materia = new GearsetMateria[5];
for (var i = 0; i < 5; ++i) for (var i = 0; i < 5; ++i)
+64 -12
View File
@@ -1,7 +1,7 @@
using System;
using Craftimizer.Plugin; using Craftimizer.Plugin;
using Dalamud.Hooking; using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using System;
using ActionType = Craftimizer.Simulator.Actions.ActionType; using ActionType = Craftimizer.Simulator.Actions.ActionType;
using ActionUtils = Craftimizer.Plugin.ActionUtils; using ActionUtils = Craftimizer.Plugin.ActionUtils;
using CSActionType = FFXIVClientStructs.FFXIV.Client.Game.ActionType; using CSActionType = FFXIVClientStructs.FFXIV.Client.Game.ActionType;
@@ -14,33 +14,77 @@ public sealed unsafe class Hooks : IDisposable
public event OnActionUsedDelegate? OnActionUsed; public event OnActionUsedDelegate? OnActionUsed;
public delegate bool UseActionDelegate(ActionManager* manager, CSActionType actionType, uint actionId, ulong targetId, uint extraParam, ActionManager.UseActionMode mode, uint comboRouteId, bool* outOptAreaTargeted); public delegate bool UseActionDelegate(
ActionManager* manager,
CSActionType actionType,
uint actionId,
ulong targetId,
uint extraParam,
ActionManager.UseActionMode mode,
uint comboRouteId,
bool* outOptAreaTargeted
);
public readonly Hook<UseActionDelegate> UseActionHook = null!; public readonly Hook<UseActionDelegate> UseActionHook = null!;
public delegate byte IsActionHighlightedDelegate(ActionManager* manager, CSActionType actionType, uint actionId); public delegate byte IsActionHighlightedDelegate(
ActionManager* manager,
CSActionType actionType,
uint actionId
);
public readonly Hook<IsActionHighlightedDelegate> IsActionHighlightedHook = null!; public readonly Hook<IsActionHighlightedDelegate> IsActionHighlightedHook = null!;
public Hooks() public Hooks()
{ {
UseActionHook = Service.GameInteropProvider.HookFromAddress<UseActionDelegate>((nint)ActionManager.MemberFunctionPointers.UseAction, UseActionDetour); UseActionHook = Service.GameInteropProvider.HookFromAddress<UseActionDelegate>(
IsActionHighlightedHook = Service.GameInteropProvider.HookFromAddress<IsActionHighlightedDelegate>((nint)ActionManager.MemberFunctionPointers.IsActionHighlighted, IsActionHighlightedDetour); (nint)ActionManager.MemberFunctionPointers.UseAction,
UseActionDetour
);
IsActionHighlightedHook =
Service.GameInteropProvider.HookFromAddress<IsActionHighlightedDelegate>(
(nint)ActionManager.MemberFunctionPointers.IsActionHighlighted,
IsActionHighlightedDetour
);
UseActionHook.Enable(); UseActionHook.Enable();
IsActionHighlightedHook.Enable(); IsActionHighlightedHook.Enable();
} }
private bool UseActionDetour(ActionManager* manager, CSActionType actionType, uint actionId, ulong targetId, uint extraParam, ActionManager.UseActionMode mode, uint comboRouteId, bool* optOutAreaTargeted) private bool UseActionDetour(
ActionManager* manager,
CSActionType actionType,
uint actionId,
ulong targetId,
uint extraParam,
ActionManager.UseActionMode mode,
uint comboRouteId,
bool* optOutAreaTargeted
)
{ {
var canCast = manager->GetActionStatus(actionType, actionId) == 0; var canCast = manager->GetActionStatus(actionType, actionId) == 0;
var ret = UseActionHook.Original(manager, actionType, actionId, targetId, extraParam, mode, comboRouteId, optOutAreaTargeted); var ret = UseActionHook.Original(
manager,
actionType,
actionId,
targetId,
extraParam,
mode,
comboRouteId,
optOutAreaTargeted
);
if (canCast && ret && actionType is CSActionType.CraftAction or CSActionType.Action) if (canCast && ret && actionType is CSActionType.CraftAction or CSActionType.Action)
{ {
var classJob = ClassJobUtils.GetClassJobFromIdx((byte)(Service.ClientState.LocalPlayer?.ClassJob.RowId ?? 0)); var classJob = ClassJobUtils.GetClassJobFromIdx(
(byte)(Service.Objects.LocalPlayer?.ClassJob.RowId ?? 0)
);
if (classJob != null) if (classJob != null)
{ {
var simActionType = ActionUtils.GetActionTypeFromId(actionId, classJob.Value, actionType == CSActionType.CraftAction); var simActionType = ActionUtils.GetActionTypeFromId(
actionId,
classJob.Value,
actionType == CSActionType.CraftAction
);
if (simActionType != null) if (simActionType != null)
{ {
try try
@@ -57,7 +101,11 @@ public sealed unsafe class Hooks : IDisposable
return ret; return ret;
} }
private byte IsActionHighlightedDetour(ActionManager* manager, CSActionType actionType, uint actionId) private byte IsActionHighlightedDetour(
ActionManager* manager,
CSActionType actionType,
uint actionId
)
{ {
var ret = IsActionHighlightedHook.Original(manager, actionType, actionId); var ret = IsActionHighlightedHook.Original(manager, actionType, actionId);
@@ -75,7 +123,7 @@ public sealed unsafe class Hooks : IDisposable
if (actionType is not (CSActionType.CraftAction or CSActionType.Action)) if (actionType is not (CSActionType.CraftAction or CSActionType.Action))
return ret; return ret;
var jobId = Service.ClientState.LocalPlayer?.ClassJob.RowId; var jobId = Service.Objects.LocalPlayer?.ClassJob.RowId;
if (jobId == null) if (jobId == null)
return ret; return ret;
@@ -83,7 +131,11 @@ public sealed unsafe class Hooks : IDisposable
if (classJob == null) if (classJob == null)
return ret; return ret;
var simActionType = ActionUtils.GetActionTypeFromId(actionId, classJob.Value, actionType == CSActionType.CraftAction); var simActionType = ActionUtils.GetActionTypeFromId(
actionId,
classJob.Value,
actionType == CSActionType.CraftAction
);
if (simActionType == null) if (simActionType == null)
return ret; return ret;
+11 -7
View File
@@ -1,14 +1,14 @@
using Craftimizer.Plugin;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Utility;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Craftimizer.Plugin;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Utility;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
@@ -52,7 +52,8 @@ public sealed class IconManager : IDisposable
return null; return null;
} }
public IDalamudTextureWrap GetWrapOrEmpty() => GetWrap() ?? Service.DalamudAssetManager.Empty4X4; public IDalamudTextureWrap GetWrapOrEmpty() =>
GetWrap() ?? Service.DalamudAssetManager.Empty4X4;
public void Dispose() public void Dispose()
{ {
@@ -85,7 +86,10 @@ public sealed class IconManager : IDisposable
Service.TextureProvider.GetFromGameIcon(new GameIconLookup(id, itemHq: isHq)); Service.TextureProvider.GetFromGameIcon(new GameIconLookup(id, itemHq: isHq));
private static ISharedImmediateTexture GetAssemblyTextureInternal(string filename) => private static ISharedImmediateTexture GetAssemblyTextureInternal(string filename) =>
Service.TextureProvider.GetFromManifestResource(Assembly.GetExecutingAssembly(), $"Craftimizer.{filename}"); Service.TextureProvider.GetFromManifestResource(
Assembly.GetExecutingAssembly(),
$"Craftimizer.{filename}"
);
public static ILoadedTextureIcon GetIcon(uint id, bool isHq = false) => public static ILoadedTextureIcon GetIcon(uint id, bool isHq = false) =>
new LoadedIcon(GetIconInternal(id, isHq)); new LoadedIcon(GetIconInternal(id, isHq));
+20 -8
View File
@@ -1,10 +1,10 @@
using Craftimizer.Plugin;
using Dalamud.Plugin;
using System; using System;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using DotNext.Reflection; using Craftimizer.Plugin;
using Dalamud.Plugin;
using DotNext.Collections.Generic; using DotNext.Collections.Generic;
using DotNext.Reflection;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
@@ -26,7 +26,7 @@ public sealed class Ipc
if (prop.GetMethod is not { } getMethod) if (prop.GetMethod is not { } getMethod)
throw new InvalidOperationException("Property must have a getter"); throw new InvalidOperationException("Property must have a getter");
if (getMethod.GetCustomAttribute<CompilerGeneratedAttribute>() is null) if (!getMethod.IsDefined<CompilerGeneratedAttribute>())
throw new InvalidOperationException("Property must have an auto getter"); throw new InvalidOperationException("Property must have an auto getter");
var type = prop.PropertyType; var type = prop.PropertyType;
@@ -39,16 +39,27 @@ public sealed class Ipc
var returnsVoid = typeMethod.ReturnType == typeof(void); var returnsVoid = typeMethod.ReturnType == typeof(void);
var propSubscriber = typeof(IDalamudPluginInterface).GetMethod("GetIpcSubscriber", typeMethod.GetParameters().Length + 1, [typeof(string)]); var propSubscriber = typeof(IDalamudPluginInterface).GetMethod(
"GetIpcSubscriber",
typeMethod.GetParameters().Length + 1,
[typeof(string)]
);
if (propSubscriber is null) if (propSubscriber is null)
throw new InvalidOperationException("GetIpcSubscriber method not found"); throw new InvalidOperationException("GetIpcSubscriber method not found");
var callGateSubscriber = propSubscriber.MakeGenericMethod([.. typeMethod.GetParameterTypes(), returnsVoid ? typeof(int) : typeMethod.ReturnType]).Invoke(Service.PluginInterface, [attr.Name ?? prop.Name]); var callGateSubscriber = propSubscriber
.MakeGenericMethod([
.. typeMethod.GetParameterTypes(),
returnsVoid ? typeof(int) : typeMethod.ReturnType,
])
.Invoke(Service.PluginInterface, [attr.Name ?? prop.Name]);
if (callGateSubscriber is null) if (callGateSubscriber is null)
throw new InvalidOperationException("CallGateSubscriber is null"); throw new InvalidOperationException("CallGateSubscriber is null");
var invokeFunc = callGateSubscriber.GetType().GetMethod(returnsVoid ? "InvokeAction" : "InvokeFunc"); var invokeFunc = callGateSubscriber
.GetType()
.GetMethod(returnsVoid ? "InvokeAction" : "InvokeFunc");
if (invokeFunc is null) if (invokeFunc is null)
throw new InvalidOperationException("Subscriber Invoke method not found"); throw new InvalidOperationException("Subscriber Invoke method not found");
@@ -62,7 +73,8 @@ public sealed class Ipc
public Func<bool> MacroMateIsAvailable { get; private set; } = null!; public Func<bool> MacroMateIsAvailable { get; private set; } = null!;
[IPCCall("MacroMate.CreateOrUpdateMacro")] [IPCCall("MacroMate.CreateOrUpdateMacro")]
public Func<string, string, string?, uint?, bool> MacroMateCreateMacro { get; private set; } = null!; public Func<string, string, string?, uint?, bool> MacroMateCreateMacro { get; private set; } =
null!;
[IPCCall("MacroMate.ValidateGroupPath")] [IPCCall("MacroMate.ValidateGroupPath")]
public Func<string, (bool, string?)> MacroMateValidateGroupPath { get; private set; } = null!; public Func<string, (bool, string?)> MacroMateValidateGroupPath { get; private set; } = null!;
+2 -1
View File
@@ -1,5 +1,5 @@
using Craftimizer.Plugin;
using System; using System;
using Craftimizer.Plugin;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
@@ -8,5 +8,6 @@ public static class Log
public static void Debug(string line) => Service.PluginLog.Debug(line); public static void Debug(string line) => Service.PluginLog.Debug(line);
public static void Error(string line) => Service.PluginLog.Error(line); public static void Error(string line) => Service.PluginLog.Error(line);
public static void Error(Exception e, string line) => Service.PluginLog.Error(e, line); public static void Error(Exception e, string line) => Service.PluginLog.Error(e, line);
} }
+95 -58
View File
@@ -1,13 +1,13 @@
using System;
using System.Collections.Generic;
using Craftimizer.Plugin; using Craftimizer.Plugin;
using Craftimizer.Simulator; using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions; using Craftimizer.Simulator.Actions;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using Dalamud.Bindings.ImGui;
using System;
using System.Collections.Generic;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
@@ -20,13 +20,15 @@ public static class MacroCopy
{ {
if (actions.Count == 0) if (actions.Count == 0)
{ {
Service.Plugin.DisplayNotification(new() Plugin.Plugin.DisplayNotification(
{ new()
Content = "Cannot copy an empty macro.", {
MinimizedText = "Cannot copy empty macro", Content = "Cannot copy an empty macro.",
Title = "Macro Not Copied", MinimizedText = "Cannot copy empty macro",
Type = NotificationType.Error Title = "Macro Not Copied",
}); Type = NotificationType.Error,
}
);
return; return;
} }
@@ -49,9 +51,14 @@ public static class MacroCopy
} }
} }
private static List<string> GetMacros(IReadOnlyList<ActionType> actions, MacroCopyConfiguration config) private static List<string> GetMacros(
IReadOnlyList<ActionType> actions,
MacroCopyConfiguration config
)
{ {
var mustSplit = (config.Type == MacroCopyConfiguration.CopyType.CopyToMacro || !config.CombineMacro) && config.Type != MacroCopyConfiguration.CopyType.CopyToMacroMate; var mustSplit =
(config.Type == MacroCopyConfiguration.CopyType.CopyToMacro || !config.CombineMacro)
&& config.Type != MacroCopyConfiguration.CopyType.CopyToMacroMate;
var macros = new List<string>(); var macros = new List<string>();
@@ -143,41 +150,54 @@ public static class MacroCopy
{ {
var config = Service.Configuration.MacroCopy; var config = Service.Configuration.MacroCopy;
int i, macroIdx; int i,
macroIdx;
for ( for (
i = 0, macroIdx = config.StartMacroIdx; i = 0, macroIdx = config.StartMacroIdx;
i < macros.Count && i < config.MaxMacroCount && macroIdx < 100; i < macros.Count && i < config.MaxMacroCount && macroIdx < 100;
i++, macroIdx += config.CopyDown ? 10 : 1) i++, macroIdx += config.CopyDown ? 10 : 1
)
SetMacro(macroIdx, config.SharedMacro, macros[i], i + 1); SetMacro(macroIdx, config.SharedMacro, macros[i], i + 1);
if (config.ShowCopiedMessage) if (config.ShowCopiedMessage)
{ {
Service.Plugin.DisplayNotification(new() Plugin.Plugin.DisplayNotification(
{ new()
Content = i > 1 ? "Copied macro to User Macros." : $"Copied {i} macros to User Macros.", {
MinimizedText = i > 1 ? "Copied macro" : $"Copied {i} macros", Content =
Title = "Macro Copied", i > 1
Type = NotificationType.Success ? "Copied macro to User Macros."
}); : $"Copied {i} macros to User Macros.",
MinimizedText = i > 1 ? "Copied macro" : $"Copied {i} macros",
Title = "Macro Copied",
Type = NotificationType.Success,
}
);
} }
if (i < macros.Count) if (i < macros.Count)
{ {
Service.Plugin.OpenMacroClipboard(macros); Service.Plugin.OpenMacroClipboard(macros);
var rest = macros.Count - i; var rest = macros.Count - i;
Service.Plugin.DisplayNotification(new() Plugin.Plugin.DisplayNotification(
{ new()
Content = $"Couldn't copy {rest} macro{(rest == 1 ? "" : "s")}, so a window was opened with all of them.", {
Minimized = false, Content =
Title = "Macro Copied", $"Couldn't copy {rest} macro{(rest == 1 ? "" : "s")}, so a window was opened with all of them.",
Type = NotificationType.Warning Minimized = false,
}); Title = "Macro Copied",
Type = NotificationType.Warning,
}
);
} }
} }
private static unsafe void SetMacro(int idx, bool isShared, string macroText, int macroIdx) private static unsafe void SetMacro(int idx, bool isShared, string macroText, int macroIdx)
{ {
if (idx >= 100 || idx < 0) if (idx >= 100 || idx < 0)
throw new ArgumentOutOfRangeException(nameof(idx), "Macro index must be between 0 and 99"); throw new ArgumentOutOfRangeException(
nameof(idx),
"Macro index must be between 0 and 99"
);
var set = isShared ? 1u : 0u; var set = isShared ? 1u : 0u;
@@ -201,13 +221,19 @@ public static class MacroCopy
ImGui.SetClipboardText(string.Join(Environment.NewLine + Environment.NewLine, macros)); ImGui.SetClipboardText(string.Join(Environment.NewLine + Environment.NewLine, macros));
if (Service.Configuration.MacroCopy.ShowCopiedMessage) if (Service.Configuration.MacroCopy.ShowCopiedMessage)
{ {
Service.Plugin.DisplayNotification(new() Plugin.Plugin.DisplayNotification(
{ new()
Content = macros.Count == 1 ? "Copied macro to clipboard." : $"Copied {macros.Count} macros to clipboard.", {
MinimizedText = macros.Count == 1 ? "Copied macro" : $"Copied {macros.Count} macros", Content =
Title = "Macro Copied", macros.Count == 1
Type = NotificationType.Success ? "Copied macro to clipboard."
}); : $"Copied {macros.Count} macros to clipboard.",
MinimizedText =
macros.Count == 1 ? "Copied macro" : $"Copied {macros.Count} macros",
Title = "Macro Copied",
Type = NotificationType.Success,
}
);
} }
} }
@@ -215,13 +241,15 @@ public static class MacroCopy
{ {
if (!Service.Ipc.MacroMateIsAvailable()) if (!Service.Ipc.MacroMateIsAvailable())
{ {
Service.Plugin.DisplayNotification(new() Plugin.Plugin.DisplayNotification(
{ new()
Content = "Please check if it installed and enabled.", {
MinimizedText = "Macro Mate is unavailable", Content = "Please check if it installed and enabled.",
Title = "Macro Mate Unavailable", MinimizedText = "Macro Mate is unavailable",
Type = NotificationType.Error Title = "Macro Mate Unavailable",
}); Type = NotificationType.Error,
}
);
return; return;
} }
@@ -232,27 +260,36 @@ public static class MacroCopy
var (isValidParent, parentError) = Service.Ipc.MacroMateValidateGroupPath(parentPath); var (isValidParent, parentError) = Service.Ipc.MacroMateValidateGroupPath(parentPath);
if (!isValidParent) if (!isValidParent)
{ {
Service.Plugin.DisplayNotification(new() Plugin.Plugin.DisplayNotification(
{ new()
Content = parentError!, {
MinimizedText = parentError, Content = parentError!,
Title = "Macro Mate Invalid Parent", MinimizedText = parentError,
Type = NotificationType.Error Title = "Macro Mate Invalid Parent",
}); Type = NotificationType.Error,
}
);
return; return;
} }
Service.Ipc.MacroMateCreateMacro(Service.Configuration.MacroCopy.MacroMateName, macro, parentPath, null); Service.Ipc.MacroMateCreateMacro(
Service.Configuration.MacroCopy.MacroMateName,
macro,
parentPath,
null
);
if (Service.Configuration.MacroCopy.ShowCopiedMessage) if (Service.Configuration.MacroCopy.ShowCopiedMessage)
{ {
Service.Plugin.DisplayNotification(new() Plugin.Plugin.DisplayNotification(
{ new()
Content = "Copied macro to Macro Mate.", {
MinimizedText = "Copied macro", Content = "Copied macro to Macro Mate.",
Title = "Macro Copied", MinimizedText = "Copied macro",
Type = NotificationType.Success Title = "Macro Copied",
}); Type = NotificationType.Success,
}
);
} }
} }
} }
+53 -28
View File
@@ -1,7 +1,3 @@
using Craftimizer.Plugin;
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Dalamud.Networking.Http;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@@ -12,6 +8,10 @@ using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Craftimizer.Plugin;
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Dalamud.Networking.Http;
using static Craftimizer.Utils.CommunityMacros; using static Craftimizer.Utils.CommunityMacros;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
@@ -69,7 +69,10 @@ public static class MacroImport
if (!Uri.TryCreate(url, UriKind.Absolute, out uri!)) if (!Uri.TryCreate(url, UriKind.Absolute, out uri!))
return false; return false;
if (!string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) if (
!string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)
&& !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)
)
return false; return false;
if (!uri.IsDefaultPort) if (!uri.IsDefaultPort)
@@ -87,34 +90,46 @@ public static class MacroImport
{ {
"ffxivteamcraft.com" => RetrieveTeamcraftUrl(uri, token), "ffxivteamcraft.com" => RetrieveTeamcraftUrl(uri, token),
"craftingway.app" => RetrieveCraftingwayUrl(uri, token), "craftingway.app" => RetrieveCraftingwayUrl(uri, token),
_ => throw new UnreachableException("TryParseUrl should handle miscellaneous edge cases"), _ => throw new UnreachableException(
"TryParseUrl should handle miscellaneous edge cases"
),
}; };
} }
private static async Task<CommunityMacro> RetrieveTeamcraftUrl(Uri uri, CancellationToken token) private static async Task<CommunityMacro> RetrieveTeamcraftUrl(Uri uri, CancellationToken token)
{ {
using var heCallback = new HappyEyeballsCallback(); using var heCallback = new HappyEyeballsCallback();
using var client = new HttpClient(new SocketsHttpHandler using var client = new HttpClient(
{ new SocketsHttpHandler
AutomaticDecompression = DecompressionMethods.All, {
ConnectCallback = heCallback.ConnectCallback, AutomaticDecompression = DecompressionMethods.All,
}); ConnectCallback = heCallback.ConnectCallback,
}
);
var path = uri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped); var path = uri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped);
if (!path.StartsWith("simulator/", StringComparison.Ordinal)) if (!path.StartsWith("simulator/", StringComparison.Ordinal))
throw new ArgumentException("Teamcraft macro url should start with /simulator", nameof(uri)); throw new ArgumentException(
"Teamcraft macro url should start with /simulator",
nameof(uri)
);
path = path[10..]; path = path[10..];
var lastSlash = path.LastIndexOf('/'); var lastSlash = path.LastIndexOf('/');
if (lastSlash == -1) if (lastSlash == -1)
throw new ArgumentException("Teamcraft macro url is not in the right format", nameof(uri)); throw new ArgumentException(
"Teamcraft macro url is not in the right format",
nameof(uri)
);
var id = path[(lastSlash + 1)..]; var id = path[(lastSlash + 1)..];
var resp = await client.GetFromJsonAsync<TeamcraftMacro>( var resp = await client
$"https://firestore.googleapis.com/v1beta1/projects/ffxivteamcraft/databases/(default)/documents/rotations/{id}", .GetFromJsonAsync<TeamcraftMacro>(
token). $"https://firestore.googleapis.com/v1beta1/projects/ffxivteamcraft/databases/(default)/documents/rotations/{id}",
ConfigureAwait(false); token
)
.ConfigureAwait(false);
if (resp is null) if (resp is null)
throw new Exception("Internal error; failed to retrieve macro"); throw new Exception("Internal error; failed to retrieve macro");
if (resp.Error is { } error) if (resp.Error is { } error)
@@ -122,31 +137,41 @@ public static class MacroImport
return new(resp); return new(resp);
} }
private static async Task<CommunityMacro> RetrieveCraftingwayUrl(Uri uri, CancellationToken token) private static async Task<CommunityMacro> RetrieveCraftingwayUrl(
Uri uri,
CancellationToken token
)
{ {
using var heCallback = new HappyEyeballsCallback(); using var heCallback = new HappyEyeballsCallback();
using var client = new HttpClient(new SocketsHttpHandler using var client = new HttpClient(
{ new SocketsHttpHandler
AutomaticDecompression = DecompressionMethods.All, {
ConnectCallback = heCallback.ConnectCallback, AutomaticDecompression = DecompressionMethods.All,
}); ConnectCallback = heCallback.ConnectCallback,
}
);
// https://craftingway.app/rotation/variable-blueprint-KmrvS // https://craftingway.app/rotation/variable-blueprint-KmrvS
var path = uri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped); var path = uri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped);
if (!path.StartsWith("rotation/", StringComparison.Ordinal)) if (!path.StartsWith("rotation/", StringComparison.Ordinal))
throw new ArgumentException("Craftingway macro url should start with /rotation", nameof(uri)); throw new ArgumentException(
"Craftingway macro url should start with /rotation",
nameof(uri)
);
path = path[9..]; path = path[9..];
var lastSlash = path.LastIndexOf('/'); var lastSlash = path.LastIndexOf('/');
if (lastSlash != -1) if (lastSlash != -1)
throw new ArgumentException("Craftingway macro url is not in the right format", nameof(uri)); throw new ArgumentException(
"Craftingway macro url is not in the right format",
nameof(uri)
);
var id = path; var id = path;
var resp = await client.GetFromJsonAsync<CraftingwayMacro>( var resp = await client
$"https://servingway.fly.dev/rotation/{id}", .GetFromJsonAsync<CraftingwayMacro>($"https://servingway.fly.dev/rotation/{id}", token)
token)
.ConfigureAwait(false); .ConfigureAwait(false);
if (resp is null) if (resp is null)
throw new Exception("Internal error; failed to retrieve macro"); throw new Exception("Internal error; failed to retrieve macro");
+56 -18
View File
@@ -1,9 +1,9 @@
using Craftimizer.Plugin;
using Craftimizer.Simulator;
using Lumina.Excel.Sheets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Craftimizer.Plugin;
using Craftimizer.Simulator;
using Lumina.Excel.Sheets;
using ClassJob = Craftimizer.Simulator.ClassJob; using ClassJob = Craftimizer.Simulator.ClassJob;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
@@ -28,16 +28,22 @@ public sealed record RecipeData
{ {
RecipeId = recipeId; RecipeId = recipeId;
Recipe = LuminaSheets.RecipeSheet.GetRowOrDefault(recipeId) ?? Recipe =
throw new ArgumentException($"Invalid recipe id {recipeId}", nameof(recipeId)); LuminaSheets.RecipeSheet.GetRowOrDefault(recipeId)
?? throw new ArgumentException($"Invalid recipe id {recipeId}", nameof(recipeId));
ClassJob = (ClassJob)Recipe.CraftType.RowId; ClassJob = (ClassJob)Recipe.CraftType.RowId;
var resolvedLevelTableRow = Recipe.RecipeLevelTable.RowId; var resolvedLevelTableRow = Recipe.RecipeLevelTable.RowId;
if (Recipe.MaxAdjustableJobLevel.RowId != 0) if (Recipe.MaxAdjustableJobLevel.RowId != 0)
{ {
AdjustedJobLevel = Math.Min(explicitlyAdjustedJobLevel ?? ClassJob.GetWKSSyncedLevel(), (ushort)Recipe.MaxAdjustableJobLevel.RowId); AdjustedJobLevel = Math.Min(
resolvedLevelTableRow = LuminaSheets.GathererCrafterLvAdjustTableSheet.GetRow(AdjustedJobLevel.Value).RecipeLevel.RowId; explicitlyAdjustedJobLevel ?? ClassJob.GetWKSSyncedLevel(),
(ushort)Recipe.MaxAdjustableJobLevel.RowId
);
resolvedLevelTableRow = LuminaSheets
.GathererCrafterLvAdjustTableSheet.GetRow(AdjustedJobLevel.Value)
.RecipeLevel.RowId;
} }
Table = LuminaSheets.RecipeLevelTableSheet.GetRow(resolvedLevelTableRow); Table = LuminaSheets.RecipeLevelTableSheet.GetRow(resolvedLevelTableRow);
@@ -46,8 +52,14 @@ public sealed record RecipeData
IsExpert = Recipe.IsExpert, IsExpert = Recipe.IsExpert,
ClassJobLevel = Table.ClassJobLevel, ClassJobLevel = Table.ClassJobLevel,
ConditionsFlag = Table.ConditionsFlag, ConditionsFlag = Table.ConditionsFlag,
MaxDurability = (Recipe.MaxAdjustableJobLevel.RowId != 0 ? 80 : Table.Durability) * Recipe.DurabilityFactor / 100, MaxDurability =
MaxQuality = (Recipe.CanHq || Recipe.IsExpert) ? (int)Table.Quality * Recipe.QualityFactor / 100 : 0, (Recipe.MaxAdjustableJobLevel.RowId != 0 ? 80 : Table.Durability)
* Recipe.DurabilityFactor
/ 100,
MaxQuality =
(Recipe.CanHq || Recipe.RequiredQuality > 0)
? (int)Table.Quality * Recipe.QualityFactor / 100
: 0,
MaxProgress = Table.Difficulty * Recipe.DifficultyFactor / 100, MaxProgress = Table.Difficulty * Recipe.DifficultyFactor / 100,
QualityModifier = Table.QualityModifier, QualityModifier = Table.QualityModifier,
QualityDivider = Table.QualityDivider, QualityDivider = Table.QualityDivider,
@@ -64,23 +76,37 @@ public sealed record RecipeData
{ {
if (entry.ItemTradeIn.RowId == Recipe.ItemResult.RowId) if (entry.ItemTradeIn.RowId == Recipe.ItemResult.RowId)
{ {
thresholds = [entry.BaseCollectableRating, entry.MidCollectableRating, entry.HighCollectableRating]; thresholds =
[
entry.BaseCollectableRating,
entry.MidCollectableRating,
entry.HighCollectableRating,
];
break; break;
} }
} }
} }
else if (Recipe.CollectableMetadata.GetValueOrDefaultSubrow<SatisfactionSupply>() is { } row3) else if (
Recipe.CollectableMetadata.GetValueOrDefaultSubrow<SatisfactionSupply>() is { } row3
)
{ {
foreach (var subrow in row3) foreach (var subrow in row3)
{ {
if (subrow.Item.RowId == Recipe.ItemResult.RowId) if (subrow.Item.RowId == Recipe.ItemResult.RowId)
{ {
thresholds = [subrow.CollectabilityLow, subrow.CollectabilityMid, subrow.CollectabilityHigh]; thresholds =
[
subrow.CollectabilityLow,
subrow.CollectabilityMid,
subrow.CollectabilityHigh,
];
break; break;
} }
} }
} }
else if (Recipe.CollectableMetadata.GetValueOrDefault<SharlayanCraftWorksSupply>() is { } row5) else if (
Recipe.CollectableMetadata.GetValueOrDefault<SharlayanCraftWorksSupply>() is { } row5
)
{ {
foreach (var item in row5.Item) foreach (var item in row5.Item)
{ {
@@ -93,10 +119,19 @@ public sealed record RecipeData
} }
else if (Recipe.CollectableMetadata.GetValueOrDefault<CollectablesRefine>() is { } row6) else if (Recipe.CollectableMetadata.GetValueOrDefault<CollectablesRefine>() is { } row6)
thresholds = [row6.CollectabilityLow, row6.CollectabilityMid, row6.CollectabilityHigh]; thresholds = [row6.CollectabilityLow, row6.CollectabilityMid, row6.CollectabilityHigh];
else if (Recipe.CollectableMetadataKey == 7 && LuminaSheets.WKSMissionToDoEvalutionRefinSheet.TryGetRow(Recipe.CollectableMetadata.RowId, out var row7)) else if (
Recipe.CollectableMetadataKey == 7
&& LuminaSheets.WKSMissionToDoEvalutionRefinSheet.TryGetRow(
Recipe.CollectableMetadata.RowId,
out var row7
)
)
{ {
thresholds = [row7.Unknown0, row7.Unknown1, row7.Unknown2]; thresholds = [row7.Unknown0, row7.Unknown1, row7.Unknown2];
thresholds = [.. thresholds.Select(percentage => RecipeInfo.MaxQuality * percentage / 1000)]; thresholds =
[
.. thresholds.Select(percentage => RecipeInfo.MaxQuality * percentage / 1000),
];
} }
if (thresholds != null) if (thresholds != null)
@@ -106,14 +141,17 @@ public sealed record RecipeData
CollectableThresholds = t.ToArray(); CollectableThresholds = t.ToArray();
} }
Ingredients = Recipe.Ingredient.Zip(Recipe.AmountIngredient) Ingredients = Recipe
.Ingredient.Zip(Recipe.AmountIngredient)
.Take(6) .Take(6)
.Where(i => i.First.IsValid) .Where(i => i.First.IsValid)
.Select(i => (i.First.Value, (int)i.Second)) .Select(i => (i.First.Value, (int)i.Second))
.ToList(); .ToList();
MaxStartingQuality = (int)Math.Floor(Recipe.MaterialQualityFactor * RecipeInfo.MaxQuality / 100f); MaxStartingQuality = (int)
Math.Floor(Recipe.MaterialQualityFactor * RecipeInfo.MaxQuality / 100f);
TotalHqILvls = (int)Ingredients.Where(i => i.Item.CanBeHq).Sum(i => i.Item.LevelItem.RowId * i.Amount); TotalHqILvls = (int)
Ingredients.Where(i => i.Item.CanBeHq).Sum(i => i.Item.LevelItem.RowId * i.Amount);
} }
public int CalculateItemStartingQuality(int itemIdx, int amount) public int CalculateItemStartingQuality(int itemIdx, int amount)
+42 -23
View File
@@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Craftimizer.Plugin; using Craftimizer.Plugin;
using Craftimizer.Simulator; using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions; using Craftimizer.Simulator.Actions;
using DotNext.Collections.Generic; using DotNext.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using Sim = Craftimizer.Simulator.Simulator; using Sim = Craftimizer.Simulator.Simulator;
using SimNoRandom = Craftimizer.Simulator.SimulatorNoRandom; using SimNoRandom = Craftimizer.Simulator.SimulatorNoRandom;
@@ -51,11 +51,11 @@ internal sealed class SimulatedMacro
Average = (float)DataList.Average(); Average = (float)DataList.Average();
} }
public ImGuiUtils.ViolinData? GetViolinData(float barMax, int resolution, double bandwidth) => public ImGuiUtils.ViolinData? GetViolinData(
ViolinData ??= float barMax,
Min != Max ? int resolution,
new(DataList, 0, barMax, resolution, bandwidth) : double bandwidth
null; ) => ViolinData ??= Min != Max ? new(DataList, 0, barMax, resolution, bandwidth) : null;
} }
public readonly Param Progress = new(); public readonly Param Progress = new();
@@ -64,7 +64,12 @@ internal sealed class SimulatedMacro
// Param is either collectability, quality, or hq%, depending on the recipe // Param is either collectability, quality, or hq%, depending on the recipe
public readonly Param ParamScore = new(); public readonly Param ParamScore = new();
public Reliablity(in SimulationState startState, IEnumerable<ActionType> actions, int iterCount, RecipeData recipeData) public Reliablity(
in SimulationState startState,
IEnumerable<ActionType> actions,
int iterCount,
RecipeData recipeData
)
{ {
Func<SimulationState, int> getParam; Func<SimulationState, int> getParam;
if (recipeData.IsCollectable) if (recipeData.IsCollectable)
@@ -97,12 +102,18 @@ internal sealed class SimulatedMacro
{ {
public ActionType Action { get; } public ActionType Action { get; }
public bool IsEphemeral { get; } public bool IsEphemeral { get; }
// State *after* executing the action // State *after* executing the action
public ActionResponse Response { get; private set; } public ActionResponse Response { get; private set; }
public SimulationState State { get; private set; } public SimulationState State { get; private set; }
private Reliablity? Reliability { get; set; } private Reliablity? Reliability { get; set; }
public Step(ActionType action, Sim sim, in SimulationState lastState, out SimulationState newState) public Step(
ActionType action,
Sim sim,
in SimulationState lastState,
out SimulationState newState
)
{ {
Action = action; Action = action;
newState = Recalculate(sim, lastState); newState = Recalculate(sim, lastState);
@@ -122,9 +133,17 @@ internal sealed class SimulatedMacro
return State; return State;
} }
public Reliablity GetReliability(in SimulationState initialState, IEnumerable<ActionType> actionSet, RecipeData recipeData) => public Reliablity GetReliability(
Reliability ??= in SimulationState initialState,
new(initialState, actionSet, Service.Configuration.ReliabilitySimulationCount, recipeData); IEnumerable<ActionType> actionSet,
RecipeData recipeData
) =>
Reliability ??= new(
initialState,
actionSet,
Service.Configuration.ReliabilitySimulationCount,
recipeData
);
}; };
private List<Step> Macro { get; set; } = []; private List<Step> Macro { get; set; } = [];
@@ -162,9 +181,9 @@ internal sealed class SimulatedMacro
} }
public Reliablity GetReliability(RecipeData recipeData, Index? idx = null) => public Reliablity GetReliability(RecipeData recipeData, Index? idx = null) =>
Macro.Count > 0 ? Macro.Count > 0
Macro[idx ?? ^1].GetReliability(InitialState, Actions.ToArray(), recipeData) : ? Macro[idx ?? ^1].GetReliability(InitialState, Actions.ToArray(), recipeData)
new(InitialState, Array.Empty<ActionType>(), 0, recipeData); : new(InitialState, Array.Empty<ActionType>(), 0, recipeData);
private void TryRecalculateFrom(int index) private void TryRecalculateFrom(int index)
{ {
@@ -177,14 +196,11 @@ internal sealed class SimulatedMacro
state = Macro[i].Recalculate(sim, state); state = Macro[i].Recalculate(sim, state);
} }
public void RecalculateState() => public void RecalculateState() => TryRecalculateFrom(0);
TryRecalculateFrom(0);
public void RemoveRange(int index, int count) => public void RemoveRange(int index, int count) => Macro.RemoveRange(index, count);
Macro.RemoveRange(index, count);
public void Clear() => public void Clear() => Macro.Clear();
Macro.Clear();
public void Add(ActionType action) public void Add(ActionType action)
{ {
@@ -247,7 +263,10 @@ internal sealed class SimulatedMacro
QueuedEphemeralSteps.Clear(); QueuedEphemeralSteps.Clear();
foreach (var action in actions) foreach (var action in actions)
{ {
if (maxSize is { } size && QueuedSteps.Count + QueuedEphemeralSteps.Count + Macro.Count >= size) if (
maxSize is { } size
&& QueuedSteps.Count + QueuedEphemeralSteps.Count + Macro.Count >= size
)
return size; return size;
QueuedEphemeralSteps.Add(new(action, true)); QueuedEphemeralSteps.Add(new(action, true));
+9 -5
View File
@@ -1,8 +1,8 @@
using Dalamud.Game.Text;
using System; using System;
using System.Collections.Frozen; using System.Collections.Frozen;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using Dalamud.Game.Text;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
@@ -10,7 +10,10 @@ public static class SqText
{ {
public static SeIconChar LevelPrefix => SeIconChar.LevelEn; public static SeIconChar LevelPrefix => SeIconChar.LevelEn;
public static readonly FrozenDictionary<char, SeIconChar> LevelNumReplacements = new Dictionary<char, SeIconChar> public static readonly FrozenDictionary<char, SeIconChar> LevelNumReplacements = new Dictionary<
char,
SeIconChar
>
{ {
['0'] = SeIconChar.Number0, ['0'] = SeIconChar.Number0,
['1'] = SeIconChar.Number1, ['1'] = SeIconChar.Number1,
@@ -24,17 +27,18 @@ public static class SqText
['9'] = SeIconChar.Number9, ['9'] = SeIconChar.Number9,
}.ToFrozenDictionary(); }.ToFrozenDictionary();
public static string ToLevelString<T>(T value) where T : IBinaryInteger<T> public static string ToLevelString<T>(T value)
where T : IBinaryInteger<T>
{ {
var str = value.ToString() ?? throw new FormatException("Failed to format value"); var str = value.ToString() ?? throw new FormatException("Failed to format value");
foreach(var (k, v) in LevelNumReplacements) foreach (var (k, v) in LevelNumReplacements)
str = str.Replace(k, v.ToIconChar()); str = str.Replace(k, v.ToIconChar());
return str; return str;
} }
public static bool TryParseLevelString(string str, out int result) public static bool TryParseLevelString(string str, out int result)
{ {
foreach(var (k, v) in LevelNumReplacements) foreach (var (k, v) in LevelNumReplacements)
str = str.Replace(v.ToIconChar(), k); str = str.Replace(v.ToIconChar(), k);
return int.TryParse(str, out result); return int.TryParse(str, out result);
} }
+10 -13
View File
@@ -1,10 +1,10 @@
using System;
using Craftimizer.Simulator; using Craftimizer.Simulator;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory; using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using System; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace Craftimizer.Utils; namespace Craftimizer.Utils;
@@ -12,7 +12,8 @@ internal sealed unsafe class SynthesisValues(AddonSynthesis* addon)
{ {
private AddonSynthesis* Addon { get; } = addon; private AddonSynthesis* Addon { get; } = addon;
private ReadOnlySpan<AtkValue> Values => new(Addon->AtkUnitBase.AtkValues, Addon->AtkUnitBase.AtkValuesCount); private ReadOnlySpan<AtkValue> Values =>
new(Addon->AtkUnitBase.AtkValues, Addon->AtkUnitBase.AtkValuesCount);
// Always 0? // Always 0?
private uint IsInitializing => GetUInt(0); private uint IsInitializing => GetUInt(0);
@@ -51,9 +52,7 @@ internal sealed unsafe class SynthesisValues(AddonSynthesis* addon)
if (Addon == null) if (Addon == null)
return null; return null;
var value = Values[i]; var value = Values[i];
return value.Type == ValueType.UInt ? return value.Type == ValueType.UInt ? value.UInt : null;
value.UInt :
null;
} }
private bool? TryGetBool(int i) private bool? TryGetBool(int i)
@@ -61,9 +60,7 @@ internal sealed unsafe class SynthesisValues(AddonSynthesis* addon)
if (Addon == null) if (Addon == null)
return null; return null;
var value = Values[i]; var value = Values[i];
return value.Type == ValueType.Bool ? return value.Type == ValueType.Bool ? value.Byte != 0 : null;
value.Byte != 0 :
null;
} }
private SeString? TryGetString(int i) private SeString? TryGetString(int i)
@@ -73,10 +70,10 @@ internal sealed unsafe class SynthesisValues(AddonSynthesis* addon)
var value = Values[i]; var value = Values[i];
return value.Type switch return value.Type switch
{ {
ValueType.ManagedString or ValueType.ManagedString or ValueType.String => MemoryHelper.ReadSeStringNullTerminated(
ValueType.String => (nint)value.String.Value
MemoryHelper.ReadSeStringNullTerminated((nint)value.String.Value), ),
_ => null _ => null,
}; };
} }
+47 -19
View File
@@ -1,13 +1,13 @@
using Craftimizer.Plugin;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Bindings.ImGui;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics;
using System.Linq; using System.Linq;
using System.Numerics;
using Craftimizer.Plugin;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
namespace Craftimizer.Windows; namespace Craftimizer.Windows;
@@ -17,7 +17,8 @@ public sealed class MacroClipboard : Window, IDisposable
private List<string> Macros { get; } private List<string> Macros { get; }
public MacroClipboard(IEnumerable<string> macros) : base("Macro Clipboard", WindowFlags) public MacroClipboard(IEnumerable<string> macros)
: base("Macro Clipboard", WindowFlags)
{ {
Macros = [.. macros]; Macros = [.. macros];
@@ -39,32 +40,49 @@ public sealed class MacroClipboard : Window, IDisposable
private void DrawMacro(int idx, string macro) private void DrawMacro(int idx, string macro)
{ {
using var id = ImRaii.PushId(idx); using var id = ImRaii.PushId(idx);
using var panel = ImRaii2.GroupPanel(Macros.Count == 1 ? "Macro" : $"Macro {idx + 1}", -1, out var availWidth); using var panel = ImRaii2.GroupPanel(
Macros.Count == 1 ? "Macro" : $"Macro {idx + 1}",
-1,
out var availWidth
);
var cursor = ImGui.GetCursorPos(); var cursor = ImGui.GetCursorPos();
ImGuiUtils.AlignRight(ImGui.GetFrameHeight(), availWidth); ImGuiUtils.AlignRight(ImGui.GetFrameHeight(), availWidth);
var buttonCursor = ImGui.GetCursorPos(); var buttonCursor = ImGui.GetCursorPos();
ImGui.InvisibleButton("##copyInvButton", new(ImGui.GetFrameHeight())); ImGui.InvisibleButton("##copyInvButton", new(ImGui.GetFrameHeight()));
var buttonHovered = ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenOverlapped | ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); var buttonHovered = ImGui.IsItemHovered(
ImGuiHoveredFlags.AllowWhenOverlapped | ImGuiHoveredFlags.AllowWhenBlockedByActiveItem
);
var buttonActive = buttonHovered && ImGui.GetIO().MouseDown[(int)ImGuiMouseButton.Left]; var buttonActive = buttonHovered && ImGui.GetIO().MouseDown[(int)ImGuiMouseButton.Left];
var buttonClicked = buttonHovered && ImGui.GetIO().MouseReleased[(int)ImGuiMouseButton.Left]; var buttonClicked =
buttonHovered && ImGui.GetIO().MouseReleased[(int)ImGuiMouseButton.Left];
ImGui.SetCursorPos(buttonCursor); ImGui.SetCursorPos(buttonCursor);
{ {
using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(buttonActive ? ImGuiCol.ButtonActive : ImGuiCol.ButtonHovered), buttonHovered); using var color = ImRaii.PushColor(
ImGuiCol.Button,
ImGui.GetColorU32(buttonActive ? ImGuiCol.ButtonActive : ImGuiCol.ButtonHovered),
buttonHovered
);
ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Paste); ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Paste);
if (buttonClicked) if (buttonClicked)
{ {
ImGui.SetClipboardText(macro); ImGui.SetClipboardText(macro);
if (Service.Configuration.MacroCopy.ShowCopiedMessage) if (Service.Configuration.MacroCopy.ShowCopiedMessage)
{ {
Service.Plugin.DisplayNotification(new() Plugin.Plugin.DisplayNotification(
{ new()
Content = Macros.Count == 1 ? "Copied macro to clipboard." : $"Copied macro {idx + 1} to clipboard.", {
MinimizedText = Macros.Count == 1 ? "Copied macro" : $"Copied macro {idx + 1}", Content =
Title = "Macro Copied", Macros.Count == 1
Type = NotificationType.Success ? "Copied macro to clipboard."
}); : $"Copied macro {idx + 1} to clipboard.",
MinimizedText =
Macros.Count == 1 ? "Copied macro" : $"Copied macro {idx + 1}",
Title = "Macro Copied",
Type = NotificationType.Success,
}
);
} }
} }
} }
@@ -77,7 +95,17 @@ public sealed class MacroClipboard : Window, IDisposable
using var padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero); using var padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero);
using var bg = ImRaii.PushColor(ImGuiCol.FrameBg, Vector4.Zero); using var bg = ImRaii.PushColor(ImGuiCol.FrameBg, Vector4.Zero);
var lineCount = macro.Count(c => c == '\n') + 1; var lineCount = macro.Count(c => c == '\n') + 1;
ImGui.InputTextMultiline("", ref macro, macro.Length + 1, new(availWidth, ImGui.GetTextLineHeight() * Math.Max(15, lineCount) + ImGui.GetStyle().FramePadding.Y), ImGuiInputTextFlags.ReadOnly | ImGuiInputTextFlags.AutoSelectAll); ImGui.InputTextMultiline(
"",
ref macro,
macro.Length + 1,
new(
availWidth,
ImGui.GetTextLineHeight() * Math.Max(15, lineCount)
+ ImGui.GetStyle().FramePadding.Y
),
ImGuiInputTextFlags.ReadOnly | ImGuiInputTextFlags.AutoSelectAll
);
} }
if (buttonHovered) if (buttonHovered)
File diff suppressed because it is too large Load Diff
+157 -54
View File
@@ -1,18 +1,18 @@
using Craftimizer.Plugin;
using Craftimizer.Utils;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface;
using Dalamud.Interface.Windowing;
using Dalamud.Bindings.ImGui;
using System; using System;
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Sim = Craftimizer.Simulator.SimulatorNoRandom; using Craftimizer.Plugin;
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Craftimizer.Utils;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Utility; using Dalamud.Utility;
using Sim = Craftimizer.Simulator.SimulatorNoRandom;
namespace Craftimizer.Windows; namespace Craftimizer.Windows;
@@ -23,10 +23,11 @@ public sealed class MacroList : Window, IDisposable
public CharacterStats? CharacterStats { get; private set; } public CharacterStats? CharacterStats { get; private set; }
public RecipeData? RecipeData { get; private set; } public RecipeData? RecipeData { get; private set; }
private IReadOnlyList<Macro> Macros => Service.Configuration.Macros; private static IReadOnlyList<Macro> Macros => Service.Configuration.Macros;
private Dictionary<Macro, SimulationState> MacroStateCache { get; } = []; private Dictionary<Macro, SimulationState> MacroStateCache { get; } = [];
public MacroList() : base("Craftimizer Macro List", WindowFlags, false) public MacroList()
: base("Craftimizer Macro List", WindowFlags, false)
{ {
RefreshSearch(); RefreshSearch();
@@ -36,7 +37,11 @@ public sealed class MacroList : Window, IDisposable
CollapsedCondition = ImGuiCond.Appearing; CollapsedCondition = ImGuiCond.Appearing;
Collapsed = false; Collapsed = false;
SizeConstraints = new() { MinimumSize = new(465, 520), MaximumSize = new(float.PositiveInfinity) }; SizeConstraints = new()
{
MinimumSize = new(465, 520),
MaximumSize = new(float.PositiveInfinity),
};
TitleBarButtons = TitleBarButtons =
[ [
@@ -45,14 +50,15 @@ public sealed class MacroList : Window, IDisposable
Icon = FontAwesomeIcon.Cog, Icon = FontAwesomeIcon.Cog,
IconOffset = new(2, 1), IconOffset = new(2, 1),
Click = _ => Service.Plugin.OpenSettingsTab("General"), Click = _ => Service.Plugin.OpenSettingsTab("General"),
ShowTooltip = () => ImGuiUtils.Tooltip("Open Settings") ShowTooltip = () => ImGuiUtils.Tooltip("Open Settings"),
}, },
new() { new()
{
Icon = FontAwesomeIcon.Heart, Icon = FontAwesomeIcon.Heart,
IconOffset = new(2, 1), IconOffset = new(2, 1),
Click = _ => Util.OpenLink(Plugin.Plugin.SupportLink), Click = _ => Util.OpenLink(Plugin.Plugin.SupportLink),
ShowTooltip = () => ImGuiUtils.Tooltip("Support me on Ko-fi!") ShowTooltip = () => ImGuiUtils.Tooltip("Support me on Ko-fi!"),
} },
]; ];
Service.WindowSystem.AddWindow(this); Service.WindowSystem.AddWindow(this);
@@ -60,7 +66,7 @@ public sealed class MacroList : Window, IDisposable
public override bool DrawConditions() public override bool DrawConditions()
{ {
return Service.ClientState.LocalPlayer != null; return Service.Objects.LocalPlayer != null;
} }
public override void PreDraw() public override void PreDraw()
@@ -90,7 +96,9 @@ public sealed class MacroList : Window, IDisposable
ImGui.InvisibleButton($"###macroButton{i}", ImGui.GetItemRectSize()); ImGui.InvisibleButton($"###macroButton{i}", ImGui.GetItemRectSize());
if (isUnsorted) if (isUnsorted)
{ {
using (var _source = ImRaii.DragDropSource(ImGuiDragDropFlags.SourceNoDisableHover)) using (
var _source = ImRaii.DragDropSource(ImGuiDragDropFlags.SourceNoDisableHover)
)
{ {
if (_source) if (_source)
{ {
@@ -115,7 +123,10 @@ public sealed class MacroList : Window, IDisposable
var text2 = "the Macro Editor here or from the Crafting Log."; var text2 = "the Macro Editor here or from the Crafting Log.";
var text3 = "Open Crafting Log"; var text3 = "Open Crafting Log";
var text4 = "Open Macro Editor"; var text4 = "Open Macro Editor";
var buttonRowWidth = ImGui.CalcTextSize(text3).X + ImGui.CalcTextSize(text4).X + ImGui.GetStyle().ItemSpacing.X * 5; var buttonRowWidth =
ImGui.CalcTextSize(text3).X
+ ImGui.CalcTextSize(text4).X
+ ImGui.GetStyle().ItemSpacing.X * 5;
var size = new Vector2( var size = new Vector2(
Math.Max( Math.Max(
Math.Max(ImGui.CalcTextSize(text1).X, ImGui.CalcTextSize(text2).X), Math.Max(ImGui.CalcTextSize(text1).X, ImGui.CalcTextSize(text2).X),
@@ -129,7 +140,7 @@ public sealed class MacroList : Window, IDisposable
ImGuiUtils.TextCentered(text2); ImGuiUtils.TextCentered(text2);
ImGuiUtils.AlignCentered(buttonRowWidth); ImGuiUtils.AlignCentered(buttonRowWidth);
if (ImGui.Button(text3)) if (ImGui.Button(text3))
Service.Plugin.OpenCraftingLog(); Plugin.Plugin.OpenCraftingLog();
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button(text4)) if (ImGui.Button(text4))
OpenEditor(null); OpenEditor(null);
@@ -139,6 +150,7 @@ public sealed class MacroList : Window, IDisposable
private string searchText = string.Empty; private string searchText = string.Empty;
private List<Macro> sortedMacros = null!; private List<Macro> sortedMacros = null!;
private bool isUnsorted = true; private bool isUnsorted = true;
private void DrawSearchBar() private void DrawSearchBar()
{ {
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
@@ -157,12 +169,20 @@ public sealed class MacroList : Window, IDisposable
var stateNullable = GetMacroState(macro); var stateNullable = GetMacroState(macro);
using var panel = ImRaii2.GroupPanel(macro.Name, width - ImGui.GetStyle().ItemSpacing.X * 2, out var availWidth); using var panel = ImRaii2.GroupPanel(
macro.Name,
width - ImGui.GetStyle().ItemSpacing.X * 2,
out var availWidth
);
var stepsAvailWidthOffset = width - availWidth; var stepsAvailWidthOffset = width - availWidth;
var spacing = ImGui.GetStyle().ItemSpacing.Y; var spacing = ImGui.GetStyle().ItemSpacing.Y;
var miniRowHeight = (windowHeight - spacing) / 2f; var miniRowHeight = (windowHeight - spacing) / 2f;
using var table = ImRaii.Table("table", stateNullable.HasValue ? 3 : 2, ImGuiTableFlags.BordersInnerV); using var table = ImRaii.Table(
"table",
stateNullable.HasValue ? 3 : 2,
ImGuiTableFlags.BordersInnerV
);
if (table) if (table)
{ {
if (stateNullable.HasValue) if (stateNullable.HasValue)
@@ -177,27 +197,36 @@ public sealed class MacroList : Window, IDisposable
if (Service.Configuration.ShowOptimalMacroStat) if (Service.Configuration.ShowOptimalMacroStat)
{ {
var progressHeight = windowHeight; var progressHeight = windowHeight;
if (state.Progress >= state.Input.Recipe.MaxProgress && state.Input.Recipe.MaxQuality > 0) if (
state.Progress >= state.Input.Recipe.MaxProgress
&& state.Input.Recipe.MaxQuality > 0
)
{ {
ImGuiUtils.ArcProgress( ImGuiUtils.ArcProgress(
(float)state.Quality / state.Input.Recipe.MaxQuality, (float)state.Quality / state.Input.Recipe.MaxQuality,
progressHeight / 2f, progressHeight / 2f,
.5f, .5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight), ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.Quality)); ImGui.GetColorU32(Colors.Quality)
);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip($"Quality: {state.Quality} / {state.Input.Recipe.MaxQuality}"); ImGuiUtils.Tooltip(
$"Quality: {state.Quality} / {state.Input.Recipe.MaxQuality}"
);
} }
else else
{ {
ImGuiUtils.ArcProgress( ImGuiUtils.ArcProgress(
(float)state.Progress / state.Input.Recipe.MaxProgress, (float)state.Progress / state.Input.Recipe.MaxProgress,
progressHeight / 2f, progressHeight / 2f,
.5f, .5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight), ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.Progress)); ImGui.GetColorU32(Colors.Progress)
);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip($"Progress: {state.Progress} / {state.Input.Recipe.MaxProgress}"); ImGuiUtils.Tooltip(
$"Progress: {state.Progress} / {state.Input.Recipe.MaxProgress}"
);
} }
} }
else else
@@ -207,9 +236,12 @@ public sealed class MacroList : Window, IDisposable
miniRowHeight / 2f, miniRowHeight / 2f,
.5f, .5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight), ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.Progress)); ImGui.GetColorU32(Colors.Progress)
);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip($"Progress: {state.Progress} / {state.Input.Recipe.MaxProgress}"); ImGuiUtils.Tooltip(
$"Progress: {state.Progress} / {state.Input.Recipe.MaxProgress}"
);
ImGui.SameLine(0, spacing); ImGui.SameLine(0, spacing);
ImGuiUtils.ArcProgress( ImGuiUtils.ArcProgress(
@@ -217,17 +249,24 @@ public sealed class MacroList : Window, IDisposable
miniRowHeight / 2f, miniRowHeight / 2f,
.5f, .5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight), ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.Quality)); ImGui.GetColorU32(Colors.Quality)
);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip($"Quality: {state.Quality} / {state.Input.Recipe.MaxQuality}"); ImGuiUtils.Tooltip(
$"Quality: {state.Quality} / {state.Input.Recipe.MaxQuality}"
);
ImGuiUtils.ArcProgress((float)state.Durability / state.Input.Recipe.MaxDurability, ImGuiUtils.ArcProgress(
(float)state.Durability / state.Input.Recipe.MaxDurability,
miniRowHeight / 2f, miniRowHeight / 2f,
.5f, .5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight), ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.Durability)); ImGui.GetColorU32(Colors.Durability)
);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip($"Remaining Durability: {state.Durability} / {state.Input.Recipe.MaxDurability}"); ImGuiUtils.Tooltip(
$"Remaining Durability: {state.Durability} / {state.Input.Recipe.MaxDurability}"
);
ImGui.SameLine(0, spacing); ImGui.SameLine(0, spacing);
ImGuiUtils.ArcProgress( ImGuiUtils.ArcProgress(
@@ -235,7 +274,8 @@ public sealed class MacroList : Window, IDisposable
miniRowHeight / 2f, miniRowHeight / 2f,
.5f, .5f,
ImGui.GetColorU32(ImGuiCol.TableBorderLight), ImGui.GetColorU32(ImGuiCol.TableBorderLight),
ImGui.GetColorU32(Colors.CP)); ImGui.GetColorU32(Colors.CP)
);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip($"Remaining CP: {state.CP} / {state.Input.Stats.CP}"); ImGuiUtils.Tooltip($"Remaining CP: {state.CP} / {state.Input.Stats.CP}");
} }
@@ -270,7 +310,11 @@ public sealed class MacroList : Window, IDisposable
ImGui.TableNextColumn(); ImGui.TableNextColumn();
{ {
var itemsPerRow = (int)MathF.Floor((ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset + spacing * 2) / (miniRowHeight + spacing)); var itemsPerRow = (int)
MathF.Floor(
(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset + spacing * 2)
/ (miniRowHeight + spacing)
);
var itemCount = macro.Actions.Count; var itemCount = macro.Actions.Count;
for (var i = 0; i < itemsPerRow * 2; i++) for (var i = 0; i < itemsPerRow * 2; i++)
{ {
@@ -281,7 +325,10 @@ public sealed class MacroList : Window, IDisposable
var shouldShowMore = i + 1 == itemsPerRow * 2 && i + 1 < itemCount; var shouldShowMore = i + 1 == itemsPerRow * 2 && i + 1 < itemCount;
if (!shouldShowMore) if (!shouldShowMore)
{ {
ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).Handle, new(miniRowHeight)); ImGui.Image(
macro.Actions[i].GetIcon(RecipeData!.ClassJob).Handle,
new(miniRowHeight)
);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip(macro.Actions[i].GetName(RecipeData!.ClassJob)); ImGuiUtils.Tooltip(macro.Actions[i].GetName(RecipeData!.ClassJob));
} }
@@ -289,12 +336,36 @@ public sealed class MacroList : Window, IDisposable
{ {
var amtMore = itemCount - itemsPerRow * 2; var amtMore = itemCount - itemsPerRow * 2;
var pos = ImGui.GetCursorPos(); var pos = ImGui.GetCursorPos();
ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).Handle, new(miniRowHeight), default, Vector2.One, new(1, 1, 1, .5f)); ImGui.Image(
macro.Actions[i].GetIcon(RecipeData!.ClassJob).Handle,
new(miniRowHeight),
default,
Vector2.One,
new(1, 1, 1, .5f)
);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGuiUtils.Tooltip($"{macro.Actions[i].GetName(RecipeData!.ClassJob)}\nand {amtMore} more"); ImGuiUtils.Tooltip(
$"{macro.Actions[i].GetName(RecipeData!.ClassJob)}\nand {amtMore} more"
);
ImGui.SetCursorPos(pos); ImGui.SetCursorPos(pos);
ImGui.GetWindowDrawList().AddRectFilled(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), ImGui.GetColorU32(ImGuiCol.FrameBg), miniRowHeight / 8f); ImGui
ImGui.GetWindowDrawList().AddTextClippedEx(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), $"+{amtMore}", null, new(.5f), null); .GetWindowDrawList()
.AddRectFilled(
ImGui.GetCursorScreenPos(),
ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight),
ImGui.GetColorU32(ImGuiCol.FrameBg),
miniRowHeight / 8f
);
ImGui
.GetWindowDrawList()
.AddTextClippedEx(
ImGui.GetCursorScreenPos(),
ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight),
$"+{amtMore}",
null,
new(.5f),
null
);
} }
} }
else else
@@ -306,12 +377,19 @@ public sealed class MacroList : Window, IDisposable
private string popupMacroName = string.Empty; private string popupMacroName = string.Empty;
private Macro? popupMacro; private Macro? popupMacro;
private void ShowRenamePopup(Macro macro) private void ShowRenamePopup(Macro macro)
{ {
ImGui.OpenPopup($"##renamePopup-{macro.GetHashCode()}"); ImGui.OpenPopup($"##renamePopup-{macro.GetHashCode()}");
popupMacro = macro; popupMacro = macro;
popupMacroName = macro.Name; popupMacroName = macro.Name;
ImGui.SetNextWindowPos(ImGui.GetMousePos() - new Vector2(ImGui.CalcItemWidth() * .25f, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.Y * 2)); ImGui.SetNextWindowPos(
ImGui.GetMousePos()
- new Vector2(
ImGui.CalcItemWidth() * .25f,
ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.Y * 2
)
);
} }
private void DrawRenamePopup(Macro macro) private void DrawRenamePopup(Macro macro)
@@ -322,7 +400,15 @@ public sealed class MacroList : Window, IDisposable
if (ImGui.IsWindowAppearing()) if (ImGui.IsWindowAppearing())
ImGui.SetKeyboardFocusHere(); ImGui.SetKeyboardFocusHere();
ImGui.SetNextItemWidth(ImGui.CalcItemWidth()); ImGui.SetNextItemWidth(ImGui.CalcItemWidth());
if (ImGui.InputTextWithHint($"##setName", "Name", ref popupMacroName, 100, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue)) if (
ImGui.InputTextWithHint(
$"##setName",
"Name",
ref popupMacroName,
100,
ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue
)
)
{ {
if (!string.IsNullOrWhiteSpace(popupMacroName)) if (!string.IsNullOrWhiteSpace(popupMacroName))
{ {
@@ -349,17 +435,34 @@ public sealed class MacroList : Window, IDisposable
} }
isUnsorted = false; isUnsorted = false;
var matcher = new FuzzyMatcher(searchText.ToLowerInvariant(), MatchMode.FuzzyParts); var matcher = new FuzzyMatcher(searchText.ToLowerInvariant(), MatchMode.FuzzyParts);
var query = Macros.AsParallel().Select(i => (Item: i, Score: matcher.Matches(i.Name.ToLowerInvariant()))) var query = Macros
.AsParallel()
.Select(i => (Item: i, Score: matcher.Matches(i.Name.ToLowerInvariant())))
.Where(t => t.Score > 0) .Where(t => t.Score > 0)
.OrderByDescending(t => t.Score) .OrderByDescending(t => t.Score)
.Select(t => t.Item); .Select(t => t.Item);
sortedMacros = [.. query]; sortedMacros = [.. query];
} }
private void OpenEditor(Macro? macro) private static void OpenEditor(Macro? macro)
{ {
var stats = Service.Plugin.GetDefaultStats(); var stats = Service.Plugin.GetDefaultStats();
Service.Plugin.OpenMacroEditor(stats.Character, stats.Recipe, stats.Buffs, null, macro?.Actions ?? Enumerable.Empty<ActionType>(), macro != null ? (actions => { macro.ActionEnumerable = actions; Service.Configuration.Save(); }) : null); Service.Plugin.OpenMacroEditor(
stats.Character,
stats.Recipe,
stats.Buffs,
null,
macro?.Actions ?? Enumerable.Empty<ActionType>(),
macro != null
? (
actions =>
{
macro.ActionEnumerable = actions;
Service.Configuration.Save();
}
)
: null
);
} }
private void OnMacroChanged(Macro macro) private void OnMacroChanged(Macro macro)
File diff suppressed because it is too large Load Diff
+337 -183
View File
@@ -1,6 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using Craftimizer.Simulator; using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions; using Craftimizer.Simulator.Actions;
using Craftimizer.Solver; using Craftimizer.Solver;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas;
@@ -8,12 +14,6 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using Dalamud.Bindings.ImGui;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
namespace Craftimizer.Plugin.Windows; namespace Craftimizer.Plugin.Windows;
@@ -31,17 +31,22 @@ public sealed class Settings : Window, IDisposable
private IFontHandle HeaderFont { get; } private IFontHandle HeaderFont { get; }
private IFontHandle SubheaderFont { get; } private IFontHandle SubheaderFont { get; }
public Settings() : base("Craftimizer Settings", WindowFlags) public Settings()
: base("Craftimizer Settings", WindowFlags)
{ {
Service.WindowSystem.AddWindow(this); Service.WindowSystem.AddWindow(this);
HeaderFont = Service.PluginInterface.UiBuilder.FontAtlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePx * 2f))); HeaderFont = Service.PluginInterface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
SubheaderFont = Service.PluginInterface.UiBuilder.FontAtlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePx * 1.5f))); e.OnPreBuild(tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePx * 2f))
);
SubheaderFont = Service.PluginInterface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
e.OnPreBuild(tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePx * 1.5f))
);
SizeConstraints = new WindowSizeConstraints() SizeConstraints = new WindowSizeConstraints()
{ {
MinimumSize = new(450, 400), MinimumSize = new(450, 400),
MaximumSize = new(float.PositiveInfinity) MaximumSize = new(float.PositiveInfinity),
}; };
} }
@@ -50,19 +55,24 @@ public sealed class Settings : Window, IDisposable
SelectedTab = label; SelectedTab = label;
} }
private ImRaii.IEndObject TabItem(string label) private ImRaii.TabItemDisposable TabItem(string label)
{ {
var isSelected = string.Equals(SelectedTab, label, StringComparison.Ordinal); var isSelected = string.Equals(SelectedTab, label, StringComparison.Ordinal);
if (isSelected) if (isSelected)
{ {
SelectedTab = null; SelectedTab = null;
var open = true; return ImRaii.TabItem(label, ImGuiTabItemFlags.SetSelected);
return ImRaii.TabItem(label, ref open, ImGuiTabItemFlags.SetSelected);
} }
return ImRaii.TabItem(label); return ImRaii.TabItem(label);
} }
private static void DrawOption(string label, string tooltip, bool val, Action<bool> setter, ref bool isDirty) private static void DrawOption(
string label,
string tooltip,
bool val,
Action<bool> setter,
ref bool isDirty
)
{ {
if (ImGui.Checkbox(label, ref val)) if (ImGui.Checkbox(label, ref val))
{ {
@@ -73,12 +83,28 @@ public sealed class Settings : Window, IDisposable
ImGuiUtils.TooltipWrapped(tooltip); ImGuiUtils.TooltipWrapped(tooltip);
} }
private static void DrawOption<T>(string label, string tooltip, T value, T min, T max, Action<T> setter, ref bool isDirty) where T : struct, INumber<T> private static void DrawOption<T>(
string label,
string tooltip,
T value,
T min,
T max,
Action<T> setter,
ref bool isDirty
)
where T : struct, INumber<T>
{ {
ImGui.SetNextItemWidth(OptionWidth); ImGui.SetNextItemWidth(OptionWidth);
var text = value.ToString(); var text = value.ToString();
ArgumentNullException.ThrowIfNull(text, nameof(value)); ArgumentNullException.ThrowIfNull(text, nameof(value));
if (ImGui.InputText(label, ref text, 8, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CharsDecimal)) if (
ImGui.InputText(
label,
ref text,
8,
ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CharsDecimal
)
)
{ {
if (T.TryParse(text, null, out var newValue)) if (T.TryParse(text, null, out var newValue))
{ {
@@ -103,7 +129,13 @@ public sealed class Settings : Window, IDisposable
ImGuiUtils.TooltipWrapped(tooltip); ImGuiUtils.TooltipWrapped(tooltip);
} }
private static void DrawOption(string label, string tooltip, string value, Action<string> setter, ref bool isDirty) private static void DrawOption(
string label,
string tooltip,
string value,
Action<string> setter,
ref bool isDirty
)
{ {
ImGui.SetNextItemWidth(OptionWidth); ImGui.SetNextItemWidth(OptionWidth);
var text = value; var text = value;
@@ -119,7 +151,17 @@ public sealed class Settings : Window, IDisposable
ImGuiUtils.TooltipWrapped(tooltip); ImGuiUtils.TooltipWrapped(tooltip);
} }
private static void DrawOption<T>(string label, string tooltip, Func<T, string> getName, Func<T, string> getTooltip, T value, Action<T> setter, ref bool isDirty, params T[] excludedValues) where T : struct, Enum private static void DrawOption<T>(
string label,
string tooltip,
Func<T, string> getName,
Func<T, string> getTooltip,
T value,
Action<T> setter,
ref bool isDirty,
params T[] excludedValues
)
where T : struct, Enum
{ {
ImGui.SetNextItemWidth(OptionWidth); ImGui.SetNextItemWidth(OptionWidth);
using (var combo = ImRaii.Combo(label, getName(value))) using (var combo = ImRaii.Combo(label, getName(value)))
@@ -159,18 +201,18 @@ public sealed class Settings : Window, IDisposable
private static string GetAlgorithmTooltip(SolverAlgorithm algorithm) => private static string GetAlgorithmTooltip(SolverAlgorithm algorithm) =>
algorithm switch algorithm switch
{ {
SolverAlgorithm.Oneshot => "Run through all iterations and pick the best macro", SolverAlgorithm.Oneshot => "Run through all iterations and pick the best macro",
SolverAlgorithm.OneshotForked => "Oneshot, but using multiple solvers simultaneously", SolverAlgorithm.OneshotForked => "Oneshot, but using multiple solvers simultaneously",
SolverAlgorithm.Stepwise => "Run through all iterations and pick the next best step, " + SolverAlgorithm.Stepwise => "Run through all iterations and pick the next best step, "
"and repeat using previous steps as a starting point", + "and repeat using previous steps as a starting point",
SolverAlgorithm.StepwiseForked => "Stepwise, but using multiple solvers simultaneously", SolverAlgorithm.StepwiseForked => "Stepwise, but using multiple solvers simultaneously",
SolverAlgorithm.StepwiseGenetic => "Stepwise Forked, but the top N next best steps are " + SolverAlgorithm.StepwiseGenetic => "Stepwise Forked, but the top N next best steps are "
"selected from the solvers, and each one is equally " + + "selected from the solvers, and each one is equally "
"used as a starting point", + "used as a starting point",
SolverAlgorithm.Raphael => "Finds the best solution, every time. This solver has " + SolverAlgorithm.Raphael => "Finds the best solution, every time. This solver has "
"very different options compared to the rest, as it " + + "very different options compared to the rest, as it "
"is designed using an entirely different algorithm.", + "is designed using an entirely different algorithm.",
_ => "Unknown" _ => "Unknown",
}; };
private static string GetCopyTypeName(MacroCopyConfiguration.CopyType type) => private static string GetCopyTypeName(MacroCopyConfiguration.CopyType type) =>
@@ -186,12 +228,16 @@ public sealed class Settings : Window, IDisposable
private static string GetCopyTypeTooltip(MacroCopyConfiguration.CopyType type) => private static string GetCopyTypeTooltip(MacroCopyConfiguration.CopyType type) =>
type switch type switch
{ {
MacroCopyConfiguration.CopyType.OpenWindow => "Open a dedicated window with all macros being copied. " + MacroCopyConfiguration.CopyType.OpenWindow =>
"Copy, view, and choose at your own leisure.", "Open a dedicated window with all macros being copied. "
MacroCopyConfiguration.CopyType.CopyToMacro => "Copy directly to the game's macro system.", + "Copy, view, and choose at your own leisure.",
MacroCopyConfiguration.CopyType.CopyToClipboard => "Copy to your clipboard. Macros are separated by a blank line.", MacroCopyConfiguration.CopyType.CopyToMacro =>
MacroCopyConfiguration.CopyType.CopyToMacroMate => "Copy directly to a Macro Mate macro. Requires the Macro Mate plugin.", "Copy directly to the game's macro system.",
_ => "Unknown" MacroCopyConfiguration.CopyType.CopyToClipboard =>
"Copy to your clipboard. Macros are separated by a blank line.",
MacroCopyConfiguration.CopyType.CopyToMacroMate =>
"Copy directly to a Macro Mate macro. Requires the Macro Mate plugin.",
_ => "Unknown",
}; };
private static string GetProgressBarTypeName(Configuration.ProgressBarType type) => private static string GetProgressBarTypeName(Configuration.ProgressBarType type) =>
@@ -208,8 +254,9 @@ public sealed class Settings : Window, IDisposable
{ {
Configuration.ProgressBarType.Colorful => "Colorful, rainbow colors", Configuration.ProgressBarType.Colorful => "Colorful, rainbow colors",
Configuration.ProgressBarType.Simple => "Simple, grayscale colors", Configuration.ProgressBarType.Simple => "Simple, grayscale colors",
Configuration.ProgressBarType.None => "No progress bar; only percent completion is shown", Configuration.ProgressBarType.None =>
_ => "Unknown" "No progress bar; only percent completion is shown",
_ => "Unknown",
}; };
public override void Draw() public override void Draw()
@@ -239,9 +286,9 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Enable Synthesis Helper", "Enable Synthesis Helper",
"Adds a helper next to your synthesis window to help solve for the best craft. " + "Adds a helper next to your synthesis window to help solve for the best craft. "
"Extremely useful for expert recipes, where the condition can greatly affect " + + "Extremely useful for expert recipes, where the condition can greatly affect "
"which actions you take.", + "which actions you take.",
Config.EnableSynthHelper, Config.EnableSynthHelper,
v => Config.EnableSynthHelper = v, v => Config.EnableSynthHelper = v,
ref isDirty ref isDirty
@@ -249,9 +296,9 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Show Only One Macro Stat in Crafting Log", "Show Only One Macro Stat in Crafting Log",
"Only one stat will be shown for a macro. If a craft will be finished, quality " + "Only one stat will be shown for a macro. If a craft will be finished, quality "
"is shown. Otherwise, progress is shown. Durability and remaining CP will be " + + "is shown. Otherwise, progress is shown. Durability and remaining CP will be "
"hidden.", + "hidden.",
Config.ShowOptimalMacroStat, Config.ShowOptimalMacroStat,
v => Config.ShowOptimalMacroStat = v, v => Config.ShowOptimalMacroStat = v,
ref isDirty ref isDirty
@@ -259,8 +306,8 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Check For Delineations", "Check For Delineations",
"Your inventory will be checked to ensure that you have delineations available " + "Your inventory will be checked to ensure that you have delineations available "
"before suggesting any specialist actions.", + "before suggesting any specialist actions.",
Config.CheckDelineations, Config.CheckDelineations,
v => Config.CheckDelineations = v, v => Config.CheckDelineations = v,
ref isDirty ref isDirty
@@ -268,9 +315,9 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Reliability Trial Count", "Reliability Trial Count",
"When testing for reliability of a macro in the editor, this many trials will be " + "When testing for reliability of a macro in the editor, this many trials will be "
"run. You should set this value to at least 100 to get a reliable spread of data. " + + "run. You should set this value to at least 100 to get a reliable spread of data. "
"If it's too low, you may not find an outlier, and the average might be skewed.", + "If it's too low, you may not find an outlier, and the average might be skewed.",
Config.ReliabilitySimulationCount, Config.ReliabilitySimulationCount,
5, 5,
5000, 5000,
@@ -302,8 +349,13 @@ public sealed class Settings : Window, IDisposable
ref isDirty ref isDirty
); );
if (Config.MacroCopy.Type == MacroCopyConfiguration.CopyType.CopyToMacroMate && if (
!Service.PluginInterface.InstalledPlugins.Any(p => p.IsLoaded && string.Equals(p.InternalName, "MacroMate", StringComparison.Ordinal))) Config.MacroCopy.Type == MacroCopyConfiguration.CopyType.CopyToMacroMate
&& !Service.PluginInterface.InstalledPlugins.Any(p =>
p.IsLoaded
&& string.Equals(p.InternalName, "MacroMate", StringComparison.Ordinal)
)
)
{ {
ImGui.SameLine(); ImGui.SameLine();
using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange)) using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange))
@@ -327,8 +379,8 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Copy to Shared Macros", "Copy to Shared Macros",
"Copy to the shared macros tab. Leaving this unchecked copies to the " + "Copy to the shared macros tab. Leaving this unchecked copies to the "
"individual tab.", + "individual tab.",
Config.MacroCopy.SharedMacro, Config.MacroCopy.SharedMacro,
v => Config.MacroCopy.SharedMacro = v, v => Config.MacroCopy.SharedMacro = v,
ref isDirty ref isDirty
@@ -336,20 +388,22 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Macro Number", "Macro Number",
"The # of the macro to being copying to. Subsequent macros will be " + "The # of the macro to being copying to. Subsequent macros will be "
"copied relative to this macro.", + "copied relative to this macro.",
Config.MacroCopy.StartMacroIdx, Config.MacroCopy.StartMacroIdx,
0, 99, 0,
99,
v => Config.MacroCopy.StartMacroIdx = v, v => Config.MacroCopy.StartMacroIdx = v,
ref isDirty ref isDirty
); );
DrawOption( DrawOption(
"Max Macro Copy Count", "Max Macro Copy Count",
"The maximum number of macros to be copied. Any more and a window is " + "The maximum number of macros to be copied. Any more and a window is "
"displayed with the rest of them.", + "displayed with the rest of them.",
Config.MacroCopy.MaxMacroCount, Config.MacroCopy.MaxMacroCount,
1, 99, 1,
99,
v => Config.MacroCopy.MaxMacroCount = v, v => Config.MacroCopy.MaxMacroCount = v,
ref isDirty ref isDirty
); );
@@ -385,15 +439,20 @@ public sealed class Settings : Window, IDisposable
{ {
DrawOption( DrawOption(
"Use Macro Chain", "Use Macro Chain",
"Replaces the last step with /nextmacro to run the next macro " + "Replaces the last step with /nextmacro to run the next macro "
"automatically. Overrides the Intermediate Notification Sound.", + "automatically. Overrides the Intermediate Notification Sound.",
Config.MacroCopy.UseNextMacro, Config.MacroCopy.UseNextMacro,
v => Config.MacroCopy.UseNextMacro = v, v => Config.MacroCopy.UseNextMacro = v,
ref isDirty ref isDirty
); );
if (Config.MacroCopy.UseNextMacro && if (
!Service.PluginInterface.InstalledPlugins.Any(p => p.IsLoaded && string.Equals(p.InternalName, "MacroChain", StringComparison.Ordinal))) Config.MacroCopy.UseNextMacro
&& !Service.PluginInterface.InstalledPlugins.Any(p =>
p.IsLoaded
&& string.Equals(p.InternalName, "MacroChain", StringComparison.Ordinal)
)
)
{ {
ImGui.SameLine(); ImGui.SameLine();
using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange)) using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange))
@@ -408,8 +467,8 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Add Macro Lock", "Add Macro Lock",
"Adds /mlock to the beginning of every macro. Prevents other " + "Adds /mlock to the beginning of every macro. Prevents other "
"macros from being run.", + "macros from being run.",
Config.MacroCopy.UseMacroLock, Config.MacroCopy.UseMacroLock,
v => Config.MacroCopy.UseMacroLock = v, v => Config.MacroCopy.UseMacroLock = v,
ref isDirty ref isDirty
@@ -425,12 +484,18 @@ public sealed class Settings : Window, IDisposable
if (Config.MacroCopy.AddNotification) if (Config.MacroCopy.AddNotification)
{ {
if ((Config.MacroCopy.Type == MacroCopyConfiguration.CopyType.CopyToMacro || !Config.MacroCopy.CombineMacro) && Config.MacroCopy.Type != MacroCopyConfiguration.CopyType.CopyToMacroMate) if (
(
Config.MacroCopy.Type == MacroCopyConfiguration.CopyType.CopyToMacro
|| !Config.MacroCopy.CombineMacro
)
&& Config.MacroCopy.Type != MacroCopyConfiguration.CopyType.CopyToMacroMate
)
{ {
DrawOption( DrawOption(
"Force Notification", "Force Notification",
"Prioritize always having a notification sound at the end of " + "Prioritize always having a notification sound at the end of "
"every macro. Keeping this off prevents macros with only 1 action.", + "every macro. Keeping this off prevents macros with only 1 action.",
Config.MacroCopy.ForceNotification, Config.MacroCopy.ForceNotification,
v => Config.MacroCopy.ForceNotification = v, v => Config.MacroCopy.ForceNotification = v,
ref isDirty ref isDirty
@@ -447,14 +512,18 @@ public sealed class Settings : Window, IDisposable
if (Config.MacroCopy.AddNotificationSound) if (Config.MacroCopy.AddNotificationSound)
{ {
if (!Config.MacroCopy.UseNextMacro && Config.MacroCopy.Type != MacroCopyConfiguration.CopyType.CopyToMacroMate) if (
!Config.MacroCopy.UseNextMacro
&& Config.MacroCopy.Type != MacroCopyConfiguration.CopyType.CopyToMacroMate
)
{ {
DrawOption( DrawOption(
"Intermediate Notification Sound", "Intermediate Notification Sound",
"Ending notification sound for an intermediary macro.\n" + "Ending notification sound for an intermediary macro.\n"
"Uses <se.#>", + "Uses <se.#>",
Config.MacroCopy.IntermediateNotificationSound, Config.MacroCopy.IntermediateNotificationSound,
1, 16, 1,
16,
v => v =>
{ {
Config.MacroCopy.IntermediateNotificationSound = v; Config.MacroCopy.IntermediateNotificationSound = v;
@@ -466,10 +535,10 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Final Notification Sound", "Final Notification Sound",
"Ending notification sound for the final macro.\n" + "Ending notification sound for the final macro.\n" + "Uses <se.#>",
"Uses <se.#>",
Config.MacroCopy.EndNotificationSound, Config.MacroCopy.EndNotificationSound,
1, 16, 1,
16,
v => v =>
{ {
Config.MacroCopy.EndNotificationSound = v; Config.MacroCopy.EndNotificationSound = v;
@@ -507,7 +576,12 @@ public sealed class Settings : Window, IDisposable
Config.Save(); Config.Save();
} }
private static void DrawSolverConfig(ref SolverConfig configRef, SolverConfig defaultConfig, bool disableOptimal, out bool isDirty) private static void DrawSolverConfig(
ref SolverConfig configRef,
SolverConfig defaultConfig,
bool disableOptimal,
out bool isDirty
)
{ {
isDirty = false; isDirty = false;
@@ -523,10 +597,10 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Algorithm", "Algorithm",
"The algorithm to use when solving for a macro. Different " + "The algorithm to use when solving for a macro. Different "
"algorithms provide different pros and cons for using them. " + + "algorithms provide different pros and cons for using them. "
"By far, the Optimal and Stepwise Genetic algorithms provide " + + "By far, the Optimal and Stepwise Genetic algorithms provide "
"the best results, especially for very difficult crafts.", + "the best results, especially for very difficult crafts.",
GetAlgorithmName, GetAlgorithmName,
GetAlgorithmTooltip, GetAlgorithmTooltip,
config.Algorithm, config.Algorithm,
@@ -535,15 +609,25 @@ public sealed class Settings : Window, IDisposable
disableOptimal ? [SolverAlgorithm.Raphael] : [] disableOptimal ? [SolverAlgorithm.Raphael] : []
); );
using (ImRaii.Disabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseGenetic or SolverAlgorithm.Raphael))) using (
ImRaii.Disabled(
config.Algorithm
is not (
SolverAlgorithm.OneshotForked
or SolverAlgorithm.StepwiseForked
or SolverAlgorithm.StepwiseGenetic
or SolverAlgorithm.Raphael
)
)
)
DrawOption( DrawOption(
"Max Core Count", "Max Core Count",
"The number of cores to use when solving. You should use as many " + "The number of cores to use when solving. You should use as many "
"as you can. If it's too high, it will have an effect on your gameplay " + + "as you can. If it's too high, it will have an effect on your gameplay "
$"experience. A good estimate would be 1 or 2 cores less than your " + + $"experience. A good estimate would be 1 or 2 cores less than your "
$"system (FYI, you have {Environment.ProcessorCount} cores), but make sure to accomodate " + + $"system (FYI, you have {Environment.ProcessorCount} cores), but make sure to accomodate "
$"for any other tasks you have in the background, if you have any.\n" + + $"for any other tasks you have in the background, if you have any.\n"
"(Only used in the Forked, Genetic, and Optimal algorithms)", + "(Only used in the Forked, Genetic, and Optimal algorithms)",
config.MaxThreadCount, config.MaxThreadCount,
1, 1,
Environment.ProcessorCount, Environment.ProcessorCount,
@@ -555,10 +639,10 @@ public sealed class Settings : Window, IDisposable
{ {
DrawOption( DrawOption(
"Target Iterations", "Target Iterations",
"The total number of iterations to run per crafting step. " + "The total number of iterations to run per crafting step. "
"Higher values require more computational power. Higher values " + + "Higher values require more computational power. Higher values "
"also may decrease variance, so other values should be tweaked " + + "also may decrease variance, so other values should be tweaked "
"as necessary to get a more favorable outcome.", + "as necessary to get a more favorable outcome.",
config.Iterations, config.Iterations,
1000, 1000,
1000000, 1000000,
@@ -568,11 +652,11 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Max Iterations", "Max Iterations",
"The solver may go about the target iteration value if the craft " + "The solver may go about the target iteration value if the craft "
"is sufficiently difficult, and it wasn't able to find any way to " + + "is sufficiently difficult, and it wasn't able to find any way to "
"complete it yet. In rare cases, the solver might go on for a very " + + "complete it yet. In rare cases, the solver might go on for a very "
"long time. This maximum is here to prevent the solver from stealing " + + "long time. This maximum is here to prevent the solver from stealing "
"all your RAM.", + "all your RAM.",
config.MaxIterations, config.MaxIterations,
config.Iterations, config.Iterations,
5000000, 5000000,
@@ -582,11 +666,11 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Max Step Count", "Max Step Count",
"The maximum number of crafting steps; this is generally the only " + "The maximum number of crafting steps; this is generally the only "
"setting you should change, and it should be set to around 5 steps " + + "setting you should change, and it should be set to around 5 steps "
"more than what you'd expect. If this value is too low, the solver " + + "more than what you'd expect. If this value is too low, the solver "
"won't learn much per iteration; too high and it will waste time " + + "won't learn much per iteration; too high and it will waste time "
"on useless extra steps.", + "on useless extra steps.",
config.MaxStepCount, config.MaxStepCount,
1, 1,
100, 100,
@@ -596,9 +680,9 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Exploration Constant", "Exploration Constant",
"A constant that decides how often the solver will explore new, " + "A constant that decides how often the solver will explore new, "
"possibly good paths. If this value is too high, " + + "possibly good paths. If this value is too high, "
"moves will mostly be decided at random.", + "moves will mostly be decided at random.",
config.ExplorationConstant, config.ExplorationConstant,
0, 0,
10, 10,
@@ -608,10 +692,10 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Score Weighting Constant", "Score Weighting Constant",
"A constant ranging from 0 to 1 that configures how the solver " + "A constant ranging from 0 to 1 that configures how the solver "
"scores and picks paths to travel to next. A value of 0 means " + + "scores and picks paths to travel to next. A value of 0 means "
"actions will be chosen based on their average outcome, whereas " + + "actions will be chosen based on their average outcome, whereas "
"1 uses their best outcome achieved so far.", + "1 uses their best outcome achieved so far.",
config.MaxScoreWeightingConstant, config.MaxScoreWeightingConstant,
0, 0,
1, 1,
@@ -619,16 +703,25 @@ public sealed class Settings : Window, IDisposable
ref isDirty ref isDirty
); );
using (ImRaii.Disabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseGenetic))) using (
ImRaii.Disabled(
config.Algorithm
is not (
SolverAlgorithm.OneshotForked
or SolverAlgorithm.StepwiseForked
or SolverAlgorithm.StepwiseGenetic
)
)
)
DrawOption( DrawOption(
"Fork Count", "Fork Count",
"Split the number of iterations across different solvers. In general, " + "Split the number of iterations across different solvers. In general, "
"you should increase this value to at least the number of cores in " + + "you should increase this value to at least the number of cores in "
$"your system (FYI, you have {Environment.ProcessorCount} cores) to attain the most speedup. " + + $"your system (FYI, you have {Environment.ProcessorCount} cores) to attain the most speedup. "
"The higher the number, the more chance you have of finding a " + + "The higher the number, the more chance you have of finding a "
"better local maximum; this concept similar but not equivalent " + + "better local maximum; this concept similar but not equivalent "
"to the exploration constant.\n" + + "to the exploration constant.\n"
"(Only used in the Forked and Genetic algorithms)", + "(Only used in the Forked and Genetic algorithms)",
config.ForkCount, config.ForkCount,
1, 1,
500, 500,
@@ -639,10 +732,10 @@ public sealed class Settings : Window, IDisposable
using (ImRaii.Disabled(config.Algorithm is not SolverAlgorithm.StepwiseGenetic)) using (ImRaii.Disabled(config.Algorithm is not SolverAlgorithm.StepwiseGenetic))
DrawOption( DrawOption(
"Elitist Action Count", "Elitist Action Count",
"On every craft step, pick this many top solutions and use them as " + "On every craft step, pick this many top solutions and use them as "
"the input for the next craft step. For best results, use Fork Count / 2 " + + "the input for the next craft step. For best results, use Fork Count / 2 "
"and add about 1 or 2 more if needed.\n" + + "and add about 1 or 2 more if needed.\n"
"(Only used in the Stepwise Genetic algorithm)", + "(Only used in the Stepwise Genetic algorithm)",
config.FurcatedActionCount, config.FurcatedActionCount,
1, 1,
500, 500,
@@ -653,17 +746,17 @@ public sealed class Settings : Window, IDisposable
else else
{ {
DrawOption( DrawOption(
"Quick Solve", "Backload Progress",
"Speeds up solve times. Backloads all Progress " + "Speeds up solve times. Backloads all Progress "
"actions to the end of the rotation.", + "actions to the end of the rotation.",
config.BackloadProgress, config.BackloadProgress,
v => config = config with { BackloadProgress = v }, v => config = config with { BackloadProgress = v },
ref isDirty ref isDirty
); );
DrawOption( DrawOption(
"Ensure Reliability", "Ensure Reliability",
"Find a rotation that can reach the target quality no matter " + "Find a rotation that can reach the target quality no matter "
"how unlucky the random conditions are.", + "how unlucky the random conditions are.",
config.Adversarial, config.Adversarial,
v => config = config with { Adversarial = v }, v => config = config with { Adversarial = v },
ref isDirty ref isDirty
@@ -678,7 +771,9 @@ public sealed class Settings : Window, IDisposable
ImGui.TextUnformatted(FontAwesomeIcon.ExclamationCircle.ToIconString()); ImGui.TextUnformatted(FontAwesomeIcon.ExclamationCircle.ToIconString());
} }
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGuiUtils.TooltipWrapped("\"Ensure Reliability\" uses a lot more memory and can significantly increase solve times."); ImGuiUtils.TooltipWrapped(
"\"Ensure Reliability\" uses a lot more memory and can significantly increase solve times."
);
} }
} }
} }
@@ -704,9 +799,9 @@ public sealed class Settings : Window, IDisposable
{ {
DrawOption( DrawOption(
"Max Rollout Step Count", "Max Rollout Step Count",
"The maximum number of crafting steps every iteration can consider. " + "The maximum number of crafting steps every iteration can consider. "
"Decreasing this value can have unintended side effects. Only change " + + "Decreasing this value can have unintended side effects. Only change "
"this value if you absolutely know what you're doing.", + "this value if you absolutely know what you're doing.",
config.MaxRolloutStepCount, config.MaxRolloutStepCount,
1, 1,
50, 50,
@@ -716,9 +811,9 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Strict Actions", "Strict Actions",
"When finding the next possible actions to execute, use a heuristic " + "When finding the next possible actions to execute, use a heuristic "
"to restrict which actions to attempt taking. This results in a much " + + "to restrict which actions to attempt taking. This results in a much "
"better macro at the cost of not finding an extremely creative one.", + "better macro at the cost of not finding an extremely creative one.",
config.StrictActions, config.StrictActions,
v => config = config with { StrictActions = v }, v => config = config with { StrictActions = v },
ref isDirty ref isDirty
@@ -772,8 +867,8 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Steps", "Steps",
"Amount of weight to give to the craft's number of steps. The lower " + "Amount of weight to give to the craft's number of steps. The lower "
"the step count, the higher the score.", + "the step count, the higher the score.",
config.ScoreSteps, config.ScoreSteps,
0, 0,
100, 100,
@@ -787,7 +882,11 @@ public sealed class Settings : Window, IDisposable
configRef = config; configRef = config;
} }
private static void DrawActionPool(ref ActionType[] actionPool, float poolWidth, out bool isDirty) private static void DrawActionPool(
ref ActionType[] actionPool,
float poolWidth,
out bool isDirty
)
{ {
isDirty = false; isDirty = false;
@@ -800,14 +899,21 @@ public sealed class Settings : Window, IDisposable
using var _color = ImRaii.PushColor(ImGuiCol.Button, Vector4.Zero); using var _color = ImRaii.PushColor(ImGuiCol.Button, Vector4.Zero);
using var _color3 = ImRaii.PushColor(ImGuiCol.ButtonHovered, Vector4.Zero); using var _color3 = ImRaii.PushColor(ImGuiCol.ButtonHovered, Vector4.Zero);
using var _color2 = ImRaii.PushColor(ImGuiCol.ButtonActive, Vector4.Zero); using var _color2 = ImRaii.PushColor(ImGuiCol.ButtonActive, Vector4.Zero);
using var _alpha = ImRaii.PushStyle(ImGuiStyleVar.DisabledAlpha, ImGui.GetStyle().DisabledAlpha * .5f); using var _alpha = ImRaii.PushStyle(
ImGuiStyleVar.DisabledAlpha,
ImGui.GetStyle().DisabledAlpha * .5f
);
foreach (var category in Enum.GetValues<ActionCategory>()) foreach (var category in Enum.GetValues<ActionCategory>())
{ {
if (category == ActionCategory.Combo) if (category == ActionCategory.Combo)
continue; continue;
var actions = category.GetActions(); var actions = category.GetActions();
using var panel = ImRaii2.GroupPanel(category.GetDisplayName(), poolWidth, out var availSpace); using var panel = ImRaii2.GroupPanel(
category.GetDisplayName(),
poolWidth,
out var availSpace
);
var itemsPerRow = (int)MathF.Floor((availSpace + spacing) / (imageSize + spacing)); var itemsPerRow = (int)MathF.Floor((availSpace + spacing) / (imageSize + spacing));
var itemCount = actions.Count; var itemCount = actions.Count;
var iterCount = (int)(Math.Ceiling((float)itemCount / itemsPerRow) * itemsPerRow); var iterCount = (int)(Math.Ceiling((float)itemCount / itemsPerRow) * itemsPerRow);
@@ -828,7 +934,17 @@ public sealed class Settings : Window, IDisposable
iconTint = new(1, 1f, .5f, 1); iconTint = new(1, 1f, .5f, 1);
else if (isRisky) else if (isRisky)
iconTint = new(1, .5f, .5f, 1); iconTint = new(1, .5f, .5f, 1);
if (ImGui.ImageButton(actions[i].GetIcon(recipeData.ClassJob).Handle, new(imageSize), default, Vector2.One, 0, default, iconTint)) if (
ImGui.ImageButton(
actions[i].GetIcon(recipeData.ClassJob).Handle,
new(imageSize),
default,
Vector2.One,
0,
default,
iconTint
)
)
{ {
isDirty = true; isDirty = true;
if (isEnabled) if (isEnabled)
@@ -842,16 +958,18 @@ public sealed class Settings : Window, IDisposable
s.AppendLine(actions[i].GetName(recipeData.ClassJob)); s.AppendLine(actions[i].GetName(recipeData.ClassJob));
if (isInefficient) if (isInefficient)
s.AppendLine( s.AppendLine(
"Not recommended. This action may be randomly used in a " + "Not recommended. This action may be randomly used in a "
"detrimental way to the rest of the craft. Always use " + + "detrimental way to the rest of the craft. Always use "
"your best judgement if enabling this action."); + "your best judgement if enabling this action."
);
if (isRisky) if (isRisky)
s.AppendLine( s.AppendLine(
"Useless; the solver currently doesn't take any risks in " + "Useless; the solver currently doesn't take any risks in "
"its crafts. It only takes steps that have a 100% chance of " + + "its crafts. It only takes steps that have a 100% chance of "
"succeeding. If you want have a moment where you want to take " + + "succeeding. If you want have a moment where you want to take "
"risks in your craft (like in expert recipes), don't rely " + + "risks in your craft (like in expert recipes), don't rely "
"on the solver during that time."); + "on the solver during that time."
);
ImGuiUtils.TooltipWrapped(s.ToString()); ImGuiUtils.TooltipWrapped(s.ToString());
} }
} }
@@ -913,8 +1031,8 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Pin Helper Window", "Pin Helper Window",
"Pins the helper window to the right of your crafting log. Disabling this will " + "Pins the helper window to the right of your crafting log. Disabling this will "
"allow you to move it around.", + "allow you to move it around.",
Config.PinRecipeNoteToWindow, Config.PinRecipeNoteToWindow,
v => Config.PinRecipeNoteToWindow = v, v => Config.PinRecipeNoteToWindow = v,
ref isDirty ref isDirty
@@ -922,8 +1040,8 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Always Collapse Helper Window", "Always Collapse Helper Window",
"Enabling this will cause the Helper Window to be collapsed whenever you start " + "Enabling this will cause the Helper Window to be collapsed whenever you start "
"a new craft, preventing the solver from running automatically.", + "a new craft, preventing the solver from running automatically.",
Config.CollapseSynthHelper, Config.CollapseSynthHelper,
v => Config.CollapseSynthHelper = v, v => Config.CollapseSynthHelper = v,
ref isDirty ref isDirty
@@ -931,11 +1049,11 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Automatically Suggest Macro", "Automatically Suggest Macro",
"(Can cause frame drops!) When navigating to a new recipe or changing your gear " + "(Can cause frame drops!) When navigating to a new recipe or changing your gear "
"stats, automatically suggest a new macro (equivalent to clicking \"Generate\" " + + "stats, automatically suggest a new macro (equivalent to clicking \"Generate\" "
"in the Macro Editor). This can cause harsh frame drops on some computers or " + + "in the Macro Editor). This can cause harsh frame drops on some computers or "
"recipes when underleveled while navigating the crafting log. Turning this off " + + "recipes when underleveled while navigating the crafting log. Turning this off "
"provides a button to allow you to manually suggest a macro only when you need it.", + "provides a button to allow you to manually suggest a macro only when you need it.",
Config.SuggestMacroAutomatically, Config.SuggestMacroAutomatically,
v => Config.SuggestMacroAutomatically = v, v => Config.SuggestMacroAutomatically = v,
ref isDirty ref isDirty
@@ -943,10 +1061,10 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Enable Community Macros", "Enable Community Macros",
"Use FFXIV Teamcraft's community rotations to search for and find the best possible " + "Use FFXIV Teamcraft's community rotations to search for and find the best possible "
"crowd-sourced macro for your craft. This sends a request to their servers to retrieve " + + "crowd-sourced macro for your craft. This sends a request to their servers to retrieve "
"a list of macros that apply to your craft's rlvl. Requests are only sent once per rlvl " + + "a list of macros that apply to your craft's rlvl. Requests are only sent once per rlvl "
"and are always cached to reduce server load.", + "and are always cached to reduce server load.",
Config.ShowCommunityMacros, Config.ShowCommunityMacros,
v => Config.ShowCommunityMacros = v, v => Config.ShowCommunityMacros = v,
ref isDirty ref isDirty
@@ -956,9 +1074,9 @@ public sealed class Settings : Window, IDisposable
{ {
DrawOption( DrawOption(
"Automatically Search for Community Macro", "Automatically Search for Community Macro",
"When navigating to a new recipe or changing your gear stats, automatically search " + "When navigating to a new recipe or changing your gear stats, automatically search "
"online for a new community macro.\n" + + "online for a new community macro.\n"
"This is turned off by default so you don't hammer their servers :)", + "This is turned off by default so you don't hammer their servers :)",
Config.SearchCommunityMacroAutomatically, Config.SearchCommunityMacroAutomatically,
v => Config.SearchCommunityMacroAutomatically = v, v => Config.SearchCommunityMacroAutomatically = v,
ref isDirty ref isDirty
@@ -970,7 +1088,12 @@ public sealed class Settings : Window, IDisposable
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
var solverConfig = Config.RecipeNoteSolverConfig; var solverConfig = Config.RecipeNoteSolverConfig;
DrawSolverConfig(ref solverConfig, SolverConfig.RecipeNoteDefault, false, out var isSolverDirty); DrawSolverConfig(
ref solverConfig,
SolverConfig.RecipeNoteDefault,
false,
out var isSolverDirty
);
if (isSolverDirty) if (isSolverDirty)
{ {
Config.RecipeNoteSolverConfig = solverConfig; Config.RecipeNoteSolverConfig = solverConfig;
@@ -992,7 +1115,12 @@ public sealed class Settings : Window, IDisposable
var isDirty = false; var isDirty = false;
var solverConfig = Config.EditorSolverConfig; var solverConfig = Config.EditorSolverConfig;
DrawSolverConfig(ref solverConfig, SolverConfig.EditorDefault, false, out var isSolverDirty); DrawSolverConfig(
ref solverConfig,
SolverConfig.EditorDefault,
false,
out var isSolverDirty
);
if (isSolverDirty) if (isSolverDirty)
{ {
Config.EditorSolverConfig = solverConfig; Config.EditorSolverConfig = solverConfig;
@@ -1015,8 +1143,8 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Pin Helper Window", "Pin Helper Window",
"Pins the synthesis helper to the right of your synthesis window. Disabling this will " + "Pins the synthesis helper to the right of your synthesis window. Disabling this will "
"allow you to move it around.", + "allow you to move it around.",
Config.PinSynthHelperToWindow, Config.PinSynthHelperToWindow,
v => Config.PinSynthHelperToWindow = v, v => Config.PinSynthHelperToWindow = v,
ref isDirty ref isDirty
@@ -1032,9 +1160,9 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Simulate Only First Step", "Simulate Only First Step",
"Only the first step is simulated by default. You can still " + "Only the first step is simulated by default. You can still "
"hover over the other steps to view their outcomes, but the " + + "hover over the other steps to view their outcomes, but the "
"reliability trials (when hovering over the macro stats) are hidden.", + "reliability trials (when hovering over the macro stats) are hidden.",
Config.SynthHelperDisplayOnlyFirstStep, Config.SynthHelperDisplayOnlyFirstStep,
v => Config.SynthHelperDisplayOnlyFirstStep = v, v => Config.SynthHelperDisplayOnlyFirstStep = v,
ref isDirty ref isDirty
@@ -1042,9 +1170,9 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Draw Ability Ants", "Draw Ability Ants",
"Turns your hotbar into a whack-a-mole game! Draws ants for " + "Turns your hotbar into a whack-a-mole game! Draws ants for "
"the next action that should be executed. Also disables ants " + + "the next action that should be executed. Also disables ants "
"for things like combo actions and condition procs.", + "for things like combo actions and condition procs.",
Config.SynthHelperAbilityAnts, Config.SynthHelperAbilityAnts,
v => Config.SynthHelperAbilityAnts = v, v => Config.SynthHelperAbilityAnts = v,
ref isDirty ref isDirty
@@ -1052,8 +1180,8 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Solver Step Count", "Solver Step Count",
"The minimum number of future steps to solve for during an in-game craft. " + "The minimum number of future steps to solve for during an in-game craft. "
"The solver may still give more than this amount if it's at no cost to you.", + "The solver may still give more than this amount if it's at no cost to you.",
Config.SynthHelperStepCount, Config.SynthHelperStepCount,
1, 1,
100, 100,
@@ -1063,8 +1191,8 @@ public sealed class Settings : Window, IDisposable
DrawOption( DrawOption(
"Max Step Display Count", "Max Step Display Count",
"Enforces a maximum number of steps to display in the synth helper to " + "Enforces a maximum number of steps to display in the synth helper to "
"get rid of clutter.", + "get rid of clutter.",
Config.SynthHelperMaxDisplayCount, Config.SynthHelperMaxDisplayCount,
Config.SynthHelperStepCount, Config.SynthHelperStepCount,
100, 100,
@@ -1077,7 +1205,12 @@ public sealed class Settings : Window, IDisposable
ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.ScaledDummy(5);
var solverConfig = Config.SynthHelperSolverConfig; var solverConfig = Config.SynthHelperSolverConfig;
DrawSolverConfig(ref solverConfig, SolverConfig.SynthHelperDefault, true, out var isSolverDirty); DrawSolverConfig(
ref solverConfig,
SolverConfig.SynthHelperDefault,
true,
out var isSolverDirty
);
if (isSolverDirty) if (isSolverDirty)
{ {
Config.SynthHelperSolverConfig = solverConfig; Config.SynthHelperSolverConfig = solverConfig;
@@ -1110,18 +1243,33 @@ public sealed class Settings : Window, IDisposable
ImGui.Image(icon.Handle, iconDim); ImGui.Image(icon.Handle, iconDim);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGuiUtils.AlignMiddle(new(float.PositiveInfinity, HeaderFont.GetFontSize() + SubheaderFont.GetFontSize() + ImGui.GetFontSize() * 3 + ImGui.GetStyle().ItemSpacing.Y * 4), new(0, iconDim.Y)); ImGuiUtils.AlignMiddle(
new(
float.PositiveInfinity,
HeaderFont.GetFontSize()
+ SubheaderFont.GetFontSize()
+ ImGui.GetFontSize() * 3
+ ImGui.GetStyle().ItemSpacing.Y * 4
),
new(0, iconDim.Y)
);
using (HeaderFont.Push()) using (HeaderFont.Push())
{ {
ImGuiUtils.AlignCentered(ImGui.CalcTextSize("Craftimizer").X); ImGuiUtils.AlignCentered(ImGui.CalcTextSize("Forgeimizer").X);
ImGuiUtils.Hyperlink("Craftimizer", "https://github.com/WorkingRobot/Craftimizer", false); ImGuiUtils.Hyperlink(
"Forgeimizer",
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer",
false
);
} }
using (SubheaderFont.Push()) using (SubheaderFont.Push())
ImGuiUtils.TextCentered($"v{plugin.Version} {plugin.BuildConfiguration}"); ImGuiUtils.TextCentered($"v{plugin.Version} {plugin.BuildConfiguration}");
ImGuiUtils.AlignCentered(ImGui.CalcTextSize($"By {plugin.Author} (WorkingRobot)").X); ImGuiUtils.AlignCentered(
ImGui.CalcTextSize($"By {plugin.Author} (WorkingRobot)").X
);
ImGui.TextUnformatted($"By {plugin.Author} ("); ImGui.TextUnformatted($"By {plugin.Author} (");
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
ImGuiUtils.Hyperlink("WorkingRobot", "https://github.com/WorkingRobot"); ImGuiUtils.Hyperlink("WorkingRobot", "https://github.com/WorkingRobot");
@@ -1179,11 +1327,17 @@ public sealed class Settings : Window, IDisposable
ImGuiUtils.TextWrappedTo("Thank you to "); ImGuiUtils.TextWrappedTo("Thank you to ");
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
ImGuiUtils.Hyperlink("this", "https://dke.maastrichtuniversity.nl/m.winands/documents/multithreadedMCTS2.pdf"); ImGuiUtils.Hyperlink(
"this",
"https://dke.maastrichtuniversity.nl/m.winands/documents/multithreadedMCTS2.pdf"
);
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
ImGuiUtils.TextWrappedTo(", "); ImGuiUtils.TextWrappedTo(", ");
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
ImGuiUtils.Hyperlink("this", "https://liacs.leidenuniv.nl/~plaata1/papers/paper_ICAART18.pdf"); ImGuiUtils.Hyperlink(
"this",
"https://liacs.leidenuniv.nl/~plaata1/papers/paper_ICAART18.pdf"
);
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
ImGuiUtils.TextWrappedTo(", and "); ImGuiUtils.TextWrappedTo(", and ");
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
+193 -58
View File
@@ -1,7 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading;
using Craftimizer.Plugin; using Craftimizer.Plugin;
using Craftimizer.Simulator; using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions; using Craftimizer.Simulator.Actions;
using Craftimizer.Utils; using Craftimizer.Utils;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
@@ -15,12 +21,6 @@ using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Shell; using FFXIVClientStructs.FFXIV.Client.UI.Shell;
using Dalamud.Bindings.ImGui;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading;
using ActionType = Craftimizer.Simulator.Actions.ActionType; using ActionType = Craftimizer.Simulator.Actions.ActionType;
using Sim = Craftimizer.Simulator.Simulator; using Sim = Craftimizer.Simulator.Simulator;
using SimNoRandom = Craftimizer.Simulator.SimulatorNoRandom; using SimNoRandom = Craftimizer.Simulator.SimulatorNoRandom;
@@ -29,12 +29,11 @@ namespace Craftimizer.Windows;
public sealed unsafe class SynthHelper : Window, IDisposable public sealed unsafe class SynthHelper : Window, IDisposable
{ {
private const ImGuiWindowFlags WindowFlagsPinned = WindowFlagsFloating private const ImGuiWindowFlags WindowFlagsPinned =
| ImGuiWindowFlags.NoSavedSettings; WindowFlagsFloating | ImGuiWindowFlags.NoSavedSettings;
private const ImGuiWindowFlags WindowFlagsFloating = private const ImGuiWindowFlags WindowFlagsFloating =
ImGuiWindowFlags.AlwaysAutoResize ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoFocusOnAppearing;
| ImGuiWindowFlags.NoFocusOnAppearing;
private const string WindowNamePinned = "Craftimizer Synthesis Helper###CraftimizerSynthHelper"; private const string WindowNamePinned = "Craftimizer Synthesis Helper###CraftimizerSynthHelper";
private const string WindowNameFloating = $"{WindowNamePinned}Floating"; private const string WindowNameFloating = $"{WindowNamePinned}Floating";
@@ -69,9 +68,12 @@ public sealed unsafe class SynthHelper : Window, IDisposable
private IFontHandle AxisFont { get; } private IFontHandle AxisFont { get; }
public SynthHelper() : base(WindowNamePinned) public SynthHelper()
: base(WindowNamePinned)
{ {
AxisFont = Service.PluginInterface.UiBuilder.FontAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.Axis14)); AxisFont = Service.PluginInterface.UiBuilder.FontAtlas.NewGameFontHandle(
new(GameFontFamilyAndSize.Axis14)
);
Service.Plugin.Hooks.OnActionUsed += OnUseAction; Service.Plugin.Hooks.OnActionUsed += OnUseAction;
@@ -83,7 +85,7 @@ public sealed unsafe class SynthHelper : Window, IDisposable
SizeConstraints = new WindowSizeConstraints SizeConstraints = new WindowSizeConstraints
{ {
MinimumSize = new(494, -1), MinimumSize = new(494, -1),
MaximumSize = new(494, 10000) MaximumSize = new(494, 10000),
}; };
TitleBarButtons = TitleBarButtons =
@@ -93,14 +95,15 @@ public sealed unsafe class SynthHelper : Window, IDisposable
Icon = FontAwesomeIcon.Cog, Icon = FontAwesomeIcon.Cog,
IconOffset = new(2, 1), IconOffset = new(2, 1),
Click = _ => Service.Plugin.OpenSettingsTab("Synthesis Helper"), Click = _ => Service.Plugin.OpenSettingsTab("Synthesis Helper"),
ShowTooltip = () => ImGuiUtils.Tooltip("Open Settings") ShowTooltip = () => ImGuiUtils.Tooltip("Open Settings"),
}, },
new() { new()
{
Icon = FontAwesomeIcon.Heart, Icon = FontAwesomeIcon.Heart,
IconOffset = new(2, 1), IconOffset = new(2, 1),
Click = _ => Util.OpenLink(Plugin.Plugin.SupportLink), Click = _ => Util.OpenLink(Plugin.Plugin.SupportLink),
ShowTooltip = () => ImGuiUtils.Tooltip("Support me on Ko-fi!") ShowTooltip = () => ImGuiUtils.Tooltip("Support me on Ko-fi!"),
} },
]; ];
Service.WindowSystem.AddWindow(this); Service.WindowSystem.AddWindow(this);
@@ -153,13 +156,13 @@ public sealed unsafe class SynthHelper : Window, IDisposable
WasCalculatable = ShouldCalculate; WasCalculatable = ShouldCalculate;
} }
public override bool DrawConditions() => public override bool DrawConditions() => ShouldOpen;
ShouldOpen;
private bool wasInCraftAction; private bool wasInCraftAction;
private bool CalculateShouldOpen() private bool CalculateShouldOpen()
{ {
if (Service.ClientState.LocalPlayer == null) if (Service.Objects.LocalPlayer == null)
return false; return false;
if (!Service.Configuration.EnableSynthHelper) if (!Service.Configuration.EnableSynthHelper)
@@ -209,7 +212,8 @@ public sealed unsafe class SynthHelper : Window, IDisposable
OnStartCrafting(recipeId); OnStartCrafting(recipeId);
OnStateUpdated(); OnStateUpdated();
if (Service.Configuration.CollapseSynthHelper) ShouldCollapse = true; if (Service.Configuration.CollapseSynthHelper)
ShouldCollapse = true;
} }
if (IsRecalculateQueued) if (IsRecalculateQueued)
@@ -228,6 +232,7 @@ public sealed unsafe class SynthHelper : Window, IDisposable
private Vector2? LastPosition { get; set; } private Vector2? LastPosition { get; set; }
private byte? StyleAlpha { get; set; } private byte? StyleAlpha { get; set; }
private byte? LastAlpha { get; set; } private byte? LastAlpha { get; set; }
public override void PreDraw() public override void PreDraw()
{ {
base.PreDraw(); base.PreDraw();
@@ -239,7 +244,9 @@ public sealed unsafe class SynthHelper : Window, IDisposable
ref var unit = ref Addon->AtkUnitBase; ref var unit = ref Addon->AtkUnitBase;
var scale = unit.Scale; var scale = unit.Scale;
var pos = new Vector2(unit.X, unit.Y); var pos = new Vector2(unit.X, unit.Y);
var size = new Vector2(unit.WindowNode->AtkResNode.Width, unit.WindowNode->AtkResNode.Height) * scale; var size =
new Vector2(unit.WindowNode->AtkResNode.Width, unit.WindowNode->AtkResNode.Height)
* scale;
var offset = 5; var offset = 5;
@@ -261,7 +268,10 @@ public sealed unsafe class SynthHelper : Window, IDisposable
WindowName = WindowNameFloating; WindowName = WindowNameFloating;
} }
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, StyleAlpha.HasValue ? (StyleAlpha.Value / 255f) : 1); ImGui.PushStyleVar(
ImGuiStyleVar.Alpha,
StyleAlpha.HasValue ? (StyleAlpha.Value / 255f) : 1
);
} }
public override void PostDraw() public override void PostDraw()
@@ -273,7 +283,6 @@ public sealed unsafe class SynthHelper : Window, IDisposable
public override void Draw() public override void Draw()
{ {
if (ShouldCollapse) if (ShouldCollapse)
{ {
ImGui.SetWindowCollapsed(true); ImGui.SetWindowCollapsed(true);
@@ -298,7 +307,9 @@ public sealed unsafe class SynthHelper : Window, IDisposable
} }
private SimulationState? hoveredState; private SimulationState? hoveredState;
private SimulationState DisplayedState => hoveredState ?? (Service.Configuration.SynthHelperDisplayOnlyFirstStep ? Macro.FirstState : Macro.State); private SimulationState DisplayedState =>
hoveredState
?? (Service.Configuration.SynthHelperDisplayOnlyFirstStep ? Macro.FirstState : Macro.State);
private void DrawMacro() private void DrawMacro()
{ {
@@ -308,7 +319,11 @@ public sealed unsafe class SynthHelper : Window, IDisposable
var lastState = Macro.InitialState; var lastState = Macro.InitialState;
hoveredState = null; hoveredState = null;
var itemsPerRow = (int)Math.Max(1, MathF.Floor((ImGui.GetContentRegionAvail().X + spacing) / (imageSize + spacing))); var itemsPerRow = (int)
Math.Max(
1,
MathF.Floor((ImGui.GetContentRegionAvail().X + spacing) / (imageSize + spacing))
);
using var _color = ImRaii.PushColor(ImGuiCol.Button, Vector4.Zero); using var _color = ImRaii.PushColor(ImGuiCol.Button, Vector4.Zero);
using var _color3 = ImRaii.PushColor(ImGuiCol.ButtonHovered, Vector4.Zero); using var _color3 = ImRaii.PushColor(ImGuiCol.ButtonHovered, Vector4.Zero);
@@ -328,9 +343,18 @@ public sealed unsafe class SynthHelper : Window, IDisposable
var offsetVec2 = ImGui.GetStyle().ItemSpacing / 2; var offsetVec2 = ImGui.GetStyle().ItemSpacing / 2;
var offset = new Vector2((offsetVec2.X + offsetVec2.Y) / 2f); var offset = new Vector2((offsetVec2.X + offsetVec2.Y) / 2f);
var color = canExecute ? ImGuiColors.DalamudWhite2 : ImGuiColors.DalamudGrey3; var color = canExecute ? ImGuiColors.DalamudWhite2 : ImGuiColors.DalamudGrey3;
ImGui.GetWindowDrawList().AddRectFilled(pos - offset, pos + new Vector2(imageSize) + offset, ImGui.GetColorU32(color), 4); ImGui
.GetWindowDrawList()
.AddRectFilled(
pos - offset,
pos + new Vector2(imageSize) + offset,
ImGui.GetColorU32(color),
4
);
} }
bool isHovered, isHeld, isPressed; bool isHovered,
isHeld,
isPressed;
{ {
var pos = ImGui.GetCursorScreenPos(); var pos = ImGui.GetCursorScreenPos();
var offset = ImGui.GetStyle().ItemSpacing / 2f; var offset = ImGui.GetStyle().ItemSpacing / 2f;
@@ -344,9 +368,23 @@ public sealed unsafe class SynthHelper : Window, IDisposable
var id = ImGui.GetID($"###ButtonContainer"); var id = ImGui.GetID($"###ButtonContainer");
var isClipped = !ImGuiExtras.ItemAdd(bb, id, out _, 0); var isClipped = !ImGuiExtras.ItemAdd(bb, id, out _, 0);
isPressed = ImGuiExtras.ButtonBehavior(bb, id, out isHovered, out isHeld, ImGuiButtonFlags.None); isPressed = ImGuiExtras.ButtonBehavior(
bb,
id,
out isHovered,
out isHeld,
ImGuiButtonFlags.None
);
} }
ImGui.ImageButton(action.GetIcon(RecipeData!.ClassJob).Handle, new(imageSize), default, Vector2.One, 0, default, failedAction ? new(1, 1, 1, ImGui.GetStyle().DisabledAlpha) : Vector4.One); ImGui.ImageButton(
action.GetIcon(RecipeData!.ClassJob).Handle,
new(imageSize),
default,
Vector2.One,
0,
default,
failedAction ? new(1, 1, 1, ImGui.GetStyle().DisabledAlpha) : Vector4.One
);
if (isPressed && i == 0) if (isPressed && i == 0)
{ {
if (ExecuteNextAction()) if (ExecuteNextAction())
@@ -354,15 +392,21 @@ public sealed unsafe class SynthHelper : Window, IDisposable
} }
if (isHovered) if (isHovered)
{ {
ImGuiUtils.Tooltip($"{action.GetName(RecipeData!.ClassJob)}\n" + ImGuiUtils.Tooltip(
$"{actionBase.GetTooltip(CreateSim(lastState), true)}" + $"{action.GetName(RecipeData!.ClassJob)}\n"
$"{(canExecute && i == 0 ? "Click or run /craftaction to execute" : string.Empty)}"); + $"{actionBase.GetTooltip(CreateSim(lastState), true)}"
+ $"{(canExecute && i == 0 ? "Click or run /craftaction to execute" : string.Empty)}"
);
hoveredState = state; hoveredState = state;
} }
lastState = state; lastState = state;
} }
var rows = (int)Math.Max(1, MathF.Ceiling(Service.Configuration.SynthHelperMaxDisplayCount / itemsPerRow)); var rows = (int)
Math.Max(
1,
MathF.Ceiling(Service.Configuration.SynthHelperMaxDisplayCount / itemsPerRow)
);
for (var i = 0; i < rows; ++i) for (var i = 0; i < rows; ++i)
{ {
if (count <= i * itemsPerRow) if (count <= i * itemsPerRow)
@@ -381,7 +425,15 @@ public sealed unsafe class SynthHelper : Window, IDisposable
var iconHeight = ImGui.GetFrameHeight() * 1.75f; var iconHeight = ImGui.GetFrameHeight() * 1.75f;
var durationShift = iconHeight * .2f; var durationShift = iconHeight * .2f;
ImGui.Dummy(new(0, iconHeight + ImGui.GetStyle().ItemSpacing.Y + ImGui.GetTextLineHeight() - durationShift)); ImGui.Dummy(
new(
0,
iconHeight
+ ImGui.GetStyle().ItemSpacing.Y
+ ImGui.GetTextLineHeight()
- durationShift
)
);
ImGui.SameLine(0, 0); ImGui.SameLine(0, 0);
var effects = state.ActiveEffects; var effects = state.ActiveEffects;
@@ -413,35 +465,92 @@ public sealed unsafe class SynthHelper : Window, IDisposable
} }
} }
var reliability = Macro.GetReliability(RecipeData!, Service.Configuration.SynthHelperDisplayOnlyFirstStep ? 0 : ^1); var reliability = Macro.GetReliability(
RecipeData!,
Service.Configuration.SynthHelperDisplayOnlyFirstStep ? 0 : ^1
);
{ {
var mainBars = new List<DynamicBars.BarData>() var mainBars = new List<DynamicBars.BarData>()
{ {
new("Progress", Colors.Progress, reliability.Progress, state.Progress, RecipeData!.RecipeInfo.MaxProgress), new(
new("Quality", Colors.Quality, reliability.Quality, state.Quality, RecipeData.RecipeInfo.MaxQuality), "Progress",
Colors.Progress,
reliability.Progress,
state.Progress,
RecipeData!.RecipeInfo.MaxProgress
),
new(
"Quality",
Colors.Quality,
reliability.Quality,
state.Quality,
RecipeData.RecipeInfo.MaxQuality
),
new("CP", Colors.CP, state.CP, CharacterStats!.CP), new("CP", Colors.CP, state.CP, CharacterStats!.CP),
}; };
if (RecipeData.RecipeInfo.MaxQuality <= 0) if (RecipeData.RecipeInfo.MaxQuality <= 0)
mainBars.RemoveAt(1); mainBars.RemoveAt(1);
var halfBars = new List<DynamicBars.BarData>() var halfBars = new List<DynamicBars.BarData>()
{ {
new("Durability", Colors.Durability, state.Durability, RecipeData.RecipeInfo.MaxDurability), new(
"Durability",
Colors.Durability,
state.Durability,
RecipeData.RecipeInfo.MaxDurability
),
}; };
if (RecipeData.IsCollectable) if (RecipeData.IsCollectable)
halfBars.Add(new("Collectability", Colors.Collectability, reliability.ParamScore, state.Collectability, state.MaxCollectability, RecipeData.CollectableThresholds, $"{state.Collectability}", $"{state.MaxCollectability:0}")); halfBars.Add(
new(
"Collectability",
Colors.Collectability,
reliability.ParamScore,
state.Collectability,
state.MaxCollectability,
RecipeData.CollectableThresholds,
$"{state.Collectability}",
$"{state.MaxCollectability:0}"
)
);
else if (RecipeData.Recipe.RequiredQuality > 0) else if (RecipeData.Recipe.RequiredQuality > 0)
{ {
var qualityPercent = (float)state.Quality / RecipeData.Recipe.RequiredQuality * 100; var qualityPercent = (float)state.Quality / RecipeData.Recipe.RequiredQuality * 100;
halfBars.Add(new("Quality %", Colors.HQ, reliability.ParamScore, qualityPercent, 100, null, $"{qualityPercent:0}%", null)); halfBars.Add(
new(
"Quality %",
Colors.HQ,
reliability.ParamScore,
qualityPercent,
100,
null,
$"{qualityPercent:0}%",
null
)
);
} }
else if (RecipeData.RecipeInfo.MaxQuality > 0) else if (RecipeData.RecipeInfo.MaxQuality > 0)
halfBars.Add(new("HQ %", Colors.HQ, reliability.ParamScore, state.HQPercent, 100, null, $"{state.HQPercent}%", null)); halfBars.Add(
new(
"HQ %",
Colors.HQ,
reliability.ParamScore,
state.HQPercent,
100,
null,
$"{state.HQPercent}%",
null
)
);
if (halfBars.Count > 1) if (halfBars.Count > 1)
{ {
var textSize = DynamicBars.GetTextSize(mainBars.Concat(halfBars)); var textSize = DynamicBars.GetTextSize(mainBars.Concat(halfBars));
DynamicBars.Draw(mainBars, textSize); DynamicBars.Draw(mainBars, textSize);
using var table = ImRaii.Table($"##{nameof(SynthHelper)}_halfbars", halfBars.Count, ImGuiTableFlags.NoPadOuterX | ImGuiTableFlags.SizingStretchSame); using var table = ImRaii.Table(
$"##{nameof(SynthHelper)}_halfbars",
halfBars.Count,
ImGuiTableFlags.NoPadOuterX | ImGuiTableFlags.SizingStretchSame
);
if (table) if (table)
{ {
foreach (var bar in halfBars) foreach (var bar in halfBars)
@@ -467,7 +576,9 @@ public sealed unsafe class SynthHelper : Window, IDisposable
using var _disabled = ImRaii.Disabled(); using var _disabled = ImRaii.Disabled();
ImGui.Button("Stopping", new(-1, 0)); ImGui.Button("Stopping", new(-1, 0));
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGuiUtils.TooltipWrapped("This might could a while, sorry! Please report if this takes longer than a second."); ImGuiUtils.TooltipWrapped(
"This might could a while, sorry! Please report if this takes longer than a second."
);
} }
else else
{ {
@@ -480,13 +591,22 @@ public sealed unsafe class SynthHelper : Window, IDisposable
if (ImGui.Button("Retry", new(-1, 0))) if (ImGui.Button("Retry", new(-1, 0)))
AttemptRetry(); AttemptRetry();
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGuiUtils.TooltipWrapped("Suggest a way to finish the crafting recipe. " + ImGuiUtils.TooltipWrapped(
"Results aren't perfect, and levels of success " + "Suggest a way to finish the crafting recipe. "
"can vary wildly depending on the solver's settings."); + "Results aren't perfect, and levels of success "
+ "can vary wildly depending on the solver's settings."
);
} }
if (ImGui.Button("Open in Macro Editor", new(-1, 0))) if (ImGui.Button("Open in Macro Editor", new(-1, 0)))
Service.Plugin.OpenMacroEditor(CharacterStats!, RecipeData!, new(Service.ClientState.LocalPlayer!.StatusList), null, [], null); Service.Plugin.OpenMacroEditor(
CharacterStats!,
RecipeData!,
new(Service.Objects.LocalPlayer!.StatusList),
null,
[],
null
);
} }
public bool ExecuteNextAction() public bool ExecuteNextAction()
@@ -519,13 +639,20 @@ public sealed unsafe class SynthHelper : Window, IDisposable
{ {
var gearStats = Gearsets.CalculateGearsetCurrentStats(); var gearStats = Gearsets.CalculateGearsetCurrentStats();
var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems); var container = InventoryManager
.Instance()
->GetInventoryContainer(InventoryType.EquippedItems);
if (container == null) if (container == null)
throw new InvalidOperationException("Could not get inventory container"); throw new InvalidOperationException("Could not get inventory container");
var gearItems = Gearsets.GetGearsetItems(container); var gearItems = Gearsets.GetGearsetItems(container);
var characterStats = Gearsets.CalculateCharacterStats(gearStats, gearItems, RecipeData.ClassJob.GetPlayerLevel(), RecipeData.ClassJob.CanPlayerUseManipulation()); var characterStats = Gearsets.CalculateCharacterStats(
gearStats,
gearItems,
RecipeData.ClassJob.GetPlayerLevel(),
RecipeData.ClassJob.CanPlayerUseManipulation()
);
if (characterStats != CharacterStats) if (characterStats != CharacterStats)
{ {
CharacterStats = characterStats; CharacterStats = characterStats;
@@ -554,12 +681,11 @@ public sealed unsafe class SynthHelper : Window, IDisposable
CurrentActionStates = CurrentState.ActionStates; CurrentActionStates = CurrentState.ActionStates;
} }
private void RefreshCurrentState() => private void RefreshCurrentState() => CurrentState = GetCurrentState();
CurrentState = GetCurrentState();
private SimulationState GetCurrentState() private SimulationState GetCurrentState()
{ {
var player = Service.ClientState.LocalPlayer!; var player = Service.Objects.LocalPlayer!;
var values = new SynthesisValues(Addon); var values = new SynthesisValues(Addon);
var statusManager = ((Character*)player.Address)->GetStatusManager(); var statusManager = ((Character*)player.Address)->GetStatusManager();
@@ -602,7 +728,7 @@ public sealed unsafe class SynthHelper : Window, IDisposable
TrainedPerfection = HasEffect((ushort)EffectType.TrainedPerfection.StatusId()), TrainedPerfection = HasEffect((ushort)EffectType.TrainedPerfection.StatusId()),
HeartAndSoul = HasEffect((ushort)EffectType.HeartAndSoul.StatusId()), HeartAndSoul = HasEffect((ushort)EffectType.HeartAndSoul.StatusId()),
}, },
ActionStates = CurrentActionStates ActionStates = CurrentActionStates,
}; };
} }
@@ -638,7 +764,11 @@ public sealed unsafe class SynthHelper : Window, IDisposable
SolverTask.Start(); SolverTask.Start();
} }
private int CalculateBestMacroTask(SimulationState state, CancellationToken token, bool hasDelineations) private int CalculateBestMacroTask(
SimulationState state,
CancellationToken token,
bool hasDelineations
)
{ {
var config = Service.Configuration.SynthHelperSolverConfig; var config = Service.Configuration.SynthHelperSolverConfig;
var canUseDelineations = !Service.Configuration.CheckDelineations || hasDelineations; var canUseDelineations = !Service.Configuration.CheckDelineations || hasDelineations;
@@ -649,7 +779,7 @@ public sealed unsafe class SynthHelper : Window, IDisposable
var solver = new Solver.Solver(config, state) { Token = token }; var solver = new Solver.Solver(config, state) { Token = token };
solver.OnLog += Log.Debug; solver.OnLog += Log.Debug;
solver.OnWarn += t => Service.Plugin.DisplaySolverWarning(t); solver.OnWarn += t => Plugin.Plugin.DisplaySolverWarning(t);
solver.OnNewAction += EnqueueAction; solver.OnNewAction += EnqueueAction;
SolverObject = solver; SolverObject = solver;
solver.Start(); solver.Start();
@@ -663,12 +793,17 @@ public sealed unsafe class SynthHelper : Window, IDisposable
private void EnqueueAction(ActionType action) private void EnqueueAction(ActionType action)
{ {
var newSize = Macro.Enqueue(action, Service.Configuration.SynthHelperMaxDisplayCount); var newSize = Macro.Enqueue(action, Service.Configuration.SynthHelperMaxDisplayCount);
if (newSize >= Service.Configuration.SynthHelperStepCount || newSize >= Service.Configuration.SynthHelperMaxDisplayCount) if (
newSize >= Service.Configuration.SynthHelperStepCount
|| newSize >= Service.Configuration.SynthHelperMaxDisplayCount
)
SolverTask?.Cancel(); SolverTask?.Cancel();
} }
private static Sim CreateSim(in SimulationState state) => private static Sim CreateSim(in SimulationState state) =>
Service.Configuration.ConditionRandomness ? new Sim() { State = state } : new SimNoRandom() { State = state }; Service.Configuration.ConditionRandomness
? new Sim() { State = state }
: new SimNoRandom() { State = state };
public void Dispose() public void Dispose()
{ {
+19 -19
View File
@@ -1,18 +1,18 @@
{ {
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net9.0-windows7.0": { "net10.0-windows7.0": {
"DalamudPackager": { "DalamudPackager": {
"type": "Direct", "type": "Direct",
"requested": "[13.0.0, )", "requested": "[15.0.0, )",
"resolved": "13.0.0", "resolved": "15.0.0",
"contentHash": "Mb3cUDSK/vDPQ8gQIeuCw03EMYrej1B4J44a1AvIJ9C759p9XeqdU9Hg4WgOmlnlPe0G7ILTD32PKSUpkQNa8w==" "contentHash": "411vwC8/X8Z/sQ2TI6v3SvOn66xFPeOjFn3Zn+h0d3Ox2t1kFm66AhDvmx/qcMwVrR+Hidxj0dadpQ2dgyXMBQ=="
}, },
"DotNet.ReproducibleBuilds": { "DotNet.ReproducibleBuilds": {
"type": "Direct", "type": "Direct",
"requested": "[1.2.25, )", "requested": "[1.2.39, )",
"resolved": "1.2.25", "resolved": "1.2.39",
"contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
}, },
"MathNet.Numerics": { "MathNet.Numerics": {
"type": "Direct", "type": "Direct",
@@ -22,22 +22,22 @@
}, },
"Meziantou.Analyzer": { "Meziantou.Analyzer": {
"type": "Direct", "type": "Direct",
"requested": "[2.0.199, )", "requested": "[2.0.264, )",
"resolved": "2.0.199", "resolved": "2.0.264",
"contentHash": "y8oRrTLDBw1b10pWci/PnFoahdIMflNSlVheL9kzUylAASnoJPFyvuyaNXcrbOTNOEk1aMLFRr1mSX/xvYR15g==" "contentHash": "zRG13RDG446rZNdd/YjKRd4utpbjleRDUqNQSrX0etMnH8Rz9NBlXUpS5aR2ExoOokhNfkdOW8HpLzjLj5x0hQ=="
}, },
"DotNext": { "DotNext": {
"type": "Transitive", "type": "Transitive",
"resolved": "5.21.0", "resolved": "5.26.1",
"contentHash": "fU63OJVSDSsOl6adjNM8e5zmyhdZkX2ztvmSeW6lBjFdvFG8ZwMOrJ+L8Ih/2yKr0cuaV8PNwnhDrlS4MFf14A==", "contentHash": "rcy6Yrpb64B7qYm/+D+4sfBUzwX/IOGeJBYReDL8/TAIp8aZrZPtXx8nwkYjWCuakYn2tBppwefIM6pd/cOnMw==",
"dependencies": { "dependencies": {
"System.IO.Hashing": "8.0.0" "System.IO.Hashing": "8.0.0"
} }
}, },
"Raphael.Net": { "Raphael.Net": {
"type": "Transitive", "type": "Transitive",
"resolved": "3.0.0", "resolved": "4.1.0",
"contentHash": "9yY+jR2gddw52HtShBL/JikQ4gZa8lxFYSUIi2y510HLsaK86iqtuRgrv8/akrHR66QRWEPtmHXyehW1hrmP9Q==" "contentHash": "KRkRUn5gdFwKMpZXtFC9W58lNnhSXQpvNmLaCn7f9Tbx92UhRfttC7/QqyWhjkD2u6L9YNIpNp3nxF6awEOntw=="
}, },
"System.IO.Hashing": { "System.IO.Hashing": {
"type": "Transitive", "type": "Transitive",
@@ -51,16 +51,16 @@
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"Craftimizer.Simulator": "[1.0.0, )", "Craftimizer.Simulator": "[1.0.0, )",
"DotNext": "[5.21.0, )", "DotNext": "[5.26.1, )",
"Raphael.Net": "[3.0.0, )" "Raphael.Net": "[4.1.0, )"
} }
} }
}, },
"net9.0-windows7.0/win-x64": { "net10.0-windows7.0/win-x64": {
"Raphael.Net": { "Raphael.Net": {
"type": "Transitive", "type": "Transitive",
"resolved": "3.0.0", "resolved": "4.1.0",
"contentHash": "9yY+jR2gddw52HtShBL/JikQ4gZa8lxFYSUIi2y510HLsaK86iqtuRgrv8/akrHR66QRWEPtmHXyehW1hrmP9Q==" "contentHash": "KRkRUn5gdFwKMpZXtFC9W58lNnhSXQpvNmLaCn7f9Tbx92UhRfttC7/QqyWhjkD2u6L9YNIpNp3nxF6awEOntw=="
} }
} }
} }
+104
View File
@@ -0,0 +1,104 @@
# Notice
## Acknowledgements
Forgeimizer is built on
[Craftimizer](https://github.com/WorkingRobot/Craftimizer) by
**[Asriel Camora](https://github.com/WorkingRobot)**, who built and
maintained the plugin for years. The entire architecture, the crafting
simulator, the macro solver, the recipe data layer, the synthesis helper,
and every UI window come from Asriel's work. Without Craftimizer, Forgeimizer
would not exist.
If Forgeimizer is useful to you, the credit for that belongs in large part
to Asriel.
## A direct word to Asriel
Hi. I am Jon. I forked Craftimizer because the upstream `main` branch had
not received an update past FFXIV 7.4, and I wanted a working crafting
plugin for personal use that still loads on the current Dalamud API. This
is not a "do it better" fork. The opposite is true. I would have happily
kept using upstream if it had stayed current.
What this fork adds is purely:
1. A Dalamud SDK 14 → 15 migration cycle. Three changes in
`FFXIVClientStructs` and Dalamud's `ImRaii` namespace were renamed or
removed; this fork ports the affected call sites to the new symbol
names and adds a local `IEndObject` interface to replace the nested
one Dalamud removed.
2. A user-facing rename to **Forgeimizer** so the plugin installs into
its own Dalamud slot and does not collide with your build in the
plugin list. A conflict detector hard-blocks the load if your
upstream `Craftimizer` plugin is active. Internal namespaces stay as
`Craftimizer.*`, the source tree structure is identical, and the
only renamed strings are the assembly name, the manifest, the window
system name, the about-tab title, the MacroMate default name, and a
`/forgeimizer` slash-command alias next to your `/craftimizer`.
No new features, no design departures, no rebranding of internal
namespaces. Where I had to modify your sources I kept the edits minimal,
isolated to the lines the SDK 15 compiler refused, and revertible.
If you pick Craftimizer back up and ship an upstream SDK 15 update,
Forgeimizer folds back into a pure mirror and the recommendation will be
to switch back to your build.
If anything in this fork ever steps on something you would not be okay
with — attribution, the `Forgeimizer` naming, the conflict detector
language, anything else — please reach out and I will fix it. Genuinely.
## Why this fork exists
The upstream `main` last commit is "Update for 7.4" from late 2025. The
plugin is otherwise dormant: no SDK 15 branch, no public roadmap, no
recent issue activity from the maintainer. FFXIV 7.5 brought the Dalamud
API jump from level 14 to 15 in early 2026, and Craftimizer stopped
loading from then on. My personal crafting workflow depends on it, so I
took the maintenance burden on for a while.
Forgeimizer is maintained for personal and friend-circle use under Hellion
Forge. It is not a competing project and does not target the broader
Craftimizer user base. The rename to Forgeimizer exists so users who
install through the Hellion Forge custom repo can run it side-by-side with
the upstream Craftimizer install in the Dalamud plugin list without one
silently shadowing the other.
## Why this fork is not upstreamed
I did not open a pull request because the upstream repository has been
quiet for months and there is no signal that maintenance bandwidth is
available on Asriel's side. A drive-by SDK-15-bump PR with no maintainer
to land it would sit open indefinitely and create the wrong impression
that the change has been reviewed. If upstream becomes active again and
wants the migration, the change set is small enough (the SDK 15 patch is
+23 / 19 lines across seven files; the rebrand is roughly a dozen lines
on top) to merge by hand in minutes.
## Maintainer contact
If something in Forgeimizer causes problems, especially if it relates
back to upstream Craftimizer or to attribution:
- **Gitea Issues:**
[JonKazama-Hellion/Craftimizer/issues](https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/issues)
- **Discord:** `@j.j_kazama`
- **Email (business):** <kontakt@hellion-media.de>
I respond on weekdays during European business hours. For attribution
or takedown questions, email is the fastest path.
## Trademarks and naming
"Craftimizer" is the name Asriel chose for the upstream plugin. The
Forgeimizer rebrand keeps Craftimizer prominently named in the manifest
description, the about-tab attribution, this NOTICE, and the COPYRIGHT
block, so the fork relationship is unambiguous. The Hellion brand is
mine.
---
Maintained under **Hellion Forge**, the modding and plugin line of
**Hellion Online Media** | Bad Harzburg |
[hellion-media.de](https://hellion-media.de)
+176 -2
View File
@@ -1,3 +1,177 @@
# Craftimizer # Forgeimizer
soon(tm) [![Build](https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/actions/workflows/build.yml/badge.svg?branch=main)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/actions/workflows/build.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Latest release](https://img.shields.io/badge/release-v0.1.0-brightgreen)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/releases/latest)
[![Dalamud API](https://img.shields.io/badge/Dalamud-API_15-purple)](https://github.com/goatcorp/Dalamud)
[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/)
[![FFXIV](https://img.shields.io/badge/FFXIV-7.5+-c3a37f)](https://www.finalfantasyxiv.com/)
[![Upstream](https://img.shields.io/badge/upstream-WorkingRobot%2FCraftimizer-blue)](https://github.com/WorkingRobot/Craftimizer)
<p align="center">
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
</p>
**Version 0.1.0** — a Hellion Forge maintenance fork of
[Craftimizer](https://github.com/WorkingRobot/Craftimizer) 2.9.1.1 by
[Asriel Camora](https://github.com/WorkingRobot), brought back to life on
Dalamud SDK 15 for FFXIV 7.5+. Installs as a standalone plugin under the name
**Forgeimizer**, refuses to load while upstream Craftimizer is active.
Forgeimizer uses an independent version line (starting at 0.1.0). The
upstream Craftimizer version the current fork is built on is always
called out in the description above and the changelog entry for each
release. See [`CHANGELOG.md`](CHANGELOG.md) for the full release history.
Upstream Craftimizer received its last update for FFXIV 7.4 in late 2025 and
has been dormant since. With the Dalamud API jump from level 14 to 15 in
early 2026, the plugin stopped loading. Forgeimizer is a narrow
API-compatibility cycle that gets the crafting plugin back on its feet for
personal and friend-circle use. No features added, no design changes, no
behavior departures from upstream. If Asriel ships an official SDK 15 update
upstream, Forgeimizer folds back into a pure mirror and the recommendation
will be to switch back.
## Acknowledgements
Every line of crafting logic, every solver heuristic, the entire simulator,
the recipe data layer, the synthesis hooks, and all UI windows are
**[Asriel Camora](https://github.com/WorkingRobot)**'s work. Forgeimizer only
unblocks the API path and changes the user-facing plugin identity. Full
acknowledgement in [NOTICE.md](NOTICE.md).
Forgeimizer is maintained under **Hellion Forge**, the modding and plugin
line of [Hellion Online Media](https://hellion-media.de).
---
## What this fork changes
| Area | Upstream Craftimizer 2.9.1.1 | Forgeimizer |
| ----------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------- |
| Plugin display name (Dalamud installer) | `Craftimizer` | **`Forgeimizer`** |
| InternalName (config slot) | `Craftimizer` | **`Forgeimizer`** (separate `pluginConfigs/Forgeimizer/`) |
| Assembly | `Craftimizer.dll` | **`Forgeimizer.dll`** |
| Conflict handling | n/a | Throws `InvalidOperationException` on load if upstream `Craftimizer` is active |
| Dalamud SDK | 14.0.1 | **15.0.0** |
| Dalamud API Level | 14 | **15** |
| `FFXIVClientStructs.FFXIV.Component.GUI` type | `ValueType` | `AtkValueType` (upstream rename) |
| `Dalamud.Interface.Utility.Raii.ImRaii` nested | `IEndObject`, `Color` (removed in SDK15) | local `IEndObject` interface in `ImRaii2.cs`; typed `ImRaii.*Disposable` returns |
| `Settings.TabItem` ref-bool lifetime | `var open = true; TabItem(label, ref open, flags)` (rejected by SDK 15 escape analysis) | `TabItem(label, flags)` overload (no ref) |
Internal namespaces (`Craftimizer.*`) are intentionally left alone. This is a
deliberate light-rename so a future upstream merge stays simple.
---
## Tech Stack
| Category | Technology |
| ---------- | ------------------------------------------------------- |
| Platform | Dalamud Plugin (API Level 15) |
| Language | C# / .NET 10 (`net10.0-windows`) |
| Build | Dalamud.NET.Sdk 15.0.0 |
| UI | Dear ImGui via Dalamud bindings |
| Solver | Raphael (Rust crate, bundled as `raphael_bindings.dll`) |
| Toolchain | dotnet 10 SDK |
---
## Install
### From custom repository (recommended, friend-circle install)
> Coming once the release pipeline lands. Once active, point Dalamud at
> the `repo.json` URL from this repository root and `Forgeimizer` shows
> up in the **All Plugins** list.
### From source (dev install)
```bash
git clone ssh://git@gitea.hellion-forge.cloud:2222/JonKazama-Hellion/Craftimizer.git
cd Craftimizer
dotnet build Craftimizer.sln -c Release
```
The plugin DLL plus manifest land in `Craftimizer/bin/x64/Release/`. Point
Dalamud's **Dev Plugin Locations** at that folder (`/xlsettings`
**Experimental**) to load it as a dev plugin.
### Conflict with upstream Craftimizer
Forgeimizer refuses to load if upstream Craftimizer is also active. Both
plugins hook the same FFXIV `UseAction` and `IsActionHighlighted` paths, and
running them in parallel would corrupt either's state. The Forgeimizer
constructor throws an `InvalidOperationException` with a clear message
pointing at `/xlplugins` if it detects an active Craftimizer install.
Disable upstream Craftimizer in `/xlplugins` before enabling Forgeimizer.
---
## Project Status
**Experimental Hellion fork — small-circle use.** Custom repo and release
pipeline are next on the roadmap. If upstream Craftimizer resumes active
maintenance, Forgeimizer archives and the recommendation will be to switch
back upstream.
---
## Slash Commands
All upstream Craftimizer slash commands work unchanged, plus one alias:
| Command | What it does |
| -------------------------------------- | ------------------------------------------------ |
| `/craftimizer` _or_ **`/forgeimizer`** | Open the settings window |
| `/crafteditor` _or_ `/macroeditor` | Open the crafting macro editor |
| `/craftaction` | Execute the next suggested action in synth helper |
| `/craftretry` | Click "Retry" in the synthesis helper |
| `/craftmacros` _or_ `/macrolist` | Open the crafting macros window |
---
## Community & Support
- Bug reports and questions:
[Gitea Issues](https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/issues)
- Discord DM: `@j.j_kazama`
- Email (business): <kontakt@hellion-media.de>
For anything that relates back to upstream Craftimizer or to attribution,
see the contact path in [NOTICE.md](NOTICE.md).
---
## License
MIT (same license as upstream Craftimizer). Full text in [LICENSE](LICENSE).
Copyright details with dual-holder block in [COPYRIGHT](COPYRIGHT). Personal
acknowledgement to the upstream author in [NOTICE.md](NOTICE.md).
© 2023 [Asriel Camora](https://github.com/WorkingRobot) for the original
Craftimizer plugin. © 2026 Hellion Online Media for the Forgeimizer
rebrand and the Dalamud SDK 15 fork-maintenance modifications.
### FFXIV Disclaimer
FINAL FANTASY XIV © SQUARE ENIX CO., LTD. All rights reserved. Craftimizer
and Forgeimizer are unofficial, fan-made plugins and are not affiliated
with, supported by, sponsored by, or approved by Square Enix.
---
## Project Documents
| Document | Contents |
| -------------------------- | -------------------------------------------------------------- |
| [`LICENSE`](LICENSE) | MIT licence full text (Asriel Camora original notice intact). |
| [`COPYRIGHT`](COPYRIGHT) | Dual-holder copyright block, visual asset attribution. |
| [`NOTICE.md`](NOTICE.md) | Acknowledgement to upstream author, scope of fork, contact. |
---
Maintained under **Hellion Forge**, the modding and plugin line of
**Hellion Online Media** | Bad Harzburg |
[hellion-media.de](https://hellion-media.de)
+2 -2
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Platforms>x64</Platforms> <Platforms>x64</Platforms>
@@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.199"> <PackageReference Include="Meziantou.Analyzer" Version="2.0.264">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
+4 -4
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks> <AllowUnsafeBlocks>True</AllowUnsafeBlocks>
@@ -10,12 +10,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DotNext" Version="5.21.0" /> <PackageReference Include="DotNext" Version="5.26.1" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.199"> <PackageReference Include="Meziantou.Analyzer" Version="2.0.264">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Raphael.Net" Version="3.0.0" /> <PackageReference Include="Raphael.Net" Version="4.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+1
View File
@@ -209,6 +209,7 @@ public sealed class Solver : IDisposable
{ {
Adversarial = Config.Adversarial, Adversarial = Config.Adversarial,
BackloadProgress = Config.BackloadProgress, BackloadProgress = Config.BackloadProgress,
AllowNonMaxQualitySolutions = true,
LogLevel = Raphael.LevelFilter.Debug, LogLevel = Raphael.LevelFilter.Debug,
ThreadCount = (ushort)Config.MaxThreadCount, ThreadCount = (ushort)Config.MaxThreadCount,
}; };
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+12
View File
@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/dotnet-tools.json",
"version": 1,
"isRoot": true,
"tools": {
"csharpier": {
"version": "1.2.6",
"commands": ["csharpier"],
"rollForward": false
}
}
}
+49
View File
@@ -0,0 +1,49 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":dependencyDashboard",
":semanticCommits",
":timezone(Europe/Berlin)",
"schedule:weekly"
],
"labels": ["dependencies", "renovate"],
"assignees": ["JonKazama-Hellion"],
"prHourlyLimit": 10,
"prConcurrentLimit": 20,
"rebaseWhen": "behind-base-branch",
"packageRules": [
{
"description": "Group all minor and patch updates per ecosystem in one PR",
"matchUpdateTypes": ["minor", "patch"],
"groupName": "minor and patch updates ({{manager}})"
},
{
"description": "Major updates always get their own PR with breaking-change label",
"matchUpdateTypes": ["major"],
"labels": ["dependencies", "major-update", "breaking-change"],
"addLabels": ["needs-review"]
},
{
"description": "Dev dependencies in their own group",
"matchDepTypes": ["devDependencies"],
"groupName": "dev dependencies"
},
{
"description": "Pin GitHub Action versions by SHA for supply-chain hygiene",
"matchManagers": ["github-actions"],
"pinDigests": true,
"ignorePaths": [".gitea/workflows/**"]
}
],
"vulnerabilityAlerts": {
"labels": ["security", "vulnerability"],
"schedule": ["at any time"]
},
"lockFileMaintenance": {
"enabled": true,
"schedule": ["before 6am on monday"],
"commitMessageAction": "Refresh"
},
"osvVulnerabilityAlerts": true
}
+49
View File
@@ -0,0 +1,49 @@
[
{
"Author": "Asriel Camora (original); Jon Kazama / Hellion Forge (fork)",
"Name": "Forgeimizer",
"InternalName": "Forgeimizer",
"AssemblyVersion": "0.1.0.0",
"Description": "A Hellion Forge plugin — maintenance fork of Craftimizer by Asriel Camora, brought back to life on Dalamud SDK 15 for FFXIV 7.5+.\n\nSimulate crafts, create computer-assisted macros, and get mid-craft suggestions from the comfort of your own game. Open your crafting log to get started.\n\nAll features come from upstream Craftimizer, unchanged in this fork:\n- Crafting simulator with full action state tracking\n- Macro solver (Raphael Rust crate, bundled)\n- Recipe note overlay\n- Synthesis helper window\n- Macro editor and macro list\n\nThis fork:\n- Refuses to load if upstream Craftimizer is active (no parallel hooks)\n- No features added, no design changes vs upstream\n- Will archive if Asriel ships an official SDK 15 update upstream\n\nBased on Craftimizer 2.9.1.1 (upstream WorkingRobot/Craftimizer, MIT).\nSource: https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer",
"ApplicableVersion": "any",
"RepoUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer",
"Tags": [
"crafting",
"doh",
"craft",
"macro",
"solver",
"generator",
"generate",
"simulate",
"sim",
"simulator",
"hellion",
"forge"
],
"DalamudApiLevel": 15,
"LoadRequiredState": 0,
"LoadSync": false,
"CanUnloadAsync": false,
"LoadPriority": 0,
"Punchline": "A Hellion Forge plugin. Crafting simulator and macro solver for FFXIV, maintenance fork of Asriel Camora's Craftimizer kept current for Dalamud API 15+.",
"Changelog": "**v0.1.0 — First Hellion fork release (2026-05-26)**\n\nInitial Hellion Forge maintenance release of the Craftimizer fork. Combines the Dalamud SDK 15 migration, the rebrand to Forgeimizer, and the conflict detector into one shippable plugin.\n\n- Dalamud SDK 14 → 15 for FFXIV 7.5+ compatibility. Migrates the call sites the SDK 15 compiler refused (ValueType → AtkValueType, local IEndObject interface, typed ImRaii disposables, ImRaii.TabItem ref-bool lifetime fix).\n- Rebrand to Forgeimizer. Assembly name, plugin manifest, WindowSystem name, About-tab header, MacroMate default, and a new /forgeimizer slash command alias all use the new name. The plugin installs into its own pluginConfigs/Forgeimizer/ slot.\n- Conflict detector. Forgeimizer refuses to load if upstream Craftimizer is active in the same Dalamud instance.\n- Hellion Forge custom repo distribution.\n\nInternal namespaces (Craftimizer.*) intentionally left alone. Crafting logic, solver, simulator, recipe data layer, synthesis hooks, macro engine, and all UI windows are unchanged from upstream Craftimizer 2.9.1.1.\n\nBased on Craftimizer 2.9.1.1 (upstream WorkingRobot/Craftimizer, MIT).",
"AcceptsFeedback": true,
"DownloadLinkInstall": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/releases/download/v0.1.0/latest.zip",
"DownloadLinkUpdate": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/releases/download/v0.1.0/latest.zip",
"DownloadLinkTesting": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/releases/download/v0.1.0/latest.zip",
"TestingAssemblyVersion": "0.1.0.0",
"IconUrl": "https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/raw/branch/main/icon.png",
"ImageUrls": [
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/raw/branch/main/Images/RecipeNote.png",
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/raw/branch/main/Images/SynthHelper.png",
"https://gitea.hellion-forge.cloud/JonKazama-Hellion/Craftimizer/raw/branch/main/Images/MacroEditor.png"
],
"DownloadCount": 0,
"IsHide": false,
"IsTestingExclusive": false,
"CategoryTags": [
"jobs"
]
}
]
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# preflight.sh — pre-push gate. Block A verifies that csproj and repo.json
# agree on the version and tag URLs. Block B does a headless `dotnet build`
# to catch compile-time API drift. Block C runs `dotnet csharpier check`
# against Craftimizer/. Block D runs markdownlint against repo *.md files.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
echo "==> preflight: Block A — version consistency"
./scripts/verify-version-consistency.sh
echo "==> preflight: Block B — plugin compile health"
dotnet build Craftimizer/Craftimizer.csproj --configuration Release --nologo --verbosity quiet
echo "==> preflight: Block C — csharpier reflow check"
dotnet csharpier check Craftimizer/
echo "==> preflight: Block D — markdownlint"
# npx --yes avoids a global install; first run caches into ~/.npm/_npx/.
# Subsequent runs are sub-second.
npx --yes markdownlint-cli2 "**/*.md" "#node_modules" "#bin" "#obj" "#.claude" "#CLAUDE.md"
echo "==> preflight: ALL GREEN"
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# setup-hooks.sh — installs pre-push hook via core.hooksPath. Idempotent.
# Note: NO pre-commit hook — Forgeimizer is a small maintenance fork with
# no separate test harness; version/manifest/build/lint checks happen on
# pre-push only.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
git config core.hooksPath .githooks
chmod +x .githooks/pre-push
chmod +x scripts/preflight.sh
chmod +x scripts/verify-version-consistency.sh
echo "setup-hooks: core.hooksPath set to .githooks. pre-push live."
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# verify-version-consistency.sh — Block A of preflight.
# csproj <Version> is 3-digit SemVer; repo.json AssemblyVersion is 4-digit (.0 suffix).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CSPROJ="$ROOT/Craftimizer/Craftimizer.csproj"
REPO_JSON="$ROOT/repo.json"
fail() { echo "verify-version-consistency: FAIL — $1" >&2; exit 1; }
ok() { echo "verify-version-consistency: OK — $1"; }
CSPROJ_VER="$(grep -oE '<Version>[^<]+</Version>' "$CSPROJ" | head -1 | sed -E 's/<[^>]+>//g')"
[ -n "$CSPROJ_VER" ] || fail "$CSPROJ has no <Version> element"
EXPECTED_4DIGIT="${CSPROJ_VER}.0"
REPO_VER="$(jq -r '.[0].AssemblyVersion' "$REPO_JSON")"
[ "$REPO_VER" = "$EXPECTED_4DIGIT" ] \
|| fail "csproj=$CSPROJ_VER expects repo.json AssemblyVersion=$EXPECTED_4DIGIT but got $REPO_VER. Fix: align in $REPO_JSON."
TEST_VER="$(jq -r '.[0].TestingAssemblyVersion' "$REPO_JSON")"
[ "$TEST_VER" = "$EXPECTED_4DIGIT" ] \
|| fail "TestingAssemblyVersion=$TEST_VER must match $EXPECTED_4DIGIT. Fix: align in $REPO_JSON."
TAG="v$CSPROJ_VER"
for KEY in DownloadLinkInstall DownloadLinkUpdate DownloadLinkTesting; do
URL="$(jq -r ".[0].$KEY" "$REPO_JSON")"
case "$URL" in
*"/$TAG/"*) ;;
*) fail "$KEY=$URL does not contain tag $TAG. Fix: update $REPO_JSON $KEY to releases/download/$TAG/latest.zip." ;;
esac
done
ok "csproj=$CSPROJ_VER, repo.json=$EXPECTED_4DIGIT, tag $TAG present in DownloadLinks"