Merge branch 'alt' into main
This commit is contained in:
@@ -158,6 +158,7 @@ dotnet_diagnostic.CA1841.severity = suggestion
|
|||||||
dotnet_diagnostic.CA1845.severity = suggestion
|
dotnet_diagnostic.CA1845.severity = suggestion
|
||||||
dotnet_diagnostic.MA0011.severity = silent
|
dotnet_diagnostic.MA0011.severity = silent
|
||||||
dotnet_diagnostic.MA0076.severity = silent
|
dotnet_diagnostic.MA0076.severity = silent
|
||||||
|
dotnet_diagnostic.MA0046.severity = silent
|
||||||
dotnet_diagnostic.MA0002.severity = silent
|
dotnet_diagnostic.MA0002.severity = silent
|
||||||
csharp_style_prefer_switch_expression = true:suggestion
|
csharp_style_prefer_switch_expression = true:suggestion
|
||||||
csharp_style_prefer_pattern_matching = true:silent
|
csharp_style_prefer_pattern_matching = true:silent
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text eol=crlf
|
||||||
|
*.png binary
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import shutil, os, subprocess, zipfile, json, sys
|
||||||
|
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
PROJECT_NAME = sys.argv[1]
|
||||||
|
OFFICIAL_ZIP = f"{PROJECT_NAME}/bin/x64/Release/{PROJECT_NAME}/latest.zip"
|
||||||
|
UNOFFICIAL_ZIP = f"{PROJECT_NAME}/bin/x64/Release/{PROJECT_NAME}/latestUnofficial.zip"
|
||||||
|
|
||||||
|
shutil.copy(OFFICIAL_ZIP, UNOFFICIAL_ZIP)
|
||||||
|
|
||||||
|
subprocess.check_call(['7z', 'd', UNOFFICIAL_ZIP, f"{PROJECT_NAME}.json"])
|
||||||
|
|
||||||
|
with zipfile.ZipFile(UNOFFICIAL_ZIP) as file:
|
||||||
|
members = [member for member in file.namelist() if member in (f"{PROJECT_NAME}.dll", f"{PROJECT_NAME}.deps.json", f"{PROJECT_NAME}.json", f"{PROJECT_NAME}.pdb")]
|
||||||
|
|
||||||
|
subprocess.check_call(['7z', 'rn', UNOFFICIAL_ZIP] + list(chain.from_iterable((m, m.replace(PROJECT_NAME, f"{PROJECT_NAME}Unofficial")) for m in members)))
|
||||||
|
|
||||||
|
with open(f"{PROJECT_NAME}/bin/x64/Release/{PROJECT_NAME}/{PROJECT_NAME}.json") as file:
|
||||||
|
manifest = json.load(file)
|
||||||
|
|
||||||
|
manifest['Punchline'] = f"Unofficial/uncertified build of {manifest['Name']}. {manifest['Punchline']}"
|
||||||
|
manifest['InternalName'] += 'Unofficial'
|
||||||
|
manifest['Name'] += ' (Unofficial)'
|
||||||
|
manifest['IconUrl'] = f"https://raw.githubusercontent.com/WorkingRobot/MyDalamudPlugins/main/icons/{manifest['InternalName']}.png"
|
||||||
|
|
||||||
|
with zipfile.ZipFile(UNOFFICIAL_ZIP, "a", zipfile.ZIP_DEFLATED, compresslevel = 7) as file:
|
||||||
|
file.writestr(f"{PROJECT_NAME}Unofficial.json", json.dumps(manifest, indent = 2))
|
||||||
+39
-16
@@ -1,53 +1,76 @@
|
|||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push
|
||||||
tags:
|
|
||||||
- '*'
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
PLUGIN_REPO: WorkingRobot/MyDalamudPlugins
|
PLUGIN_REPO: WorkingRobot/MyDalamudPlugins
|
||||||
PROJECT_NAME: Craftimizer
|
PROJECT_NAME: Craftimizer
|
||||||
|
IS_OFFICIAL: ${{true}}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: windows-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Setup MSBuild
|
- name: Setup .NET
|
||||||
uses: microsoft/setup-msbuild@v1.0.3
|
uses: actions/setup-dotnet@v3
|
||||||
|
with:
|
||||||
|
dotnet-version: '7.0'
|
||||||
|
|
||||||
- name: Download Dalamud
|
- name: Download Dalamud
|
||||||
run: |
|
run: |
|
||||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
|
wget https://goatcorp.github.io/dalamud-distrib/stg/latest.zip
|
||||||
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev\"
|
unzip latest.zip -d dalamud/
|
||||||
|
echo "DALAMUD_HOME=$PWD/dalamud" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Restore
|
||||||
|
run: |
|
||||||
|
dotnet restore -r win
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
dotnet restore -r win ${{env.PROJECT_NAME}}.sln
|
dotnet build --configuration Release --no-restore
|
||||||
dotnet build --configuration Release
|
|
||||||
env:
|
|
||||||
DOTNET_CLI_TELEMETRY_OUTPUT: true
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Test
|
||||||
uses: actions/upload-artifact@v2.2.1
|
run: |
|
||||||
|
dotnet test --configuration Release --logger "trx;logfilename=results.trx" --logger "html;logfilename=results.html" --logger "console;verbosity=detailed" --no-build --results-directory="TestResults"
|
||||||
|
|
||||||
|
- name: Create Unofficial Builds
|
||||||
|
if: ${{env.IS_OFFICIAL}}
|
||||||
|
run: python ./.github/create_unofficial.py ${{env.PROJECT_NAME}}
|
||||||
|
|
||||||
|
- name: Upload Artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{env.PROJECT_NAME}}
|
name: ${{env.PROJECT_NAME}}
|
||||||
path: ${{env.PROJECT_NAME}}/bin/x64/Release/${{env.PROJECT_NAME}}
|
path: ${{env.PROJECT_NAME}}/bin/x64/Release/${{env.PROJECT_NAME}}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload Test Results
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
with:
|
||||||
|
name: TestResults
|
||||||
|
path: TestResults
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
id: release
|
||||||
with:
|
with:
|
||||||
files: ${{env.PROJECT_NAME}}/bin/x64/Release/${{env.PROJECT_NAME}}/*
|
files: ${{env.PROJECT_NAME}}/bin/x64/Release/${{env.PROJECT_NAME}}/*
|
||||||
|
|
||||||
- name: Trigger Plugin Repo Update
|
- name: Trigger Plugin Repo Update
|
||||||
uses: peter-evans/repository-dispatch@v1
|
uses: peter-evans/repository-dispatch@v2
|
||||||
|
if: ${{ steps.release.conclusion == 'success' }}
|
||||||
with:
|
with:
|
||||||
token: ${{secrets.PAT}}
|
token: ${{secrets.PAT}}
|
||||||
repository: ${{env.PLUGIN_REPO}}
|
repository: ${{env.PLUGIN_REPO}}
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
obj/
|
obj/
|
||||||
bin/
|
bin/
|
||||||
*.user
|
*.user
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using BenchmarkDotNet.Engines;
|
||||||
|
using Craftimizer.Simulator;
|
||||||
|
using Craftimizer.Solver;
|
||||||
|
|
||||||
|
namespace Craftimizer.Benchmark;
|
||||||
|
|
||||||
|
[SimpleJob(RunStrategy.Monitoring)]
|
||||||
|
[MinColumn, Q1Column, Q3Column, MaxColumn]
|
||||||
|
public class Bench
|
||||||
|
{
|
||||||
|
private static SimulationInput[] Inputs { get; } = new SimulationInput[] {
|
||||||
|
// https://craftingway.app/rotation/loud-namazu-jVe9Y
|
||||||
|
// Chondrite Saw
|
||||||
|
new(new()
|
||||||
|
{
|
||||||
|
Craftsmanship = 3304,
|
||||||
|
Control = 3374,
|
||||||
|
CP = 575,
|
||||||
|
Level = 90,
|
||||||
|
CanUseManipulation = true,
|
||||||
|
HasSplendorousBuff = false,
|
||||||
|
IsSpecialist = false,
|
||||||
|
CLvl = 560,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
IsExpert = false,
|
||||||
|
ClassJobLevel = 90,
|
||||||
|
RLvl = 560,
|
||||||
|
ConditionsFlag = 0b1111,
|
||||||
|
MaxDurability = 80,
|
||||||
|
MaxQuality = 7200,
|
||||||
|
MaxProgress = 3500,
|
||||||
|
QualityModifier = 80,
|
||||||
|
QualityDivider = 115,
|
||||||
|
ProgressModifier = 90,
|
||||||
|
ProgressDivider = 130
|
||||||
|
}),
|
||||||
|
|
||||||
|
// https://craftingway.app/rotation/sandy-fafnir-doVCs
|
||||||
|
// Classical Longsword
|
||||||
|
new(new()
|
||||||
|
{
|
||||||
|
Craftsmanship = 3290,
|
||||||
|
Control = 3541,
|
||||||
|
CP = 649,
|
||||||
|
Level = 90,
|
||||||
|
CanUseManipulation = true,
|
||||||
|
HasSplendorousBuff = false,
|
||||||
|
IsSpecialist = false,
|
||||||
|
CLvl = 560,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
IsExpert = false,
|
||||||
|
ClassJobLevel = 90,
|
||||||
|
RLvl = 580,
|
||||||
|
ConditionsFlag = 0b1111,
|
||||||
|
MaxDurability = 70,
|
||||||
|
MaxQuality = 10920,
|
||||||
|
MaxProgress = 3900,
|
||||||
|
QualityModifier = 70,
|
||||||
|
QualityDivider = 115,
|
||||||
|
ProgressModifier = 80,
|
||||||
|
ProgressDivider = 130
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
public static IEnumerable<SimulationState> States => Inputs.Select(i => new SimulationState(i));
|
||||||
|
|
||||||
|
public static IEnumerable<SolverConfig> Configs => new SolverConfig[]
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Iterations = 100_000,
|
||||||
|
ForkCount = 32,
|
||||||
|
FurcatedActionCount = 16,
|
||||||
|
MaxStepCount = 30,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
[ParamsSource(nameof(States))]
|
||||||
|
public SimulationState State { get; set; }
|
||||||
|
|
||||||
|
[ParamsSource(nameof(Configs))]
|
||||||
|
public SolverConfig Config { get; set; }
|
||||||
|
|
||||||
|
[Benchmark]
|
||||||
|
public async Task<float> Solve()
|
||||||
|
{
|
||||||
|
var solver = new Solver.Solver(Config, State);
|
||||||
|
solver.Start();
|
||||||
|
var (_, s) = await solver.GetTask().ConfigureAwait(false);
|
||||||
|
return (float)s.Quality / s.Input.Recipe.MaxQuality;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,19 @@
|
|||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<Platforms>x64</Platforms>
|
||||||
|
<Configurations>Debug;Release</Configurations>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.5" />
|
<Compile Remove="BenchmarkDotNet.Artifacts\**" />
|
||||||
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
|
<EmbeddedResource Remove="BenchmarkDotNet.Artifacts\**" />
|
||||||
|
<None Remove="BenchmarkDotNet.Artifacts\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BenchmarkDotNet" Version="0.13.8" />
|
||||||
|
<PackageReference Include="Meziantou.Analyzer" Version="2.0.92">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
@@ -20,4 +28,9 @@
|
|||||||
<ProjectReference Include="..\Simulator\Craftimizer.Simulator.csproj" />
|
<ProjectReference Include="..\Simulator\Craftimizer.Simulator.csproj" />
|
||||||
<ProjectReference Include="..\Solver\Craftimizer.Solver.csproj" />
|
<ProjectReference Include="..\Solver\Craftimizer.Solver.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(IS_BENCH)'=='1'">
|
||||||
|
<DefineConstants>$(DefineConstants);IS_DETERMINISTIC</DefineConstants>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+61
-67
@@ -1,17 +1,68 @@
|
|||||||
using Craftimizer.Simulator;
|
using Craftimizer.Simulator;
|
||||||
using Craftimizer.Simulator.Actions;
|
using Craftimizer.Simulator.Actions;
|
||||||
using Craftimizer.Solver.Crafty;
|
using Craftimizer.Solver;
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace Craftimizer.Benchmark;
|
namespace Craftimizer.Benchmark;
|
||||||
|
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
private static void Main()
|
private static Task Main(string[] args)
|
||||||
{
|
{
|
||||||
//var summary = BenchmarkRunner.Run<Bench>();
|
RunBench(args);
|
||||||
//return;
|
return Task.CompletedTask;
|
||||||
|
// return RunOther();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunBench(string[] args)
|
||||||
|
{
|
||||||
|
Environment.SetEnvironmentVariable("IS_BENCH", "1");
|
||||||
|
BenchmarkDotNet.Running.BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task RunTrace()
|
||||||
|
{
|
||||||
|
var input = new SimulationInput(
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Craftsmanship = 4041,
|
||||||
|
Control = 3905,
|
||||||
|
CP = 609,
|
||||||
|
Level = 90,
|
||||||
|
CanUseManipulation = true,
|
||||||
|
HasSplendorousBuff = false,
|
||||||
|
IsSpecialist = false,
|
||||||
|
CLvl = 560,
|
||||||
|
},
|
||||||
|
new RecipeInfo()
|
||||||
|
{
|
||||||
|
IsExpert = false,
|
||||||
|
ClassJobLevel = 90,
|
||||||
|
RLvl = 640,
|
||||||
|
ConditionsFlag = 15,
|
||||||
|
MaxDurability = 70,
|
||||||
|
MaxQuality = 14040,
|
||||||
|
MaxProgress = 6600,
|
||||||
|
QualityModifier = 70,
|
||||||
|
QualityDivider = 115,
|
||||||
|
ProgressModifier = 80,
|
||||||
|
ProgressDivider = 130,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
var config = new SolverConfig()
|
||||||
|
{
|
||||||
|
Algorithm = SolverAlgorithm.Stepwise,
|
||||||
|
Iterations = 30000,
|
||||||
|
MaxStepCount = 25
|
||||||
|
};
|
||||||
|
var solver = new Solver.Solver(config, new(input));
|
||||||
|
solver.OnNewAction += s => Console.WriteLine($">{s}");
|
||||||
|
solver.Start();
|
||||||
|
var (_, s) = await solver.GetTask().ConfigureAwait(false);
|
||||||
|
Console.WriteLine($"Qual: {s.Quality}/{s.Input.Recipe.MaxQuality}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task RunOther()
|
||||||
|
{
|
||||||
//TypeLayout.PrintLayout<ArenaNode<SimulationNode>>(true);
|
//TypeLayout.PrintLayout<ArenaNode<SimulationNode>>(true);
|
||||||
//return;
|
//return;
|
||||||
|
|
||||||
@@ -77,68 +128,11 @@ internal static class Program
|
|||||||
|
|
||||||
Console.WriteLine($"{state.Quality} {state.CP} {state.Progress} {state.Durability}");
|
Console.WriteLine($"{state.Quality} {state.CP} {state.Progress} {state.Durability}");
|
||||||
//return;
|
//return;
|
||||||
var (_, s) = Solver.Crafty.Solver.SearchStepwiseFurcated(config, state, a => Console.WriteLine(a));
|
var solver = new Solver.Solver(config, state);
|
||||||
|
solver.OnLog += Console.WriteLine;
|
||||||
|
solver.OnNewAction += s => Console.WriteLine(s);
|
||||||
|
solver.Start();
|
||||||
|
var (_, s) = await solver.GetTask().ConfigureAwait(false);
|
||||||
Console.WriteLine($"Qual: {s.Quality}/{s.Input.Recipe.MaxQuality}");
|
Console.WriteLine($"Qual: {s.Quality}/{s.Input.Recipe.MaxQuality}");
|
||||||
return;
|
|
||||||
|
|
||||||
Solver.Crafty.Solver.SearchStepwiseFurcated(config, input);
|
|
||||||
//Benchmark(() => );
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Benchmark(Func<SolverSolution> search)
|
|
||||||
{
|
|
||||||
var s = Stopwatch.StartNew();
|
|
||||||
List<int> q = new();
|
|
||||||
for (var i = 0; i < 15; ++i)
|
|
||||||
{
|
|
||||||
var state = search().State;
|
|
||||||
//Console.WriteLine($"Qual: {state.Quality}/{state.Input.Recipe.MaxQuality}");
|
|
||||||
|
|
||||||
q.Add(state.Quality);
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Stop();
|
|
||||||
Console.WriteLine($"{s.Elapsed.TotalMilliseconds / 60:0.00}ms/cycle");
|
|
||||||
Console.WriteLine(string.Join(',', q));
|
|
||||||
q.Sort();
|
|
||||||
Console.WriteLine($"Min: {Quartile(q, 0)}, Max: {Quartile(q, 4)}, Avg: {Quartile(q, 2)}, Q1: {Quartile(q, 1)}, Q3: {Quartile(q, 3)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://stackoverflow.com/a/31536435
|
|
||||||
private static float Quartile(List<int> input, int quartile)
|
|
||||||
{
|
|
||||||
float dblPercentage = quartile switch
|
|
||||||
{
|
|
||||||
0 => 0, // Smallest value in the data set
|
|
||||||
1 => 25, // First quartile (25th percentile)
|
|
||||||
2 => 50, // Second quartile (50th percentile)
|
|
||||||
3 => 75, // Third quartile (75th percentile)
|
|
||||||
4 => 100, // Largest value in the data set
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
if (dblPercentage >= 100) return input[^1];
|
|
||||||
|
|
||||||
var position = (input.Count + 1) * dblPercentage / 100f;
|
|
||||||
var n = (dblPercentage / 100f * (input.Count - 1)) + 1;
|
|
||||||
|
|
||||||
float leftNumber, rightNumber;
|
|
||||||
if (position >= 1)
|
|
||||||
{
|
|
||||||
leftNumber = input[(int)MathF.Floor(n) - 1];
|
|
||||||
rightNumber = input[(int)MathF.Floor(n)];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
leftNumber = input[0]; // first data
|
|
||||||
rightNumber = input[1]; // first data
|
|
||||||
}
|
|
||||||
|
|
||||||
if (leftNumber == rightNumber)
|
|
||||||
return leftNumber;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var part = n - MathF.Floor(n);
|
|
||||||
return leftNumber + (part * (rightNumber - leftNumber));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-30
@@ -17,46 +17,34 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Solver", "Solve
|
|||||||
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3} = {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}
|
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3} = {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Test", "Test\Craftimizer.Test.csproj", "{C3AEA981-9DA8-405C-995B-86528493891B}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Debug|x64 = Debug|x64
|
Debug|x64 = Debug|x64
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
Release|x64 = Release|x64
|
Release|x64 = Release|x64
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|x64
|
|
||||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|x64
|
|
||||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64
|
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64
|
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64
|
||||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|x64
|
|
||||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|x64
|
|
||||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64
|
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64
|
||||||
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64
|
{13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64
|
||||||
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.Build.0 = Debug|x64
|
||||||
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.ActiveCfg = Release|x64
|
||||||
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.Build.0 = Debug|Any CPU
|
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.Build.0 = Release|x64
|
||||||
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|Any CPU.Build.0 = Release|Any CPU
|
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.Build.0 = Debug|x64
|
||||||
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.ActiveCfg = Release|Any CPU
|
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.ActiveCfg = Release|x64
|
||||||
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.Build.0 = Release|Any CPU
|
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.Build.0 = Release|x64
|
||||||
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.Build.0 = Debug|x64
|
||||||
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.ActiveCfg = Release|x64
|
||||||
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.Build.0 = Debug|Any CPU
|
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.Build.0 = Release|x64
|
||||||
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.Build.0 = Debug|x64
|
||||||
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.ActiveCfg = Release|Any CPU
|
{C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.ActiveCfg = Release|x64
|
||||||
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.Build.0 = Release|Any CPU
|
{C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.Build.0 = Release|x64
|
||||||
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -1,37 +1,74 @@
|
|||||||
using Craftimizer.Simulator;
|
|
||||||
using Craftimizer.Simulator.Actions;
|
using Craftimizer.Simulator.Actions;
|
||||||
using Craftimizer.Solver.Crafty;
|
using Craftimizer.Solver;
|
||||||
using Dalamud.Configuration;
|
using Dalamud.Configuration;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Craftimizer.Plugin;
|
namespace Craftimizer.Plugin;
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class Macro
|
public class Macro
|
||||||
{
|
{
|
||||||
|
public static event Action<Macro>? OnMacroChanged;
|
||||||
|
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public List<ActionType> Actions { get; set; } = new();
|
[JsonProperty(PropertyName = "Actions")]
|
||||||
|
private List<ActionType> actions { get; set; } = new();
|
||||||
|
[JsonIgnore]
|
||||||
|
public IReadOnlyList<ActionType> Actions
|
||||||
|
{
|
||||||
|
get => actions;
|
||||||
|
set => ActionEnumerable = value;
|
||||||
|
}
|
||||||
|
[JsonIgnore]
|
||||||
|
public IEnumerable<ActionType> ActionEnumerable
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
actions = new(value);
|
||||||
|
OnMacroChanged?.Invoke(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class AlgorithmUtils
|
[Serializable]
|
||||||
|
public class MacroCopyConfiguration
|
||||||
{
|
{
|
||||||
public static void Invoke(this SolverConfig me, SimulationState state, Action<ActionType>? actionCallback = null, CancellationToken token = default)
|
public enum CopyType
|
||||||
{
|
{
|
||||||
try
|
OpenWindow, // useful for big macros
|
||||||
{
|
CopyToMacro, // (add option for down or right) (max macro count; open copy-paste window if too much)
|
||||||
Solver.Crafty.Solver.Search(me, state, actionCallback, token);
|
CopyToClipboard,
|
||||||
}
|
}
|
||||||
catch (AggregateException e)
|
|
||||||
{
|
|
||||||
e.Handle(ex => ex is OperationCanceledException);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
public CopyType Type { get; set; } = CopyType.OpenWindow;
|
||||||
}
|
|
||||||
|
// CopyToMacro
|
||||||
|
public bool CopyDown { get; set; }
|
||||||
|
public bool SharedMacro { get; set; }
|
||||||
|
public int StartMacroIdx { get; set; } = 1;
|
||||||
|
public int MaxMacroCount { get; set; } = 5;
|
||||||
|
|
||||||
|
// Add /nextmacro [down]
|
||||||
|
public bool UseNextMacro { get; set; }
|
||||||
|
|
||||||
|
// Add /mlock
|
||||||
|
public bool UseMacroLock { get; set; }
|
||||||
|
|
||||||
|
public bool AddNotification { get; set; } = true;
|
||||||
|
|
||||||
|
// Requires AddNotification
|
||||||
|
public bool ForceNotification { get; set; }
|
||||||
|
public bool AddNotificationSound { get; set; } = true;
|
||||||
|
public int IntermediateNotificationSound { get; set; } = 10;
|
||||||
|
public int EndNotificationSound { get; set; } = 6;
|
||||||
|
|
||||||
|
// For SND
|
||||||
|
public bool RemoveWaitTimes { get; set; }
|
||||||
|
|
||||||
|
// For SND; Cannot use CopyToMacro
|
||||||
|
public bool CombineMacro { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@@ -39,19 +76,36 @@ public class Configuration : IPluginConfiguration
|
|||||||
{
|
{
|
||||||
public int Version { get; set; } = 1;
|
public int Version { get; set; } = 1;
|
||||||
|
|
||||||
public bool OverrideUncraftability { get; set; } = true;
|
public static event Action? OnMacroListChanged;
|
||||||
public bool HideUnlearnedActions { get; set; } = true;
|
|
||||||
public List<Macro> Macros { get; set; } = new();
|
[JsonProperty(PropertyName = "Macros")]
|
||||||
|
private List<Macro> macros { get; set; } = new();
|
||||||
|
[JsonIgnore]
|
||||||
|
public IReadOnlyList<Macro> Macros => macros;
|
||||||
public bool ConditionRandomness { get; set; } = true;
|
public bool ConditionRandomness { get; set; } = true;
|
||||||
public SolverConfig SimulatorSolverConfig { get; set; } = SolverConfig.SimulatorDefault;
|
public SolverConfig SimulatorSolverConfig { get; set; } = SolverConfig.SimulatorDefault;
|
||||||
public SolverConfig SynthHelperSolverConfig { get; set; } = SolverConfig.SynthHelperDefault;
|
public SolverConfig SynthHelperSolverConfig { get; set; } = SolverConfig.SynthHelperDefault;
|
||||||
public bool EnableSynthHelper { get; set; } = true;
|
public bool EnableSynthHelper { get; set; } = true;
|
||||||
|
public bool ShowOptimalMacroStat { get; set; } = true;
|
||||||
public int SynthHelperStepCount { get; set; } = 5;
|
public int SynthHelperStepCount { get; set; } = 5;
|
||||||
|
|
||||||
public Simulator.Simulator CreateSimulator(SimulationState state) =>
|
public MacroCopyConfiguration MacroCopy { get; set; } = new();
|
||||||
ConditionRandomness ?
|
|
||||||
new Simulator.Simulator(state) :
|
public void AddMacro(Macro macro)
|
||||||
new SimulatorNoRandom(state);
|
{
|
||||||
|
macros.Add(macro);
|
||||||
|
Save();
|
||||||
|
OnMacroListChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveMacro(Macro macro)
|
||||||
|
{
|
||||||
|
if (macros.Remove(macro))
|
||||||
|
{
|
||||||
|
Save();
|
||||||
|
OnMacroListChanged?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Save() =>
|
public void Save() =>
|
||||||
Service.PluginInterface.SavePluginConfig(this);
|
Service.PluginInterface.SavePluginConfig(this);
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Authors>Asriel Camora</Authors>
|
<Authors>Asriel Camora</Authors>
|
||||||
<Version>1.2.0.0</Version>
|
<Version>1.9.2.0</Version>
|
||||||
<PackageProjectUrl>https://github.com/WorkingRobot/craftimizer.git</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/WorkingRobot/craftimizer.git</PackageProjectUrl>
|
||||||
|
<Configurations>Debug;Release</Configurations>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@@ -28,11 +29,17 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="../icon.png" />
|
<EmbeddedResource Include="../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>
|
<ItemGroup>
|
||||||
<PackageReference Include="DalamudPackager" Version="2.1.11" />
|
<PackageReference Include="DalamudPackager" Version="2.1.12" />
|
||||||
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
|
<PackageReference Include="Meziantou.Analyzer" Version="2.0.92">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"Author": "Asriel Camora",
|
"Author": "Asriel",
|
||||||
"Name": "Craftimizer",
|
"Name": "Craftimizer",
|
||||||
"Punchline": "Simulate crafts and create computer-assisted macros from the comfort of your own game",
|
"Punchline": "Simulate crafts and create computer-assisted macros 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.",
|
"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",
|
"RepoUrl": "https://github.com/WorkingRobot/craftimizer",
|
||||||
"InternalName": "craftimizer",
|
"InternalName": "Craftimizer",
|
||||||
"ApplicableVersion": "any",
|
"ApplicableVersion": "any",
|
||||||
"DalamudApiLevel": 8,
|
|
||||||
"Tags": [
|
"Tags": [
|
||||||
"crafting",
|
"crafting",
|
||||||
"doh",
|
"doh",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 816 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 896 B |
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,19 +0,0 @@
|
|||||||
using ImGuiScene;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Craftimizer.Plugin;
|
|
||||||
|
|
||||||
internal static class Icons
|
|
||||||
{
|
|
||||||
private static readonly Dictionary<string, TextureWrap> Cache = new();
|
|
||||||
|
|
||||||
public static TextureWrap GetIconFromPath(string path)
|
|
||||||
{
|
|
||||||
if (!Cache.TryGetValue(path, out var ret))
|
|
||||||
Cache.Add(path, ret = Service.DataManager.GetImGuiTexture(path)!);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TextureWrap GetIconFromId(ushort id) =>
|
|
||||||
GetIconFromPath($"ui/icon/{id / 1000 * 1000:000000}/{id:000000}_hr1.tex");
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
using ImGuiNET;
|
||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Craftimizer;
|
||||||
|
|
||||||
|
internal static unsafe class ImGuiExtras
|
||||||
|
{
|
||||||
|
// https://github.com/ImGuiNET/ImGui.NET/blob/069363672fed940ebdaa02f9b032c282b66467c7/src/CodeGenerator/definitions/cimgui/definitions.json#L25394
|
||||||
|
[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);
|
||||||
|
|
||||||
|
// https://github.com/ImGuiNET/ImGui.NET/blob/069363672fed940ebdaa02f9b032c282b66467c7/src/ImGui.NET/Util.cs
|
||||||
|
#region Util
|
||||||
|
|
||||||
|
private const int StackAllocationSizeLimit = 2048;
|
||||||
|
|
||||||
|
private static unsafe byte* Allocate(int byteCount) => (byte*)Marshal.AllocHGlobal(byteCount);
|
||||||
|
|
||||||
|
private static unsafe void Free(byte* ptr) => Marshal.FreeHGlobal((IntPtr)ptr);
|
||||||
|
|
||||||
|
private static unsafe int GetUtf8(ReadOnlySpan<char> s, byte* utf8Bytes, int utf8ByteCount)
|
||||||
|
{
|
||||||
|
if (s.IsEmpty)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
fixed (char* utf16Ptr = s)
|
||||||
|
return Encoding.UTF8.GetBytes(utf16Ptr, s.Length, utf8Bytes, utf8ByteCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe bool AreStringsEqual(byte* a, int aLength, byte* b)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < aLength; i++)
|
||||||
|
{
|
||||||
|
if (a[i] != b[i])
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return b[aLength] == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe string StringFromPtr(byte* ptr)
|
||||||
|
{
|
||||||
|
var characters = 0;
|
||||||
|
while (ptr[characters] != 0)
|
||||||
|
{
|
||||||
|
characters++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.UTF8.GetString(ptr, characters);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
var utf8LabelByteCount = Encoding.UTF8.GetByteCount(label);
|
||||||
|
byte* utf8LabelBytes;
|
||||||
|
if (utf8LabelByteCount > StackAllocationSizeLimit)
|
||||||
|
{
|
||||||
|
utf8LabelBytes = Allocate(utf8LabelByteCount + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var stackPtr = stackalloc byte[utf8LabelByteCount + 1];
|
||||||
|
utf8LabelBytes = stackPtr;
|
||||||
|
}
|
||||||
|
GetUtf8(label, utf8LabelBytes, utf8LabelByteCount);
|
||||||
|
|
||||||
|
var utf8HintByteCount = Encoding.UTF8.GetByteCount(hint);
|
||||||
|
byte* utf8HintBytes;
|
||||||
|
if (utf8HintByteCount > StackAllocationSizeLimit)
|
||||||
|
{
|
||||||
|
utf8HintBytes = Allocate(utf8HintByteCount + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var stackPtr = stackalloc byte[utf8HintByteCount + 1];
|
||||||
|
utf8HintBytes = stackPtr;
|
||||||
|
}
|
||||||
|
GetUtf8(hint, utf8HintBytes, utf8HintByteCount);
|
||||||
|
|
||||||
|
var utf8InputByteCount = Encoding.UTF8.GetByteCount(input);
|
||||||
|
var inputBufSize = Math.Max(maxLength + 1, utf8InputByteCount + 1);
|
||||||
|
|
||||||
|
byte* utf8InputBytes;
|
||||||
|
byte* originalUtf8InputBytes;
|
||||||
|
if (inputBufSize > StackAllocationSizeLimit)
|
||||||
|
{
|
||||||
|
utf8InputBytes = Allocate(inputBufSize);
|
||||||
|
originalUtf8InputBytes = Allocate(inputBufSize);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var inputStackBytes = stackalloc byte[inputBufSize];
|
||||||
|
utf8InputBytes = inputStackBytes;
|
||||||
|
var originalInputStackBytes = stackalloc byte[inputBufSize];
|
||||||
|
originalUtf8InputBytes = originalInputStackBytes;
|
||||||
|
}
|
||||||
|
GetUtf8(input, utf8InputBytes, inputBufSize);
|
||||||
|
var clearBytesCount = (uint)(inputBufSize - utf8InputByteCount);
|
||||||
|
Unsafe.InitBlockUnaligned(utf8InputBytes + utf8InputByteCount, 0, clearBytesCount);
|
||||||
|
Unsafe.CopyBlock(originalUtf8InputBytes, utf8InputBytes, (uint)inputBufSize);
|
||||||
|
|
||||||
|
var result = igInputTextEx(
|
||||||
|
utf8LabelBytes,
|
||||||
|
utf8HintBytes,
|
||||||
|
utf8InputBytes,
|
||||||
|
inputBufSize,
|
||||||
|
size,
|
||||||
|
flags,
|
||||||
|
callback,
|
||||||
|
user_data.ToPointer());
|
||||||
|
if (!AreStringsEqual(originalUtf8InputBytes, inputBufSize, utf8InputBytes))
|
||||||
|
{
|
||||||
|
input = StringFromPtr(utf8InputBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (utf8LabelByteCount > StackAllocationSizeLimit)
|
||||||
|
{
|
||||||
|
Free(utf8LabelBytes);
|
||||||
|
}
|
||||||
|
if (utf8HintByteCount > StackAllocationSizeLimit)
|
||||||
|
{
|
||||||
|
Free(utf8HintBytes);
|
||||||
|
}
|
||||||
|
if (inputBufSize > StackAllocationSizeLimit)
|
||||||
|
{
|
||||||
|
Free(utf8InputBytes);
|
||||||
|
Free(originalUtf8InputBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
+467
-71
@@ -1,138 +1,476 @@
|
|||||||
|
using Craftimizer.Utils;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Craftimizer.Plugin;
|
namespace Craftimizer.Plugin;
|
||||||
|
|
||||||
internal static class ImGuiUtils
|
internal static class ImGuiUtils
|
||||||
{
|
{
|
||||||
public static float ButtonHeight =>
|
private static readonly Stack<(Vector2 Min, Vector2 Max, float TopPadding)> GroupPanelLabelStack = new();
|
||||||
ImGui.CalcTextSize("A").Y + (ImGui.GetStyle().FramePadding.Y * 2);
|
|
||||||
|
|
||||||
private static readonly Stack<(Vector2 Min, Vector2 Max)> GroupPanelLabelStack = new();
|
|
||||||
|
|
||||||
// Adapted from https://github.com/ocornut/imgui/issues/1496#issuecomment-655048353
|
// Adapted from https://github.com/ocornut/imgui/issues/1496#issuecomment-655048353
|
||||||
public static void BeginGroupPanel(float width = -1, bool addPadding = true)
|
// width = -1 -> size to parent
|
||||||
|
// width = 0 -> size to content
|
||||||
|
// returns available width (better since it accounts for the right side padding)
|
||||||
|
// ^ only useful if width = -1
|
||||||
|
public static float BeginGroupPanel(string name, float width)
|
||||||
{
|
{
|
||||||
|
ImGui.PushID(name);
|
||||||
|
|
||||||
|
// container group
|
||||||
ImGui.BeginGroup();
|
ImGui.BeginGroup();
|
||||||
|
|
||||||
var itemSpacing = ImGui.GetStyle().ItemSpacing;
|
var itemSpacing = ImGui.GetStyle().ItemSpacing;
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
|
||||||
|
|
||||||
var frameHeight = ImGui.GetFrameHeight();
|
var frameHeight = ImGui.GetFrameHeight();
|
||||||
|
width = width < 0 ? ImGui.GetContentRegionAvail().X - (2 * itemSpacing.X) : width;
|
||||||
|
var fullWidth = width > 0 ? width + (2 * itemSpacing.X) : 0;
|
||||||
|
{
|
||||||
|
using var noPadding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero);
|
||||||
|
using var noSpacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
||||||
|
|
||||||
|
// inner group
|
||||||
ImGui.BeginGroup();
|
ImGui.BeginGroup();
|
||||||
ImGui.Dummy(new Vector2(width < 0 ? ImGui.GetContentRegionAvail().X : width, 0));
|
ImGui.Dummy(new Vector2(fullWidth, 0));
|
||||||
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
|
ImGui.Dummy(new Vector2(itemSpacing.X, 0)); // shifts next group by is.x
|
||||||
ImGui.SameLine(0, 0);
|
ImGui.SameLine(0, 0);
|
||||||
|
|
||||||
|
// label group
|
||||||
ImGui.BeginGroup();
|
ImGui.BeginGroup();
|
||||||
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
|
if (ImGui.CalcTextSize(name, true).X == 0)
|
||||||
GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax()));
|
{
|
||||||
|
GroupPanelLabelStack.Push(default);
|
||||||
|
ImGui.Dummy(new Vector2(0f, itemSpacing.Y)); // shifts content by is.y
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.Dummy(new Vector2(frameHeight / 2, 0)); // shifts text by fh/2
|
||||||
ImGui.SameLine(0, 0);
|
ImGui.SameLine(0, 0);
|
||||||
ImGui.Dummy(new Vector2(0f, frameHeight * (addPadding ? 1 : .5f) + itemSpacing.Y));
|
var textFrameHeight = ImGui.GetFrameHeight();
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
ImGui.BeginGroup();
|
ImGui.Text(name);
|
||||||
|
GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), textFrameHeight / 2f)); // push rect to stack
|
||||||
ImGui.PopStyleVar(2);
|
ImGui.SameLine(0, 0);
|
||||||
|
ImGui.Dummy(new Vector2(0f, textFrameHeight + itemSpacing.Y)); // shifts content by fh + is.y
|
||||||
ImGui.PushItemWidth(MathF.Max(0, ImGui.CalcItemWidth() - frameHeight));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void BeginGroupPanel(string name, float width = -1, bool addPadding = true)
|
// content group
|
||||||
{
|
|
||||||
ImGui.BeginGroup();
|
ImGui.BeginGroup();
|
||||||
|
}
|
||||||
|
|
||||||
var itemSpacing = ImGui.GetStyle().ItemSpacing;
|
return width;
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
|
||||||
|
|
||||||
var frameHeight = ImGui.GetFrameHeight();
|
|
||||||
|
|
||||||
ImGui.BeginGroup();
|
|
||||||
ImGui.Dummy(new Vector2(width < 0 ? ImGui.GetContentRegionAvail().X : width, 0));
|
|
||||||
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
|
|
||||||
ImGui.SameLine(0, 0);
|
|
||||||
|
|
||||||
ImGui.BeginGroup();
|
|
||||||
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
|
|
||||||
ImGui.SameLine(0, 0);
|
|
||||||
ImGui.TextUnformatted(name);
|
|
||||||
GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax()));
|
|
||||||
ImGui.SameLine(0, 0);
|
|
||||||
ImGui.Dummy(new Vector2(0f, frameHeight * (addPadding ? 1 : .5f) + itemSpacing.Y));
|
|
||||||
|
|
||||||
ImGui.BeginGroup();
|
|
||||||
|
|
||||||
ImGui.PopStyleVar(2);
|
|
||||||
|
|
||||||
ImGui.PushItemWidth(MathF.Max(0, ImGui.CalcItemWidth() - frameHeight));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void EndGroupPanel()
|
public static void EndGroupPanel()
|
||||||
{
|
{
|
||||||
ImGui.PopItemWidth();
|
|
||||||
|
|
||||||
var itemSpacing = ImGui.GetStyle().ItemSpacing;
|
var itemSpacing = ImGui.GetStyle().ItemSpacing;
|
||||||
|
{
|
||||||
|
using var noPadding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero);
|
||||||
|
using var noSpacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
||||||
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
|
// content group
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
|
|
||||||
|
|
||||||
var frameHeight = ImGui.GetFrameHeight();
|
|
||||||
|
|
||||||
ImGui.EndGroup();
|
ImGui.EndGroup();
|
||||||
|
|
||||||
|
// label group
|
||||||
ImGui.EndGroup();
|
ImGui.EndGroup();
|
||||||
|
|
||||||
ImGui.SameLine(0, 0);
|
ImGui.SameLine(0, 0);
|
||||||
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
|
// shifts full size by is (for rect placement)
|
||||||
ImGui.Dummy(new Vector2(0f, frameHeight * 0.5f - itemSpacing.Y));
|
ImGui.Dummy(new(itemSpacing.X, 0));
|
||||||
|
ImGui.Dummy(new(0, itemSpacing.Y * 2)); // * 2 for some reason (otherwise the bottom is too skinny)
|
||||||
|
|
||||||
|
// inner group
|
||||||
ImGui.EndGroup();
|
ImGui.EndGroup();
|
||||||
|
|
||||||
var itemMin = ImGui.GetItemRectMin();
|
var (labelMin, labelMax, labelPadding) = GroupPanelLabelStack.Pop();
|
||||||
var itemMax = ImGui.GetItemRectMax();
|
|
||||||
var labelRect = GroupPanelLabelStack.Pop();
|
|
||||||
|
|
||||||
var halfFrame = new Vector2(frameHeight * 0.25f, frameHeight) * 0.5f;
|
var innerMin = ImGui.GetItemRectMin();
|
||||||
(Vector2 Min, Vector2 Max) frameRect = (itemMin + halfFrame, itemMax - new Vector2(halfFrame.X, 0));
|
var innerMax = ImGui.GetItemRectMax();
|
||||||
labelRect.Min.X -= itemSpacing.X;
|
// If there was actual text
|
||||||
labelRect.Max.X += itemSpacing.X;
|
if (labelMax.X != labelMin.X)
|
||||||
|
{
|
||||||
|
innerMin += new Vector2(0, labelPadding);
|
||||||
|
|
||||||
|
// add itemspacing padding on the label's sides
|
||||||
|
labelMin.X -= itemSpacing.X / 2;
|
||||||
|
labelMax.X += itemSpacing.X / 2;
|
||||||
|
}
|
||||||
for (var i = 0; i < 4; ++i)
|
for (var i = 0; i < 4; ++i)
|
||||||
{
|
{
|
||||||
var (minClip, maxClip) = i switch
|
var (minClip, maxClip) = i switch
|
||||||
{
|
{
|
||||||
0 => (new Vector2(float.NegativeInfinity), new Vector2(labelRect.Min.X, float.PositiveInfinity)),
|
0 => (new Vector2(float.NegativeInfinity), new Vector2(labelMin.X, float.PositiveInfinity)),
|
||||||
1 => (new Vector2(labelRect.Max.X, float.NegativeInfinity), new Vector2(float.PositiveInfinity)),
|
1 => (new Vector2(labelMax.X, float.NegativeInfinity), new Vector2(float.PositiveInfinity)),
|
||||||
2 => (new Vector2(labelRect.Min.X, float.NegativeInfinity), new Vector2(labelRect.Max.X, labelRect.Min.Y)),
|
2 => (new Vector2(labelMin.X, float.NegativeInfinity), new Vector2(labelMax.X, labelMin.Y)),
|
||||||
3 => (new Vector2(labelRect.Min.X, labelRect.Max.Y), new Vector2(labelRect.Max.X, float.PositiveInfinity)),
|
3 => (new Vector2(labelMin.X, labelMax.Y), new Vector2(labelMax.X, float.PositiveInfinity)),
|
||||||
_ => (Vector2.Zero, Vector2.Zero)
|
_ => (Vector2.Zero, Vector2.Zero)
|
||||||
};
|
};
|
||||||
|
|
||||||
ImGui.PushClipRect(minClip, maxClip, true);
|
ImGui.PushClipRect(minClip, maxClip, true);
|
||||||
ImGui.GetWindowDrawList().AddRect(
|
ImGui.GetWindowDrawList().AddRect(
|
||||||
frameRect.Min, frameRect.Max,
|
innerMin, innerMax,
|
||||||
ImGui.GetColorU32(ImGuiCol.Border),
|
ImGui.GetColorU32(ImGuiCol.Border),
|
||||||
halfFrame.X);
|
itemSpacing.X);
|
||||||
ImGui.PopClipRect();
|
ImGui.PopClipRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.PopStyleVar(2);
|
|
||||||
|
|
||||||
ImGui.Dummy(Vector2.Zero);
|
ImGui.Dummy(Vector2.Zero);
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.EndGroup();
|
ImGui.EndGroup();
|
||||||
|
|
||||||
|
ImGui.PopID();
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct EndUnconditionally : ImRaii.IEndObject, IDisposable
|
||||||
|
{
|
||||||
|
private Action EndAction { get; }
|
||||||
|
|
||||||
|
public bool Success { get; }
|
||||||
|
|
||||||
|
public bool Disposed { get; private set; }
|
||||||
|
|
||||||
|
public EndUnconditionally(Action endAction, bool success)
|
||||||
|
{
|
||||||
|
EndAction = endAction;
|
||||||
|
Success = success;
|
||||||
|
Disposed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!Disposed)
|
||||||
|
{
|
||||||
|
EndAction();
|
||||||
|
Disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ImRaii.IEndObject GroupPanel(string name, float width, out float internalWidth)
|
||||||
|
{
|
||||||
|
internalWidth = BeginGroupPanel(name, width);
|
||||||
|
return new EndUnconditionally(EndGroupPanel, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vector2 UnitCircle(float theta)
|
||||||
|
{
|
||||||
|
var (s, c) = MathF.SinCos(theta);
|
||||||
|
// SinCos positive y is downwards, but we want it upwards for ImGui
|
||||||
|
return new Vector2(c, -s);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float Lerp(float a, float b, float t) =>
|
||||||
|
MathF.FusedMultiplyAdd(b - a, t, a);
|
||||||
|
|
||||||
|
private static void ArcSegment(Vector2 o, Vector2 prev, Vector2 cur, Vector2? next, float radius, float ratio, uint color)
|
||||||
|
{
|
||||||
|
var d = ImGui.GetWindowDrawList();
|
||||||
|
|
||||||
|
d.PathLineTo(o + cur * radius);
|
||||||
|
d.PathLineTo(o + prev * radius);
|
||||||
|
d.PathLineTo(o + prev * radius * ratio);
|
||||||
|
d.PathLineTo(o + cur * radius * ratio);
|
||||||
|
if (next is { } nextValue)
|
||||||
|
d.PathLineTo(o + nextValue * radius);
|
||||||
|
d.PathFillConvex(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if (startAngle > endAngle)
|
||||||
|
(startAngle, endAngle) = (endAngle, startAngle);
|
||||||
|
|
||||||
|
var offset = ImGui.GetCursorScreenPos() + new Vector2(radius);
|
||||||
|
|
||||||
|
var segments = ImGui.GetWindowDrawList()._CalcCircleAutoSegmentCount(radius);
|
||||||
|
var incrementAngle = MathF.Tau / segments;
|
||||||
|
var isFullCircle = (endAngle - startAngle) % MathF.Tau == 0;
|
||||||
|
|
||||||
|
var prevA = startAngle;
|
||||||
|
var prev = UnitCircle(prevA);
|
||||||
|
for (var i = 1; i <= segments; ++i)
|
||||||
|
{
|
||||||
|
var a = startAngle + incrementAngle * i;
|
||||||
|
var cur = UnitCircle(a);
|
||||||
|
|
||||||
|
var nextA = a + incrementAngle;
|
||||||
|
var next = UnitCircle(nextA);
|
||||||
|
|
||||||
|
// full segment is background
|
||||||
|
if (prevA >= endAngle)
|
||||||
|
{
|
||||||
|
// don't overlap with the first segment
|
||||||
|
if (i == segments && !isFullCircle)
|
||||||
|
ArcSegment(offset, prev, cur, null, radius, ratio, backgroundColor);
|
||||||
|
else
|
||||||
|
ArcSegment(offset, prev, cur, next, radius, ratio, backgroundColor);
|
||||||
|
}
|
||||||
|
// segment is partially filled
|
||||||
|
else if (a > endAngle && !isFullCircle)
|
||||||
|
{
|
||||||
|
// we split the drawing in two
|
||||||
|
var end = UnitCircle(endAngle);
|
||||||
|
ArcSegment(offset, prev, end, null, radius, ratio, filledColor);
|
||||||
|
ArcSegment(offset, end, cur, next, radius, ratio, backgroundColor);
|
||||||
|
// set the previous segment to endAngle
|
||||||
|
a = endAngle;
|
||||||
|
cur = end;
|
||||||
|
}
|
||||||
|
// full segment is filled
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// if the next segment will be partially filled, the next segment will be the endAngle
|
||||||
|
if (nextA > endAngle && !isFullCircle)
|
||||||
|
{
|
||||||
|
var end = UnitCircle(endAngle);
|
||||||
|
ArcSegment(offset, prev, cur, end, radius, ratio, filledColor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
ArcSegment(offset, prev, cur, next, radius, ratio, filledColor);
|
||||||
|
}
|
||||||
|
prevA = a;
|
||||||
|
prev = cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addDummy)
|
||||||
|
ImGui.Dummy(new Vector2(radius * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ArcProgress(float value, float radiusInner, float radiusOuter, uint backgroundColor, uint filledColor)
|
||||||
|
{
|
||||||
|
Arc(MathF.PI / 2, MathF.PI / 2 - MathF.Tau * Math.Clamp(value, 0, 1), radiusInner, radiusOuter, backgroundColor, filledColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SearchableComboData<T> where T : class
|
||||||
|
{
|
||||||
|
public readonly ImmutableArray<T> items;
|
||||||
|
public List<T> filteredItems;
|
||||||
|
public T selectedItem;
|
||||||
|
public string input;
|
||||||
|
public bool wasTextActive;
|
||||||
|
public bool wasPopupActive;
|
||||||
|
public CancellationTokenSource? cts;
|
||||||
|
public Task? task;
|
||||||
|
|
||||||
|
private readonly Func<T, string> getString;
|
||||||
|
|
||||||
|
public SearchableComboData(IEnumerable<T> items, T selectedItem, Func<T, string> getString)
|
||||||
|
{
|
||||||
|
this.items = items.ToImmutableArray();
|
||||||
|
filteredItems = new() { selectedItem };
|
||||||
|
this.selectedItem = selectedItem;
|
||||||
|
this.getString = getString;
|
||||||
|
input = GetString(selectedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetItem(T selectedItem)
|
||||||
|
{
|
||||||
|
if (this.selectedItem != selectedItem)
|
||||||
|
{
|
||||||
|
input = GetString(selectedItem);
|
||||||
|
this.selectedItem = selectedItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetString(T item) => getString(item);
|
||||||
|
|
||||||
|
public void Filter()
|
||||||
|
{
|
||||||
|
cts?.Cancel();
|
||||||
|
var inp = input;
|
||||||
|
cts = new();
|
||||||
|
var token = cts.Token;
|
||||||
|
task = Task.Run(() => FilterTask(inp, token), token)
|
||||||
|
.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (cts.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Log.Error(e, "Filtering recipes failed");
|
||||||
|
}
|
||||||
|
}, TaskContinuationOptions.OnlyOnFaulted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FilterTask(string input, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
{
|
||||||
|
filteredItems = items.ToList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var matcher = new FuzzyMatcher(input.ToLowerInvariant(), MatchMode.FuzzyParts);
|
||||||
|
var query = items.AsParallel().Select(i => (Item: i, Score: matcher.Matches(getString(i).ToLowerInvariant())))
|
||||||
|
.Where(t => t.Score > 0)
|
||||||
|
.OrderByDescending(t => t.Score)
|
||||||
|
.Select(t => t.Item);
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
filteredItems = query.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static readonly Dictionary<uint, object> ComboData = new();
|
||||||
|
|
||||||
|
private static SearchableComboData<T> GetComboData<T>(uint comboKey, IEnumerable<T> items, T selectedItem, Func<T, string> getString) where T : class =>
|
||||||
|
(SearchableComboData<T>)(
|
||||||
|
ComboData.TryGetValue(comboKey, out var data)
|
||||||
|
? data
|
||||||
|
: ComboData[comboKey] = new SearchableComboData<T>(items, selectedItem, getString));
|
||||||
|
|
||||||
|
// 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 : class
|
||||||
|
{
|
||||||
|
var comboKey = ImGui.GetID(id);
|
||||||
|
var data = GetComboData(comboKey, items, selectedItem, getString);
|
||||||
|
data.SetItem(selectedItem);
|
||||||
|
|
||||||
|
using var pushId = ImRaii.PushId(id);
|
||||||
|
|
||||||
|
width = width == 0 ? ImGui.GetContentRegionAvail().X : width;
|
||||||
|
var availableSpace = Math.Min(ImGui.GetContentRegionAvail().X, width);
|
||||||
|
ImGui.SetNextItemWidth(availableSpace);
|
||||||
|
var isInputTextEnterPressed = ImGui.InputText("##input", ref data.input, 256, ImGuiInputTextFlags.EnterReturnsTrue);
|
||||||
|
var min = ImGui.GetItemRectMin();
|
||||||
|
var size = ImGui.GetItemRectSize();
|
||||||
|
size.X = Math.Min(size.X, availableSpace);
|
||||||
|
|
||||||
|
var isInputTextActivated = ImGui.IsItemActivated();
|
||||||
|
|
||||||
|
if (isInputTextActivated)
|
||||||
|
{
|
||||||
|
ImGui.SetNextWindowPos(min - ImGui.GetStyle().WindowPadding);
|
||||||
|
ImGui.OpenPopup("##popup");
|
||||||
|
data.wasTextActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var popup = ImRaii.Popup("##popup", ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoSavedSettings))
|
||||||
|
{
|
||||||
|
if (popup)
|
||||||
|
{
|
||||||
|
data.wasPopupActive = true;
|
||||||
|
|
||||||
|
if (isInputTextActivated)
|
||||||
|
{
|
||||||
|
ImGui.SetKeyboardFocusHere(0);
|
||||||
|
data.Filter();
|
||||||
|
}
|
||||||
|
ImGui.SetNextItemWidth(size.X);
|
||||||
|
if (ImGui.InputText("##input_popup", ref data.input, 256))
|
||||||
|
data.Filter();
|
||||||
|
var isActive = ImGui.IsItemActive();
|
||||||
|
if (!isActive && data.wasTextActive && ImGui.IsKeyPressed(ImGuiKey.Enter))
|
||||||
|
isInputTextEnterPressed = true;
|
||||||
|
data.wasTextActive = isActive;
|
||||||
|
|
||||||
|
using (var scrollingRegion = ImRaii.Child("scrollingRegion", new Vector2(size.X, size.Y * 10), false, ImGuiWindowFlags.HorizontalScrollbar))
|
||||||
|
{
|
||||||
|
T? _selectedItem = default;
|
||||||
|
var height = ImGui.GetTextLineHeight();
|
||||||
|
var r = ListClip(data.filteredItems, height, t =>
|
||||||
|
{
|
||||||
|
var name = getString(t);
|
||||||
|
using (var selectFont = ImRaii.PushFont(selectableFont))
|
||||||
|
{
|
||||||
|
if (ImGui.Selectable($"##{getId(t)}"))
|
||||||
|
{
|
||||||
|
_selectedItem = t;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X / 2f);
|
||||||
|
draw(t);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if (r)
|
||||||
|
{
|
||||||
|
selectedItem = _selectedItem!;
|
||||||
|
data.SetItem(selectedItem);
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInputTextEnterPressed || ImGui.IsKeyPressed(ImGuiKey.Escape))
|
||||||
|
{
|
||||||
|
if (isInputTextEnterPressed && data.filteredItems.Count > 0)
|
||||||
|
{
|
||||||
|
selectedItem = data.filteredItems[0];
|
||||||
|
data.SetItem(selectedItem);
|
||||||
|
}
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (data.wasPopupActive)
|
||||||
|
{
|
||||||
|
data.wasPopupActive = false;
|
||||||
|
data.input = getString(selectedItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ListClip<T>(IReadOnlyList<T> data, float lineHeight, Predicate<T> func)
|
||||||
|
{
|
||||||
|
ImGuiListClipperPtr imGuiListClipperPtr;
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
imGuiListClipperPtr = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
imGuiListClipperPtr.Begin(data.Count, lineHeight);
|
||||||
|
while (imGuiListClipperPtr.Step())
|
||||||
|
{
|
||||||
|
for (var i = imGuiListClipperPtr.DisplayStart; i <= imGuiListClipperPtr.DisplayEnd; i++)
|
||||||
|
{
|
||||||
|
if (i >= data.Count)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (i >= 0)
|
||||||
|
{
|
||||||
|
if (func(data[i]))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
imGuiListClipperPtr.End();
|
||||||
|
imGuiListClipperPtr.Destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
return ImGuiExtras.InputTextEx(label, hint, ref input, maxLength, size, flags | Multiline, callback, user_data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IconButtonSized(FontAwesomeIcon icon, Vector2 size)
|
public static bool IconButtonSized(FontAwesomeIcon icon, Vector2 size)
|
||||||
{
|
{
|
||||||
ImGui.PushFont(UiBuilder.IconFont);
|
using var font = ImRaii.PushFont(UiBuilder.IconFont);
|
||||||
var ret = ImGui.Button(icon.ToIconString(), size);
|
var ret = ImGui.Button(icon.ToIconString(), size);
|
||||||
ImGui.PopFont();
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,4 +496,62 @@ internal static class ImGuiUtils
|
|||||||
ImGui.SetTooltip("Open in Browser");
|
ImGui.SetTooltip("Open in Browser");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void AlignCentered(float width, float availWidth = default)
|
||||||
|
{
|
||||||
|
if (availWidth == default)
|
||||||
|
availWidth = ImGui.GetContentRegionAvail().X;
|
||||||
|
if (availWidth > width)
|
||||||
|
ImGui.SetCursorPosX(ImGui.GetCursorPos().X + (availWidth - width) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AlignRight(float width, float availWidth = default)
|
||||||
|
{
|
||||||
|
if (availWidth == default)
|
||||||
|
availWidth = ImGui.GetContentRegionAvail().X;
|
||||||
|
if (availWidth > width)
|
||||||
|
ImGui.SetCursorPosX(ImGui.GetCursorPos().X + availWidth - width);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void AlignMiddle(Vector2 size, Vector2 availSize = default)
|
||||||
|
{
|
||||||
|
if (availSize == default)
|
||||||
|
availSize = ImGui.GetContentRegionAvail();
|
||||||
|
if (availSize.X > size.X)
|
||||||
|
ImGui.SetCursorPosX(ImGui.GetCursorPos().X + (availSize.X - size.X) / 2);
|
||||||
|
if (availSize.Y > size.Y)
|
||||||
|
ImGui.SetCursorPosY(ImGui.GetCursorPos().Y + (availSize.Y - size.Y) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/67855985
|
||||||
|
public static void TextCentered(string text, float availWidth = default)
|
||||||
|
{
|
||||||
|
AlignCentered(ImGui.CalcTextSize(text).X, availWidth);
|
||||||
|
ImGui.TextUnformatted(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void TextRight(string text, float availWidth = default)
|
||||||
|
{
|
||||||
|
AlignRight(ImGui.CalcTextSize(text).X, availWidth);
|
||||||
|
ImGui.TextUnformatted(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void TextMiddleNewLine(string text, Vector2 availSize)
|
||||||
|
{
|
||||||
|
if (availSize == default)
|
||||||
|
availSize = ImGui.GetContentRegionAvail();
|
||||||
|
var c = ImGui.GetCursorPos();
|
||||||
|
AlignMiddle(ImGui.CalcTextSize(text), availSize);
|
||||||
|
ImGui.TextUnformatted(text);
|
||||||
|
ImGui.SetCursorPos(c + new Vector2(0, availSize.Y + ImGui.GetStyle().ItemSpacing.Y - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool ButtonCentered(string text, Vector2 buttonSize = default)
|
||||||
|
{
|
||||||
|
var buttonWidth = buttonSize.X;
|
||||||
|
if (buttonSize == default)
|
||||||
|
buttonWidth = ImGui.CalcTextSize(text).X + ImGui.GetStyle().FramePadding.X * 2;
|
||||||
|
AlignCentered(buttonWidth);
|
||||||
|
return ImGui.Button(text, buttonSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ public static class LuminaSheets
|
|||||||
public static readonly ExcelSheet<ClassJob> ClassJobSheet = Service.DataManager.GetExcelSheet<ClassJob>()!;
|
public static readonly ExcelSheet<ClassJob> ClassJobSheet = Service.DataManager.GetExcelSheet<ClassJob>()!;
|
||||||
public static readonly ExcelSheet<Item> ItemSheet = Service.DataManager.GetExcelSheet<Item>()!;
|
public static readonly ExcelSheet<Item> ItemSheet = Service.DataManager.GetExcelSheet<Item>()!;
|
||||||
public static readonly ExcelSheet<Item> ItemSheetEnglish = Service.DataManager.GetExcelSheet<Item>(ClientLanguage.English)!;
|
public static readonly ExcelSheet<Item> ItemSheetEnglish = Service.DataManager.GetExcelSheet<Item>(ClientLanguage.English)!;
|
||||||
|
public static readonly ExcelSheet<ENpcResident> ENpcResidentSheet = Service.DataManager.GetExcelSheet<ENpcResident>()!;
|
||||||
|
public static readonly ExcelSheet<Level> LevelSheet = Service.DataManager.GetExcelSheet<Level>()!;
|
||||||
|
public static readonly ExcelSheet<Quest> QuestSheet = Service.DataManager.GetExcelSheet<Quest>()!;
|
||||||
public static readonly ExcelSheet<Materia> MateriaSheet = Service.DataManager.GetExcelSheet<Materia>()!;
|
public static readonly ExcelSheet<Materia> MateriaSheet = Service.DataManager.GetExcelSheet<Materia>()!;
|
||||||
public static readonly ExcelSheet<BaseParam> BaseParamSheet = Service.DataManager.GetExcelSheet<BaseParam>()!;
|
public static readonly ExcelSheet<BaseParam> BaseParamSheet = Service.DataManager.GetExcelSheet<BaseParam>()!;
|
||||||
public static readonly ExcelSheet<ItemFood> ItemFoodSheet = Service.DataManager.GetExcelSheet<ItemFood>()!;
|
public static readonly ExcelSheet<ItemFood> ItemFoodSheet = Service.DataManager.GetExcelSheet<ItemFood>()!;
|
||||||
|
|||||||
+112
-36
@@ -1,76 +1,126 @@
|
|||||||
|
using Craftimizer.Plugin.Utils;
|
||||||
using Craftimizer.Plugin.Windows;
|
using Craftimizer.Plugin.Windows;
|
||||||
using Craftimizer.Simulator;
|
using Craftimizer.Simulator;
|
||||||
|
using Craftimizer.Simulator.Actions;
|
||||||
using Craftimizer.Utils;
|
using Craftimizer.Utils;
|
||||||
|
using Craftimizer.Windows;
|
||||||
|
using Dalamud.Game.Command;
|
||||||
|
using Dalamud.Interface.Internal;
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
using Dalamud.IoC;
|
using Dalamud.IoC;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using ImGuiScene;
|
using System;
|
||||||
using Lumina.Excel.GeneratedSheets;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using ClassJob = Craftimizer.Simulator.ClassJob;
|
|
||||||
|
|
||||||
namespace Craftimizer.Plugin;
|
namespace Craftimizer.Plugin;
|
||||||
|
|
||||||
public sealed class Plugin : IDalamudPlugin
|
public sealed class Plugin : IDalamudPlugin
|
||||||
{
|
{
|
||||||
public string Name => "Craftimizer";
|
|
||||||
public string Version { get; }
|
public string Version { get; }
|
||||||
public string Author { get; }
|
public string Author { get; }
|
||||||
public string Configuration { get; }
|
public string BuildConfiguration { get; }
|
||||||
public TextureWrap Icon { get; }
|
public IDalamudTextureWrap Icon { get; }
|
||||||
|
|
||||||
public WindowSystem WindowSystem { get; }
|
public WindowSystem WindowSystem { get; }
|
||||||
public Settings SettingsWindow { get; }
|
public Settings SettingsWindow { get; }
|
||||||
public CraftingLog RecipeNoteWindow { get; }
|
public RecipeNote RecipeNoteWindow { get; }
|
||||||
public Craft SynthesisWindow { get; }
|
public MacroList ListWindow { get; private set; }
|
||||||
public Windows.Simulator? SimulatorWindow { get; set; }
|
public MacroEditor? EditorWindow { get; private set; }
|
||||||
|
public MacroClipboard? ClipboardWindow { get; private set; }
|
||||||
|
|
||||||
|
public Configuration Configuration { get; }
|
||||||
public Hooks Hooks { get; }
|
public Hooks Hooks { get; }
|
||||||
public RecipeNote RecipeNote { get; }
|
public IconManager IconManager { get; }
|
||||||
|
|
||||||
public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface)
|
public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface)
|
||||||
{
|
{
|
||||||
Service.Plugin = this;
|
Service.Initialize(this, pluginInterface);
|
||||||
pluginInterface.Create<Service>();
|
|
||||||
Service.Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
|
WindowSystem = new("Craftimizer");
|
||||||
|
Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new();
|
||||||
|
Hooks = new();
|
||||||
|
IconManager = new();
|
||||||
|
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
Version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion;
|
Version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion;
|
||||||
Author = assembly.GetCustomAttribute<AssemblyCompanyAttribute>()!.Company;
|
Author = assembly.GetCustomAttribute<AssemblyCompanyAttribute>()!.Company;
|
||||||
Configuration = assembly.GetCustomAttribute<AssemblyConfigurationAttribute>()!.Configuration;
|
BuildConfiguration = assembly.GetCustomAttribute<AssemblyConfigurationAttribute>()!.Configuration;
|
||||||
byte[] iconData;
|
Icon = IconManager.GetAssemblyTexture("icon.png");
|
||||||
using (var stream = assembly.GetManifestResourceStream("Craftimizer.icon.png")!)
|
|
||||||
{
|
|
||||||
iconData = new byte[stream.Length];
|
|
||||||
_ = stream.Read(iconData);
|
|
||||||
}
|
|
||||||
Icon = Service.PluginInterface.UiBuilder.LoadImage(iconData);
|
|
||||||
|
|
||||||
Hooks = new();
|
|
||||||
RecipeNote = new();
|
|
||||||
WindowSystem = new(Name);
|
|
||||||
|
|
||||||
SettingsWindow = new();
|
SettingsWindow = new();
|
||||||
RecipeNoteWindow = new();
|
RecipeNoteWindow = new();
|
||||||
SynthesisWindow = new();
|
ListWindow = new();
|
||||||
|
|
||||||
|
// Trigger static constructors so a huge hitch doesn't occur on first RecipeNote frame.
|
||||||
|
FoodStatus.Initialize();
|
||||||
|
Gearsets.Initialize();
|
||||||
|
ActionUtils.Initialize();
|
||||||
|
|
||||||
Service.PluginInterface.UiBuilder.Draw += WindowSystem.Draw;
|
Service.PluginInterface.UiBuilder.Draw += WindowSystem.Draw;
|
||||||
Service.PluginInterface.UiBuilder.OpenConfigUi += OpenSettingsWindow;
|
Service.PluginInterface.UiBuilder.OpenConfigUi += OpenSettingsWindow;
|
||||||
|
Service.PluginInterface.UiBuilder.OpenMainUi += OpenCraftingLog;
|
||||||
|
|
||||||
|
Service.CommandManager.AddHandler("/craftimizer", new CommandInfo((_, _) => OpenSettingsWindow())
|
||||||
|
{
|
||||||
|
HelpMessage = "Open the settings window.",
|
||||||
|
});
|
||||||
|
Service.CommandManager.AddHandler("/craftmacros", new CommandInfo((_, _) => OpenMacroListWindow())
|
||||||
|
{
|
||||||
|
HelpMessage = "Open the crafting macros window.",
|
||||||
|
});
|
||||||
|
Service.CommandManager.AddHandler("/crafteditor", new CommandInfo((_, _) => OpenEmptyMacroEditor())
|
||||||
|
{
|
||||||
|
HelpMessage = "Open the crafting macro editor.",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenSimulatorWindow(Item item, bool isExpert, SimulationInput input, ClassJob classJob, Macro? macro)
|
public (CharacterStats? Character, RecipeData? Recipe, MacroEditor.CrafterBuffs? Buffs) GetOpenedStats()
|
||||||
{
|
{
|
||||||
if (SimulatorWindow != null)
|
var editorWindow = (EditorWindow?.IsOpen ?? false) ? EditorWindow : null;
|
||||||
{
|
var recipeData = editorWindow?.RecipeData ?? Service.Plugin.RecipeNoteWindow.RecipeData;
|
||||||
SimulatorWindow.IsOpen = false;
|
var characterStats = editorWindow?.CharacterStats ?? Service.Plugin.RecipeNoteWindow.CharacterStats;
|
||||||
WindowSystem.RemoveWindow(SimulatorWindow);
|
var buffs = editorWindow?.Buffs ?? (RecipeNoteWindow.CharacterStats != null ? new(Service.ClientState.LocalPlayer?.StatusList) : null);
|
||||||
|
|
||||||
|
return (characterStats, recipeData, buffs);
|
||||||
}
|
}
|
||||||
SimulatorWindow = new(item, isExpert, input, classJob, macro);
|
|
||||||
|
public (CharacterStats Character, RecipeData Recipe, MacroEditor.CrafterBuffs Buffs) GetDefaultStats()
|
||||||
|
{
|
||||||
|
var stats = GetOpenedStats();
|
||||||
|
return (
|
||||||
|
stats.Character ?? new()
|
||||||
|
{
|
||||||
|
Craftsmanship = 100,
|
||||||
|
Control = 100,
|
||||||
|
CP = 200,
|
||||||
|
Level = 10,
|
||||||
|
CanUseManipulation = false,
|
||||||
|
HasSplendorousBuff = false,
|
||||||
|
IsSpecialist = false,
|
||||||
|
CLvl = 10,
|
||||||
|
},
|
||||||
|
stats.Recipe ?? new(1023),
|
||||||
|
stats.Buffs ?? new(null)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenEmptyMacroEditor()
|
||||||
|
{
|
||||||
|
var stats = GetDefaultStats();
|
||||||
|
OpenMacroEditor(stats.Character, stats.Recipe, stats.Buffs, Enumerable.Empty<ActionType>(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenMacroEditor(CharacterStats characterStats, RecipeData recipeData, MacroEditor.CrafterBuffs buffs, IEnumerable<ActionType> actions, Action<IEnumerable<ActionType>>? setter)
|
||||||
|
{
|
||||||
|
EditorWindow?.Dispose();
|
||||||
|
EditorWindow = new(characterStats, recipeData, buffs, actions, setter);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenSettingsWindow()
|
public void OpenSettingsWindow()
|
||||||
{
|
{
|
||||||
SettingsWindow.IsOpen = true;
|
if (SettingsWindow.IsOpen ^= true)
|
||||||
SettingsWindow.BringToFront();
|
SettingsWindow.BringToFront();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,11 +130,37 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
SettingsWindow.SelectTab(selectedTabLabel);
|
SettingsWindow.SelectTab(selectedTabLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OpenMacroListWindow()
|
||||||
|
{
|
||||||
|
ListWindow.IsOpen = true;
|
||||||
|
ListWindow.BringToFront();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenCraftingLog()
|
||||||
|
{
|
||||||
|
Chat.SendMessage("/craftinglog");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenMacroClipboard(List<string> macros)
|
||||||
|
{
|
||||||
|
ClipboardWindow?.Dispose();
|
||||||
|
ClipboardWindow = new(macros);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CopyMacro(IReadOnlyList<ActionType> actions) =>
|
||||||
|
MacroCopy.Copy(actions);
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
SimulatorWindow?.Dispose();
|
Service.CommandManager.RemoveHandler("/craftimizer");
|
||||||
SynthesisWindow.Dispose();
|
Service.CommandManager.RemoveHandler("/craftmacros");
|
||||||
RecipeNote.Dispose();
|
Service.CommandManager.RemoveHandler("/crafteditor");
|
||||||
|
SettingsWindow.Dispose();
|
||||||
|
RecipeNoteWindow.Dispose();
|
||||||
|
ListWindow.Dispose();
|
||||||
|
EditorWindow?.Dispose();
|
||||||
|
ClipboardWindow?.Dispose();
|
||||||
Hooks.Dispose();
|
Hooks.Dispose();
|
||||||
|
IconManager.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-16
@@ -1,13 +1,10 @@
|
|||||||
using Dalamud.Data;
|
using Craftimizer.Utils;
|
||||||
using Dalamud.Game;
|
using Dalamud.Game;
|
||||||
using Dalamud.Game.ClientState;
|
|
||||||
using Dalamud.Game.ClientState.Conditions;
|
|
||||||
using Dalamud.Game.ClientState.Objects;
|
using Dalamud.Game.ClientState.Objects;
|
||||||
using Dalamud.Game.Command;
|
|
||||||
using Dalamud.Game.Gui;
|
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
using Dalamud.IoC;
|
using Dalamud.IoC;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
|
||||||
namespace Craftimizer.Plugin;
|
namespace Craftimizer.Plugin;
|
||||||
|
|
||||||
@@ -15,19 +12,28 @@ 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 DalamudPluginInterface PluginInterface { get; private set; }
|
[PluginService] public static DalamudPluginInterface PluginInterface { get; private set; }
|
||||||
[PluginService] public static CommandManager CommandManager { get; private set; }
|
[PluginService] public static ICommandManager CommandManager { get; private set; }
|
||||||
[PluginService] public static ObjectTable Objects { get; private set; }
|
[PluginService] public static IObjectTable Objects { get; private set; }
|
||||||
[PluginService] public static SigScanner SigScanner { get; private set; }
|
[PluginService] public static ISigScanner SigScanner { get; private set; }
|
||||||
[PluginService] public static GameGui GameGui { get; private set; }
|
[PluginService] public static IGameGui GameGui { get; private set; }
|
||||||
[PluginService] public static ClientState ClientState { get; private set; }
|
[PluginService] public static IClientState ClientState { get; private set; }
|
||||||
[PluginService] public static DataManager DataManager { get; private set; }
|
[PluginService] public static IDataManager DataManager { get; private set; }
|
||||||
[PluginService] public static TargetManager TargetManager { get; private set; }
|
[PluginService] public static ITextureProvider TextureProvider { get; private set; }
|
||||||
[PluginService] public static Condition Condition { get; private set; }
|
[PluginService] public static ITargetManager TargetManager { get; private set; }
|
||||||
[PluginService] public static Framework Framework { 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; }
|
||||||
|
|
||||||
public static Plugin Plugin { get; internal set; }
|
public static Plugin Plugin { get; private set; }
|
||||||
public static Configuration Configuration { get; internal set; }
|
public static Configuration Configuration => Plugin.Configuration;
|
||||||
public static WindowSystem WindowSystem => Plugin.WindowSystem;
|
public static WindowSystem WindowSystem => Plugin.WindowSystem;
|
||||||
|
public static IconManager IconManager => Plugin.IconManager;
|
||||||
#pragma warning restore CS8618
|
#pragma warning restore CS8618
|
||||||
|
|
||||||
|
internal static void Initialize(Plugin plugin, DalamudPluginInterface iface)
|
||||||
|
{
|
||||||
|
Plugin = plugin;
|
||||||
|
iface.Create<Service>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
using Craftimizer.Simulator;
|
using Craftimizer.Simulator;
|
||||||
using Craftimizer.Simulator.Actions;
|
using Craftimizer.Simulator.Actions;
|
||||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
|
using Dalamud.Interface.Internal;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using ImGuiScene;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||||
using Lumina.Excel.GeneratedSheets;
|
using Lumina.Excel.GeneratedSheets;
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -10,8 +13,10 @@ using System.Linq;
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Action = Lumina.Excel.GeneratedSheets.Action;
|
using Action = Lumina.Excel.GeneratedSheets.Action;
|
||||||
|
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.GeneratedSheets.Status;
|
||||||
|
|
||||||
namespace Craftimizer.Plugin;
|
namespace Craftimizer.Plugin;
|
||||||
|
|
||||||
@@ -58,6 +63,8 @@ internal static class ActionUtils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void Initialize() { }
|
||||||
|
|
||||||
public static (CraftAction? CraftAction, Action? Action) GetActionRow(this ActionType me, ClassJob classJob) =>
|
public static (CraftAction? CraftAction, Action? Action) GetActionRow(this ActionType me, ClassJob classJob) =>
|
||||||
ActionRows[(int)me, (int)classJob];
|
ActionRows[(int)me, (int)classJob];
|
||||||
|
|
||||||
@@ -81,15 +88,15 @@ internal static class ActionUtils
|
|||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TextureWrap GetIcon(this ActionType me, ClassJob classJob)
|
public static IDalamudTextureWrap GetIcon(this ActionType me, ClassJob classJob)
|
||||||
{
|
{
|
||||||
var (craftAction, action) = GetActionRow(me, classJob);
|
var (craftAction, action) = GetActionRow(me, classJob);
|
||||||
if (craftAction != null)
|
if (craftAction != null)
|
||||||
return Icons.GetIconFromId(craftAction.Icon);
|
return Service.IconManager.GetIcon(craftAction.Icon);
|
||||||
if (action != null)
|
if (action != null)
|
||||||
return Icons.GetIconFromId(action.Icon);
|
return Service.IconManager.GetIcon(action.Icon);
|
||||||
// Old "Steady Hand" action icon
|
// Old "Steady Hand" action icon
|
||||||
return Icons.GetIconFromId(1953);
|
return Service.IconManager.GetIcon(1953);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ActionType? GetActionTypeFromId(uint actionId, ClassJob classJob, bool isCraftAction)
|
public static ActionType? GetActionTypeFromId(uint actionId, ClassJob classJob, bool isCraftAction)
|
||||||
@@ -142,12 +149,33 @@ internal static class ClassJobUtils
|
|||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
|
|
||||||
public static string GetName(this ClassJob classJob)
|
public static sbyte GetExpArrayIdx(this ClassJob me) =>
|
||||||
|
LuminaSheets.ClassJobSheet.GetRow(me.GetClassJobIndex())!.ExpArrayIndex;
|
||||||
|
|
||||||
|
public static unsafe short GetPlayerLevel(this ClassJob me) =>
|
||||||
|
PlayerState.Instance()->ClassJobLevelArray[me.GetExpArrayIdx()];
|
||||||
|
|
||||||
|
public static unsafe bool CanPlayerUseManipulation(this ClassJob me) =>
|
||||||
|
ActionManager.CanUseActionOnTarget(ActionType.Manipulation.GetId(me), (GameObject*)Service.ClientState.LocalPlayer!.Address);
|
||||||
|
|
||||||
|
public static string GetName(this ClassJob me)
|
||||||
{
|
{
|
||||||
var job = LuminaSheets.ClassJobSheet.GetRow(classJob.GetClassJobIndex())!;
|
var job = LuminaSheets.ClassJobSheet.GetRow(me.GetClassJobIndex())!;
|
||||||
return CultureInfo.InvariantCulture.TextInfo.ToTitleCase(job.Name.ToDalamudString().TextValue);
|
return CultureInfo.InvariantCulture.TextInfo.ToTitleCase(job.Name.ToDalamudString().TextValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetAbbreviation(this ClassJob me)
|
||||||
|
{
|
||||||
|
var job = LuminaSheets.ClassJobSheet.GetRow(me.GetClassJobIndex())!;
|
||||||
|
return job.Abbreviation.ToDalamudString().TextValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Quest GetUnlockQuest(this ClassJob me) =>
|
||||||
|
LuminaSheets.QuestSheet.GetRow(65720 + (uint)me) ?? throw new ArgumentException($"Could not get unlock quest for {me}", nameof(me));
|
||||||
|
|
||||||
|
public static ushort GetIconId(this ClassJob me) =>
|
||||||
|
(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
|
||||||
{
|
{
|
||||||
@@ -282,11 +310,14 @@ internal static class EffectUtils
|
|||||||
EffectType.FinalAppraisal => 2190,
|
EffectType.FinalAppraisal => 2190,
|
||||||
EffectType.WasteNot2 => 257,
|
EffectType.WasteNot2 => 257,
|
||||||
EffectType.MuscleMemory => 2191,
|
EffectType.MuscleMemory => 2191,
|
||||||
EffectType.Manipulation => 258,
|
EffectType.Manipulation => 1164,
|
||||||
EffectType.HeartAndSoul => 2665,
|
EffectType.HeartAndSoul => 2665,
|
||||||
_ => 3412,
|
_ => throw new ArgumentOutOfRangeException(nameof(me)),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static bool IsIndefinite(this EffectType me) =>
|
||||||
|
me is EffectType.InnerQuiet or EffectType.HeartAndSoul;
|
||||||
|
|
||||||
public static Status Status(this EffectType me) =>
|
public static Status Status(this EffectType me) =>
|
||||||
LuminaSheets.StatusSheet.GetRow(me.StatusId())!;
|
LuminaSheets.StatusSheet.GetRow(me.StatusId())!;
|
||||||
|
|
||||||
@@ -299,8 +330,8 @@ internal static class EffectUtils
|
|||||||
return (ushort)iconId;
|
return (ushort)iconId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TextureWrap GetIcon(this EffectType me, int strength) =>
|
public static IDalamudTextureWrap GetIcon(this EffectType me, int strength) =>
|
||||||
Icons.GetIconFromId(me.GetIconId(strength));
|
Service.IconManager.GetIcon(me.GetIconId(strength));
|
||||||
|
|
||||||
public static string GetTooltip(this EffectType me, int strength, int duration)
|
public static string GetTooltip(this EffectType me, int strength, int duration)
|
||||||
{
|
{
|
||||||
|
|||||||
+15
-14
@@ -1,6 +1,7 @@
|
|||||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||||
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;
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -8,7 +9,7 @@ using System.Text;
|
|||||||
namespace Craftimizer.Plugin.Utils;
|
namespace Craftimizer.Plugin.Utils;
|
||||||
|
|
||||||
// https://github.com/Caraxi/SimpleTweaksPlugin/blob/0973b93931cdf8a1b01153984d62f76d998747ff/Utility/ChatHelper.cs#L17
|
// https://github.com/Caraxi/SimpleTweaksPlugin/blob/0973b93931cdf8a1b01153984d62f76d998747ff/Utility/ChatHelper.cs#L17
|
||||||
public static class Chat
|
public static unsafe class Chat
|
||||||
{
|
{
|
||||||
private static class Signatures
|
private static class Signatures
|
||||||
{
|
{
|
||||||
@@ -16,11 +17,11 @@ public static class Chat
|
|||||||
internal const string SanitiseString = "E8 ?? ?? ?? ?? EB 0A 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8D";
|
internal const string SanitiseString = "E8 ?? ?? ?? ?? EB 0A 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8D";
|
||||||
}
|
}
|
||||||
|
|
||||||
private delegate void ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4);
|
private delegate void ProcessChatBoxDelegate(UIModule* uiModule, IntPtr message, IntPtr unused, byte a4);
|
||||||
|
|
||||||
private static ProcessChatBoxDelegate? ProcessChatBox { get; }
|
private static ProcessChatBoxDelegate? ProcessChatBox { get; }
|
||||||
|
|
||||||
private static readonly unsafe delegate* unmanaged<Utf8String*, int, IntPtr, void> _sanitiseString = null!;
|
private static readonly unsafe delegate* unmanaged<Utf8String*, int, IntPtr, void> SanitiseString = null!;
|
||||||
|
|
||||||
static Chat()
|
static Chat()
|
||||||
{
|
{
|
||||||
@@ -33,7 +34,7 @@ public static class Chat
|
|||||||
{
|
{
|
||||||
if (Service.SigScanner.TryScanText(Signatures.SanitiseString, out var sanitisePtr))
|
if (Service.SigScanner.TryScanText(Signatures.SanitiseString, out var sanitisePtr))
|
||||||
{
|
{
|
||||||
_sanitiseString = (delegate* unmanaged<Utf8String*, int, IntPtr, void>)sanitisePtr;
|
SanitiseString = (delegate* unmanaged<Utf8String*, int, IntPtr, void>)sanitisePtr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +59,7 @@ public static class Chat
|
|||||||
throw new InvalidOperationException("Could not find signature for chat sending");
|
throw new InvalidOperationException("Could not find signature for chat sending");
|
||||||
}
|
}
|
||||||
|
|
||||||
var uiModule = (IntPtr)Framework.Instance()->GetUiModule();
|
var uiModule = Framework.Instance()->GetUiModule();
|
||||||
|
|
||||||
using var payload = new ChatPayload(message);
|
using var payload = new ChatPayload(message);
|
||||||
var mem1 = Marshal.AllocHGlobal(400);
|
var mem1 = Marshal.AllocHGlobal(400);
|
||||||
@@ -118,14 +119,14 @@ public static class Chat
|
|||||||
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
|
/// <exception cref="InvalidOperationException">If the signature for this function could not be found</exception>
|
||||||
public static unsafe string SanitiseText(string text)
|
public static unsafe string SanitiseText(string text)
|
||||||
{
|
{
|
||||||
if (_sanitiseString == null)
|
if (SanitiseString == null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Could not find signature for chat sanitisation");
|
throw new InvalidOperationException("Could not find signature for chat sanitisation");
|
||||||
}
|
}
|
||||||
|
|
||||||
var uText = Utf8String.FromString(text);
|
var uText = Utf8String.FromString(text);
|
||||||
|
|
||||||
_sanitiseString(uText, 0x27F, IntPtr.Zero);
|
SanitiseString(uText, 0x27F, IntPtr.Zero);
|
||||||
var sanitised = uText->ToString();
|
var sanitised = uText->ToString();
|
||||||
|
|
||||||
uText->Dtor();
|
uText->Dtor();
|
||||||
@@ -151,19 +152,19 @@ public static class Chat
|
|||||||
|
|
||||||
internal ChatPayload(byte[] stringBytes)
|
internal ChatPayload(byte[] stringBytes)
|
||||||
{
|
{
|
||||||
this.textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
|
textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
|
||||||
Marshal.Copy(stringBytes, 0, this.textPtr, stringBytes.Length);
|
Marshal.Copy(stringBytes, 0, textPtr, stringBytes.Length);
|
||||||
Marshal.WriteByte(this.textPtr + stringBytes.Length, 0);
|
Marshal.WriteByte(textPtr + stringBytes.Length, 0);
|
||||||
|
|
||||||
this.textLen = (ulong)(stringBytes.Length + 1);
|
textLen = (ulong)(stringBytes.Length + 1);
|
||||||
|
|
||||||
this.unk1 = 64;
|
unk1 = 64;
|
||||||
this.unk2 = 0;
|
unk2 = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Marshal.FreeHGlobal(this.textPtr);
|
Marshal.FreeHGlobal(textPtr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Craftimizer.Utils;
|
||||||
|
|
||||||
|
public static class Colors
|
||||||
|
{
|
||||||
|
public static readonly Vector4 Progress = new(0.44f, 0.65f, 0.18f, 1f);
|
||||||
|
public static readonly Vector4 Quality = new(0.26f, 0.71f, 0.69f, 1f);
|
||||||
|
public static readonly Vector4 Durability = new(0.13f, 0.52f, 0.93f, 1f);
|
||||||
|
public static readonly Vector4 HQ = new(0.592f, 0.863f, 0.376f, 1f);
|
||||||
|
public static readonly Vector4 CP = new(0.63f, 0.37f, 0.75f, 1f);
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
using Craftimizer.Plugin;
|
||||||
|
using Craftimizer.Plugin.Utils;
|
||||||
|
using Lumina.Excel.GeneratedSheets;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Craftimizer.Utils;
|
||||||
|
|
||||||
|
public static class FoodStatus
|
||||||
|
{
|
||||||
|
private static readonly ReadOnlyDictionary<uint, uint> ItemFoodToItemLUT;
|
||||||
|
private static readonly ReadOnlyDictionary<uint, Food> FoodItems;
|
||||||
|
private static readonly ReadOnlyDictionary<uint, Food> MedicineItems;
|
||||||
|
private static readonly ImmutableArray<uint> FoodOrder;
|
||||||
|
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 Food(Item Item, FoodStat? Craftsmanship, FoodStat? Control, FoodStat? CP);
|
||||||
|
|
||||||
|
static FoodStatus()
|
||||||
|
{
|
||||||
|
var lut = new Dictionary<uint, uint>();
|
||||||
|
foreach (var item in LuminaSheets.ItemSheet)
|
||||||
|
{
|
||||||
|
var isFood = item.ItemUICategory.Row == 46;
|
||||||
|
var isMedicine = item.ItemUICategory.Row == 44;
|
||||||
|
if (!isFood && !isMedicine)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (item.ItemAction.Value == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!(item.ItemAction.Value.Type is 844 or 845 or 846))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var itemFood = LuminaSheets.ItemFoodSheet.GetRow(item.ItemAction.Value.Data[1]);
|
||||||
|
if (itemFood == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
lut.TryAdd(itemFood.RowId, item.RowId);
|
||||||
|
}
|
||||||
|
ItemFoodToItemLUT = lut.AsReadOnly();
|
||||||
|
|
||||||
|
var foods = new Dictionary<uint, Food>();
|
||||||
|
var medicines = new Dictionary<uint, Food>();
|
||||||
|
foreach (var item in LuminaSheets.ItemSheet)
|
||||||
|
{
|
||||||
|
var isFood = item.ItemUICategory.Row == 46;
|
||||||
|
var isMedicine = item.ItemUICategory.Row == 44;
|
||||||
|
if (!isFood && !isMedicine)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (item.ItemAction.Value == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!(item.ItemAction.Value.Type is 844 or 845 or 846))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var itemFood = LuminaSheets.ItemFoodSheet.GetRow(item.ItemAction.Value.Data[1]);
|
||||||
|
if (itemFood == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
FoodStat? craftsmanship = null, control = null, cp = null;
|
||||||
|
foreach (var stat in itemFood.UnkData1)
|
||||||
|
{
|
||||||
|
if (stat.BaseParam == 0)
|
||||||
|
continue;
|
||||||
|
var foodStat = new FoodStat(stat.IsRelative, stat.Value, stat.Max, stat.ValueHQ, stat.MaxHQ);
|
||||||
|
switch (stat.BaseParam)
|
||||||
|
{
|
||||||
|
case Gearsets.ParamCraftsmanship: craftsmanship = foodStat; break;
|
||||||
|
case Gearsets.ParamControl: control = foodStat; break;
|
||||||
|
case Gearsets.ParamCP: cp = foodStat; break;
|
||||||
|
default: continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (craftsmanship != null || control != null || cp != null)
|
||||||
|
{
|
||||||
|
var food = new Food(item, craftsmanship, control, cp);
|
||||||
|
if (isFood)
|
||||||
|
foods.Add(item.RowId, food);
|
||||||
|
if (isMedicine)
|
||||||
|
medicines.Add(item.RowId, food);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FoodItems = foods.AsReadOnly();
|
||||||
|
MedicineItems = medicines.AsReadOnly();
|
||||||
|
|
||||||
|
FoodOrder = FoodItems.OrderByDescending(a => a.Value.Item.LevelItem.Row).Select(a => a.Key).ToImmutableArray();
|
||||||
|
MedicineOrder = MedicineItems.OrderByDescending(a => a.Value.Item.LevelItem.Row).Select(a => a.Key).ToImmutableArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Initialize() { }
|
||||||
|
|
||||||
|
public static IEnumerable<Food> OrderedFoods => FoodOrder.Select(id => FoodItems[id]);
|
||||||
|
public static IEnumerable<Food> OrderedMedicines => MedicineOrder.Select(id => MedicineItems[id]);
|
||||||
|
|
||||||
|
public static (uint ItemId, bool IsHQ)? ResolveFoodParam(ushort param)
|
||||||
|
{
|
||||||
|
var isHq = param > 10000;
|
||||||
|
param -= 10000;
|
||||||
|
|
||||||
|
if (!ItemFoodToItemLUT.TryGetValue(param, out var itemId))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return (itemId, isHq);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Food? TryGetFood(uint itemId)
|
||||||
|
{
|
||||||
|
if (FoodItems.TryGetValue(itemId, out var food))
|
||||||
|
return food;
|
||||||
|
if (MedicineItems.TryGetValue(itemId, out food))
|
||||||
|
return food;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Craftimizer.Utils;
|
||||||
|
|
||||||
|
internal readonly struct FuzzyMatcher
|
||||||
|
{
|
||||||
|
private const bool IsBorderMatching = true;
|
||||||
|
private static readonly (int, int)[] EmptySegArray = Array.Empty<(int, int)>();
|
||||||
|
|
||||||
|
private readonly string needleString = string.Empty;
|
||||||
|
private readonly int needleFinalPosition = -1;
|
||||||
|
private readonly (int Start, int End)[] needleSegments = EmptySegArray;
|
||||||
|
private readonly MatchMode mode = MatchMode.Simple;
|
||||||
|
|
||||||
|
public FuzzyMatcher(string term, MatchMode matchMode)
|
||||||
|
{
|
||||||
|
needleString = term;
|
||||||
|
needleFinalPosition = needleString.Length - 1;
|
||||||
|
mode = matchMode;
|
||||||
|
|
||||||
|
needleSegments = matchMode switch
|
||||||
|
{
|
||||||
|
MatchMode.FuzzyParts => FindNeedleSegments(needleString),
|
||||||
|
MatchMode.Fuzzy or MatchMode.Simple => EmptySegArray,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(matchMode), matchMode, "Invalid match mode"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int Start, int End)[] FindNeedleSegments(ReadOnlySpan<char> span)
|
||||||
|
{
|
||||||
|
var segments = new List<(int, int)>();
|
||||||
|
var wordStart = -1;
|
||||||
|
|
||||||
|
for (var i = 0; i < span.Length; i++)
|
||||||
|
{
|
||||||
|
if (span[i] is not ' ' and not '\u3000')
|
||||||
|
{
|
||||||
|
if (wordStart < 0)
|
||||||
|
wordStart = i;
|
||||||
|
}
|
||||||
|
else if (wordStart >= 0)
|
||||||
|
{
|
||||||
|
segments.Add((wordStart, i - 1));
|
||||||
|
wordStart = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wordStart >= 0)
|
||||||
|
segments.Add((wordStart, span.Length - 1));
|
||||||
|
|
||||||
|
return segments.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Matches(string value)
|
||||||
|
{
|
||||||
|
if (needleFinalPosition < 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (mode == MatchMode.Simple)
|
||||||
|
return value.Contains(needleString, StringComparison.InvariantCultureIgnoreCase) ? 1 : 0;
|
||||||
|
|
||||||
|
if (mode == MatchMode.Fuzzy)
|
||||||
|
return GetRawScore(value, 0, needleFinalPosition);
|
||||||
|
|
||||||
|
if (mode == MatchMode.FuzzyParts)
|
||||||
|
{
|
||||||
|
if (needleSegments.Length < 2)
|
||||||
|
return GetRawScore(value, 0, needleFinalPosition);
|
||||||
|
|
||||||
|
var total = 0;
|
||||||
|
for (var i = 0; i < needleSegments.Length; i++)
|
||||||
|
{
|
||||||
|
var (start, end) = needleSegments[i];
|
||||||
|
var cur = GetRawScore(value, start, end);
|
||||||
|
if (cur == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
total += cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int MatchesAny(params string[] values)
|
||||||
|
{
|
||||||
|
var max = 0;
|
||||||
|
for (var i = 0; i < values.Length; i++)
|
||||||
|
{
|
||||||
|
var cur = Matches(values[i]);
|
||||||
|
if (cur > max)
|
||||||
|
max = cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetRawScore(ReadOnlySpan<char> haystack, int needleStart, int needleEnd)
|
||||||
|
{
|
||||||
|
var (startPos, gaps, consecutive, borderMatches, endPos) = FindForward(haystack, needleStart, needleEnd);
|
||||||
|
if (startPos < 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var needleSize = needleEnd - needleStart + 1;
|
||||||
|
|
||||||
|
var score = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches);
|
||||||
|
|
||||||
|
(startPos, gaps, consecutive, borderMatches) = FindReverse(haystack, endPos, needleStart, needleEnd);
|
||||||
|
var revScore = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches);
|
||||||
|
|
||||||
|
return int.Max(score, revScore);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static int CalculateRawScore(int needleSize, int startPos, int gaps, int consecutive, int borderMatches)
|
||||||
|
{
|
||||||
|
var score = 100
|
||||||
|
+ needleSize * 3
|
||||||
|
+ borderMatches * 3
|
||||||
|
+ consecutive * 5
|
||||||
|
- startPos
|
||||||
|
- gaps * 2;
|
||||||
|
if (startPos == 0)
|
||||||
|
score += 5;
|
||||||
|
return score < 1 ? 1 : score;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (int StartPos, int Gaps, int Consecutive, int BorderMatches, int HaystackIndex) FindForward(
|
||||||
|
ReadOnlySpan<char> haystack, int needleStart, int needleEnd)
|
||||||
|
{
|
||||||
|
var needleIndex = needleStart;
|
||||||
|
var lastMatchIndex = -10;
|
||||||
|
|
||||||
|
var startPos = 0;
|
||||||
|
var gaps = 0;
|
||||||
|
var consecutive = 0;
|
||||||
|
var borderMatches = 0;
|
||||||
|
|
||||||
|
for (var haystackIndex = 0; haystackIndex < haystack.Length; haystackIndex++)
|
||||||
|
{
|
||||||
|
if (haystack[haystackIndex] == needleString[needleIndex])
|
||||||
|
{
|
||||||
|
if (IsBorderMatching)
|
||||||
|
{
|
||||||
|
if (haystackIndex > 0)
|
||||||
|
{
|
||||||
|
if (!char.IsLetterOrDigit(haystack[haystackIndex - 1]))
|
||||||
|
borderMatches++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
needleIndex++;
|
||||||
|
|
||||||
|
if (haystackIndex == lastMatchIndex + 1)
|
||||||
|
consecutive++;
|
||||||
|
|
||||||
|
if (needleIndex > needleEnd)
|
||||||
|
return (startPos, gaps, consecutive, borderMatches, haystackIndex);
|
||||||
|
|
||||||
|
lastMatchIndex = haystackIndex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (needleIndex > needleStart)
|
||||||
|
gaps++;
|
||||||
|
else
|
||||||
|
startPos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (-1, 0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private (int StartPos, int Gaps, int Consecutive, int BorderMatches) FindReverse(
|
||||||
|
ReadOnlySpan<char> haystack, int haystackLastMatchIndex, int needleStart, int needleEnd)
|
||||||
|
{
|
||||||
|
var needleIndex = needleEnd;
|
||||||
|
var revLastMatchIndex = haystack.Length + 10;
|
||||||
|
|
||||||
|
var gaps = 0;
|
||||||
|
var consecutive = 0;
|
||||||
|
var borderMatches = 0;
|
||||||
|
|
||||||
|
for (var haystackIndex = haystackLastMatchIndex; haystackIndex >= 0; haystackIndex--)
|
||||||
|
{
|
||||||
|
if (haystack[haystackIndex] == needleString[needleIndex])
|
||||||
|
{
|
||||||
|
if (IsBorderMatching)
|
||||||
|
{
|
||||||
|
if (haystackIndex > 0)
|
||||||
|
{
|
||||||
|
if (!char.IsLetterOrDigit(haystack[haystackIndex - 1]))
|
||||||
|
borderMatches++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
needleIndex--;
|
||||||
|
|
||||||
|
if (haystackIndex == revLastMatchIndex - 1)
|
||||||
|
consecutive++;
|
||||||
|
|
||||||
|
if (needleIndex < needleStart)
|
||||||
|
return (haystackIndex, gaps, consecutive, borderMatches);
|
||||||
|
|
||||||
|
revLastMatchIndex = haystackIndex;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
gaps++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (-1, 0, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum MatchMode
|
||||||
|
{
|
||||||
|
Simple,
|
||||||
|
Fuzzy,
|
||||||
|
FuzzyParts,
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace Craftimizer.Plugin.Utils;
|
namespace Craftimizer.Plugin.Utils;
|
||||||
internal 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);
|
||||||
@@ -20,6 +20,24 @@ internal static unsafe class Gearsets
|
|||||||
public const int ParamCraftsmanship = 70;
|
public const int ParamCraftsmanship = 70;
|
||||||
public const int ParamControl = 71;
|
public const int ParamControl = 71;
|
||||||
|
|
||||||
|
private static readonly int[] LevelToCLvlLUT;
|
||||||
|
|
||||||
|
static Gearsets()
|
||||||
|
{
|
||||||
|
LevelToCLvlLUT = new int[90];
|
||||||
|
for (uint i = 0; i < 80; ++i) {
|
||||||
|
var level = i + 1;
|
||||||
|
LevelToCLvlLUT[i] = LuminaSheets.ParamGrowSheet.GetRow(level)!.CraftingLevel;
|
||||||
|
}
|
||||||
|
for (var i = 80; i < 90; ++i)
|
||||||
|
{
|
||||||
|
var level = i + 1;
|
||||||
|
LevelToCLvlLUT[i] = (int)LuminaSheets.RecipeLevelTableSheet.First(r => r.ClassJobLevel == level).RowId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Initialize() { }
|
||||||
|
|
||||||
public static GearsetItem[] GetGearsetItems(InventoryContainer* container)
|
public static GearsetItem[] GetGearsetItems(InventoryContainer* container)
|
||||||
{
|
{
|
||||||
var items = new GearsetItem[(int)container->Size];
|
var items = new GearsetItem[(int)container->Size];
|
||||||
@@ -33,7 +51,7 @@ internal static unsafe class Gearsets
|
|||||||
|
|
||||||
public static GearsetItem[] GetGearsetItems(RaptureGearsetModule.GearsetEntry* entry)
|
public static GearsetItem[] GetGearsetItems(RaptureGearsetModule.GearsetEntry* entry)
|
||||||
{
|
{
|
||||||
var gearsetItems = new Span<RaptureGearsetModule.GearsetItem>(entry->ItemsData, 14);
|
var gearsetItems = entry->ItemsSpan;
|
||||||
var items = new GearsetItem[14];
|
var items = new GearsetItem[14];
|
||||||
for (var i = 0; i < 14; ++i)
|
for (var i = 0; i < 14; ++i)
|
||||||
{
|
{
|
||||||
@@ -117,18 +135,21 @@ internal static unsafe class Gearsets
|
|||||||
|
|
||||||
public static bool IsSpecialistSoulCrystal(GearsetItem item)
|
public static bool IsSpecialistSoulCrystal(GearsetItem item)
|
||||||
{
|
{
|
||||||
|
if (item.itemId == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
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.Row != 62 && luminaItem.ClassJobUse.Value!.ClassJobCategory.Row == 33;
|
return luminaItem.ItemUICategory.Row == 62 && luminaItem.ClassJobUse.Value!.ClassJobCategory.Row == 33;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsSplendorousTool(GearsetItem item) =>
|
public static bool IsSplendorousTool(GearsetItem item) =>
|
||||||
LuminaSheets.ItemSheetEnglish.GetRow(item.itemId)!.Description.ToDalamudString().TextValue.Contains("Increases to quality are 1.75 times higher than normal when material condition is Good.", StringComparison.Ordinal);
|
LuminaSheets.ItemSheetEnglish.GetRow(item.itemId)!.Description.ToDalamudString().TextValue.Contains("Increases to quality are 1.75 times higher than normal when material condition is Good.", StringComparison.Ordinal);
|
||||||
|
|
||||||
public static int CalculateCLvl(int characterLevel) =>
|
public static int CalculateCLvl(int level) =>
|
||||||
characterLevel <= 80
|
(level > 0 && level <= 90) ?
|
||||||
? LuminaSheets.ParamGrowSheet.GetRow((uint)characterLevel)!.CraftingLevel
|
LevelToCLvlLUT[level - 1] :
|
||||||
: (int)LuminaSheets.RecipeLevelTableSheet.First(r => r.ClassJobLevel == characterLevel).RowId;
|
throw new ArgumentOutOfRangeException(nameof(level), level, "Level is out of range.");
|
||||||
|
|
||||||
// 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, int paramId)
|
private static int CalculateParamCap(Item item, int paramId)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public sealed unsafe class Hooks : IDisposable
|
|||||||
|
|
||||||
public Hooks()
|
public Hooks()
|
||||||
{
|
{
|
||||||
UseActionHook = Hook<UseActionDelegate>.FromAddress((nint)ActionManager.MemberFunctionPointers.UseAction, UseActionDetour);
|
UseActionHook = Service.GameInteropProvider.HookFromAddress<UseActionDelegate>((nint)ActionManager.MemberFunctionPointers.UseAction, UseActionDetour);
|
||||||
UseActionHook.Enable();
|
UseActionHook.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ public sealed unsafe class Hooks : IDisposable
|
|||||||
{
|
{
|
||||||
var canCast = manager->GetActionStatus(actionType, actionId) == 0;
|
var canCast = manager->GetActionStatus(actionType, actionId) == 0;
|
||||||
var ret = UseActionHook.Original(manager, actionType, actionId, targetId, param, useType, pvp, a8);
|
var ret = UseActionHook.Original(manager, actionType, actionId, targetId, param, useType, pvp, a8);
|
||||||
if (canCast && ret && (actionType == CSActionType.CraftAction || actionType == CSActionType.Spell))
|
if (canCast && ret && (actionType == CSActionType.CraftAction || actionType == CSActionType.Action))
|
||||||
{
|
{
|
||||||
var classJob = ClassJobUtils.GetClassJobFromIdx((byte)(Service.ClientState.LocalPlayer?.ClassJob.Id ?? 0));
|
var classJob = ClassJobUtils.GetClassJobFromIdx((byte)(Service.ClientState.LocalPlayer?.ClassJob.Id ?? 0));
|
||||||
if (classJob != null)
|
if (classJob != null)
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using Craftimizer.Plugin;
|
||||||
|
using Dalamud.Interface.Internal;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Craftimizer.Utils;
|
||||||
|
|
||||||
|
public sealed class IconManager : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Dictionary<uint, IDalamudTextureWrap> iconCache = new();
|
||||||
|
private readonly Dictionary<uint, IDalamudTextureWrap> hqIconCache = new();
|
||||||
|
private readonly Dictionary<string, IDalamudTextureWrap> textureCache = new();
|
||||||
|
private readonly Dictionary<string, IDalamudTextureWrap> assemblyCache = new();
|
||||||
|
|
||||||
|
public IDalamudTextureWrap GetIcon(uint id)
|
||||||
|
{
|
||||||
|
if (!iconCache.TryGetValue(id, out var ret))
|
||||||
|
iconCache.Add(id, ret = Service.TextureProvider.GetIcon(id) ??
|
||||||
|
throw new ArgumentException($"Invalid icon id {id}", nameof(id)));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDalamudTextureWrap GetHqIcon(uint id, bool isHq = true)
|
||||||
|
{
|
||||||
|
if (!isHq)
|
||||||
|
return GetIcon(id);
|
||||||
|
if (!hqIconCache.TryGetValue(id, out var ret))
|
||||||
|
hqIconCache.Add(id, ret = Service.TextureProvider.GetIcon(id, ITextureProvider.IconFlags.HiRes | ITextureProvider.IconFlags.ItemHighQuality) ??
|
||||||
|
throw new ArgumentException($"Invalid hq icon id {id}", nameof(id)));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDalamudTextureWrap GetTexture(string path)
|
||||||
|
{
|
||||||
|
if (!textureCache.TryGetValue(path, out var ret))
|
||||||
|
textureCache.Add(path, ret = Service.TextureProvider.GetTextureFromGame(path) ??
|
||||||
|
throw new ArgumentException($"Invalid texture {path}", nameof(path)));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDalamudTextureWrap GetAssemblyTexture(string filename)
|
||||||
|
{
|
||||||
|
if (!assemblyCache.TryGetValue(filename, out var ret))
|
||||||
|
assemblyCache.Add(filename, ret = GetAssemblyTextureInternal(filename));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IDalamudTextureWrap GetAssemblyTextureInternal(string filename)
|
||||||
|
{
|
||||||
|
var assembly = Assembly.GetExecutingAssembly();
|
||||||
|
byte[] iconData;
|
||||||
|
using (var stream = assembly.GetManifestResourceStream($"Craftimizer.{filename}") ?? throw new InvalidDataException($"Could not load resource {filename}"))
|
||||||
|
{
|
||||||
|
iconData = new byte[stream.Length];
|
||||||
|
_ = stream.Read(iconData);
|
||||||
|
}
|
||||||
|
return Service.PluginInterface.UiBuilder.LoadImage(iconData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var image in iconCache.Values)
|
||||||
|
image.Dispose();
|
||||||
|
iconCache.Clear();
|
||||||
|
|
||||||
|
foreach (var image in hqIconCache.Values)
|
||||||
|
image.Dispose();
|
||||||
|
hqIconCache.Clear();
|
||||||
|
|
||||||
|
foreach (var image in textureCache.Values)
|
||||||
|
image.Dispose();
|
||||||
|
textureCache.Clear();
|
||||||
|
|
||||||
|
foreach (var image in assemblyCache.Values)
|
||||||
|
image.Dispose();
|
||||||
|
assemblyCache.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Craftimizer.Plugin;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Craftimizer.Utils;
|
||||||
|
|
||||||
|
public static class Log
|
||||||
|
{
|
||||||
|
public static void Debug(string line) => Service.PluginLog.Debug(line);
|
||||||
|
|
||||||
|
public static void Error(string line) => Service.PluginLog.Error(line);
|
||||||
|
public static void Error(Exception e, string line) => Service.PluginLog.Error(e, line);
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
using Craftimizer.Plugin;
|
||||||
|
using Craftimizer.Simulator;
|
||||||
|
using Craftimizer.Simulator.Actions;
|
||||||
|
using Dalamud.Interface.Internal.Notifications;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Memory;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.String;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||||
|
using ImGuiNET;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Craftimizer.Utils;
|
||||||
|
|
||||||
|
public static class MacroCopy
|
||||||
|
{
|
||||||
|
private const ClassJob DefaultJob = ClassJob.Carpenter;
|
||||||
|
private const int MacroSize = 15;
|
||||||
|
|
||||||
|
public static void Copy(IReadOnlyList<ActionType> actions)
|
||||||
|
{
|
||||||
|
if (actions.Count == 0)
|
||||||
|
{
|
||||||
|
Service.PluginInterface.UiBuilder.AddNotification("Could not copy macro. It's empty!", "Craftimizer Macro Not Copied", NotificationType.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = Service.Configuration.MacroCopy;
|
||||||
|
var macros = new List<string>();
|
||||||
|
var s = new List<string>();
|
||||||
|
for (var i = 0; i < actions.Count; ++i)
|
||||||
|
{
|
||||||
|
if (s.Count == 0)
|
||||||
|
{
|
||||||
|
if (config.UseMacroLock)
|
||||||
|
s.Add("/mlock");
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Add(GetActionCommand(actions[i], config));
|
||||||
|
|
||||||
|
if (config.Type == MacroCopyConfiguration.CopyType.CopyToMacro || !config.CombineMacro)
|
||||||
|
{
|
||||||
|
if (i != actions.Count - 1 && (i != actions.Count - 2 || config.ForceNotification))
|
||||||
|
{
|
||||||
|
if (s.Count == MacroSize - 1)
|
||||||
|
{
|
||||||
|
if (GetEndCommand(macros.Count, false, config) is { } endCommand)
|
||||||
|
s.Add(endCommand);
|
||||||
|
}
|
||||||
|
if (s.Count == MacroSize)
|
||||||
|
{
|
||||||
|
macros.Add(string.Join(Environment.NewLine, s));
|
||||||
|
s.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (s.Count > 0)
|
||||||
|
{
|
||||||
|
if (s.Count < MacroSize || (config.Type != MacroCopyConfiguration.CopyType.CopyToMacro && config.CombineMacro))
|
||||||
|
{
|
||||||
|
if (GetEndCommand(macros.Count, true, config) is { } endCommand)
|
||||||
|
s.Add(endCommand);
|
||||||
|
}
|
||||||
|
macros.Add(string.Join(Environment.NewLine, s));
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (config.Type)
|
||||||
|
{
|
||||||
|
case MacroCopyConfiguration.CopyType.OpenWindow:
|
||||||
|
Service.Plugin.OpenMacroClipboard(macros);
|
||||||
|
break;
|
||||||
|
case MacroCopyConfiguration.CopyType.CopyToMacro:
|
||||||
|
CopyToMacro(macros, config);
|
||||||
|
break;
|
||||||
|
case MacroCopyConfiguration.CopyType.CopyToClipboard:
|
||||||
|
CopyToClipboard(macros, config);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetActionCommand(ActionType action, MacroCopyConfiguration config)
|
||||||
|
{
|
||||||
|
var actionBase = action.Base();
|
||||||
|
if (actionBase is BaseComboAction)
|
||||||
|
throw new ArgumentException("Combo actions are not supported", nameof(action));
|
||||||
|
if (config.Type != MacroCopyConfiguration.CopyType.CopyToMacro && config.RemoveWaitTimes)
|
||||||
|
return $"/ac \"{action.GetName(DefaultJob)}\"";
|
||||||
|
else
|
||||||
|
return $"/ac \"{action.GetName(DefaultJob)}\" <wait.{actionBase.MacroWaitTime}>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetEndCommand(int macroIdx, bool isEnd, MacroCopyConfiguration config)
|
||||||
|
{
|
||||||
|
if (config.UseNextMacro && !isEnd)
|
||||||
|
{
|
||||||
|
if (config.Type == MacroCopyConfiguration.CopyType.CopyToMacro && config.CopyDown)
|
||||||
|
return $"/nextmacro down";
|
||||||
|
else
|
||||||
|
return $"/nextmacro";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.AddNotification)
|
||||||
|
{
|
||||||
|
if (isEnd)
|
||||||
|
{
|
||||||
|
if (config.AddNotificationSound)
|
||||||
|
return $"/echo Craft complete! <se.{config.EndNotificationSound}>";
|
||||||
|
else
|
||||||
|
return $"/echo Craft complete!";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (config.AddNotificationSound)
|
||||||
|
return $"/echo Macro #{macroIdx + 1} complete! <se.{config.IntermediateNotificationSound}>";
|
||||||
|
else
|
||||||
|
return $"/echo Macro #{macroIdx + 1} complete!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CopyToMacro(List<string> macros, MacroCopyConfiguration config)
|
||||||
|
{
|
||||||
|
int i, macroIdx;
|
||||||
|
for (
|
||||||
|
i = 0, macroIdx = config.StartMacroIdx;
|
||||||
|
i < macros.Count && i < config.MaxMacroCount && macroIdx < 100;
|
||||||
|
i++, macroIdx += config.CopyDown ? 10 : 1)
|
||||||
|
SetMacro(macroIdx, config.SharedMacro, macros[i]);
|
||||||
|
|
||||||
|
Service.PluginInterface.UiBuilder.AddNotification(i > 1 ? "Copied macro to User Macros." : $"Copied {i} macros to User Macros.", "Craftimizer Macro Copied", NotificationType.Success);
|
||||||
|
if (i < macros.Count)
|
||||||
|
{
|
||||||
|
Service.Plugin.OpenMacroClipboard(macros);
|
||||||
|
var rest = macros.Count - i;
|
||||||
|
Service.PluginInterface.UiBuilder.AddNotification($"Couldn't copy {rest} macro{(rest == 1 ? "" : "s")}, so a window was opened with all of them.", "Craftimizer Macro Copied", NotificationType.Info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe void SetMacro(int idx, bool isShared, string macroText)
|
||||||
|
{
|
||||||
|
if (idx >= 100 || idx < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(idx), "Macro index must be between 0 and 99");
|
||||||
|
|
||||||
|
var module = RaptureMacroModule.Instance();
|
||||||
|
var macro = module->GetMacro(isShared ? 1u : 0u, (uint)idx);
|
||||||
|
var text = Utf8String.FromString(macroText.ReplaceLineEndings("\n"));
|
||||||
|
module->ReplaceMacroLines(macro, text);
|
||||||
|
text->Dtor();
|
||||||
|
IMemorySpace.Free(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CopyToClipboard(List<string> macros, MacroCopyConfiguration config)
|
||||||
|
{
|
||||||
|
ImGui.SetClipboardText(string.Join(Environment.NewLine + Environment.NewLine, macros));
|
||||||
|
Service.PluginInterface.UiBuilder.AddNotification(macros.Count > 1 ? "Copied macro to clipboard." : $"Copied {macros.Count} macros to clipboard.", "Craftimizer Macro Copied", NotificationType.Success);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
using Craftimizer.Plugin;
|
||||||
|
using Craftimizer.Simulator;
|
||||||
|
using Craftimizer.Simulator.Actions;
|
||||||
|
using Dalamud.Networking.Http;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
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;
|
||||||
|
|
||||||
|
namespace Craftimizer.Utils;
|
||||||
|
|
||||||
|
public static class MacroImport
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<ActionType>? TryParseMacro(string inputMacro)
|
||||||
|
{
|
||||||
|
var actions = new List<ActionType>();
|
||||||
|
foreach (var line in inputMacro.ReplaceLineEndings("\n").Split("\n"))
|
||||||
|
{
|
||||||
|
if (TryParseLine(line) is { } action)
|
||||||
|
actions.Add(action);
|
||||||
|
}
|
||||||
|
return actions.Count > 0 ? actions : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ActionType? TryParseLine(string line)
|
||||||
|
{
|
||||||
|
if (line.StartsWith("/ac", StringComparison.OrdinalIgnoreCase))
|
||||||
|
line = line[3..];
|
||||||
|
else if (line.StartsWith("/action", StringComparison.OrdinalIgnoreCase))
|
||||||
|
line = line[7..];
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
|
||||||
|
line = line.TrimStart();
|
||||||
|
|
||||||
|
// get first word
|
||||||
|
if (line.StartsWith('"'))
|
||||||
|
{
|
||||||
|
line = line[1..];
|
||||||
|
|
||||||
|
var end = line.IndexOf('"', 1);
|
||||||
|
if (end != -1)
|
||||||
|
line = line[..end];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var end = line.IndexOf(' ', 1);
|
||||||
|
if (end != -1)
|
||||||
|
line = line[..end];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var action in Enum.GetValues<ActionType>())
|
||||||
|
{
|
||||||
|
if (line.Equals(action.GetName(ClassJob.Carpenter), StringComparison.OrdinalIgnoreCase))
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParseUrl(string url, out Uri uri)
|
||||||
|
{
|
||||||
|
if (!Uri.TryCreate(url, UriKind.Absolute, out uri!))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!uri.IsDefaultPort)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return uri.DnsSafeHost is "ffxivteamcraft.com" or "craftingway.app";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task<RetrievedMacro> RetrieveUrl(string url, CancellationToken token)
|
||||||
|
{
|
||||||
|
if (!TryParseUrl(url, out var uri))
|
||||||
|
throw new ArgumentException("Unsupported url", nameof(url));
|
||||||
|
|
||||||
|
switch (uri.DnsSafeHost)
|
||||||
|
{
|
||||||
|
case "ffxivteamcraft.com":
|
||||||
|
return RetrieveTeamcraftUrl(uri, token);
|
||||||
|
case "craftingway.app":
|
||||||
|
return RetrieveCraftingwayUrl(uri, token);
|
||||||
|
default:
|
||||||
|
throw new UnreachableException("TryParseUrl should handle miscellaneous edge cases");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record TeamcraftMacro
|
||||||
|
{
|
||||||
|
public sealed record StringValue
|
||||||
|
{
|
||||||
|
[JsonPropertyName("stringValue")]
|
||||||
|
[JsonRequired]
|
||||||
|
public required string Value { get; set; }
|
||||||
|
|
||||||
|
public static implicit operator string(StringValue v) => v.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record IntegerValue
|
||||||
|
{
|
||||||
|
[JsonPropertyName("integerValue")]
|
||||||
|
[JsonRequired]
|
||||||
|
public required int Value { get; set; }
|
||||||
|
|
||||||
|
public static implicit operator int(IntegerValue v) => v.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record MapValue<T>
|
||||||
|
{
|
||||||
|
public sealed record ValueData
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
public required T Fields { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("mapValue")]
|
||||||
|
[JsonRequired]
|
||||||
|
public required ValueData Data { get; set; }
|
||||||
|
|
||||||
|
public T Value => Data.Fields;
|
||||||
|
public static implicit operator T(MapValue<T> v) => v.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record ArrayValue<T>
|
||||||
|
{
|
||||||
|
public sealed record ValueData
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
public required T[] Values { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonPropertyName("arrayValue")]
|
||||||
|
[JsonRequired]
|
||||||
|
public required ValueData Data { get; set; }
|
||||||
|
|
||||||
|
public T[] Value => Data.Values;
|
||||||
|
public static implicit operator T[](ArrayValue<T> v) => v.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record RecipeFieldData
|
||||||
|
{
|
||||||
|
[JsonRequired]
|
||||||
|
public required IntegerValue RLvl { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
public required IntegerValue Durability { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record FieldData
|
||||||
|
{
|
||||||
|
public StringValue? Name { get; set; }
|
||||||
|
[JsonRequired]
|
||||||
|
public required ArrayValue<StringValue> Rotation { get; set; }
|
||||||
|
public MapValue<RecipeFieldData>? Recipe { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record ErrorData
|
||||||
|
{
|
||||||
|
public required int Code { get; set; }
|
||||||
|
public required string Message { get; set; }
|
||||||
|
public required string Status { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public FieldData? Fields { get; set; }
|
||||||
|
|
||||||
|
public ErrorData? Error { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record CraftingwayMacro
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string? Slug { get; set; }
|
||||||
|
public string? Version { get; set; }
|
||||||
|
public string? Job { get; set; }
|
||||||
|
[JsonPropertyName("job_level")]
|
||||||
|
public int JobLevel { get; set; }
|
||||||
|
public int Craftsmanship { get; set; }
|
||||||
|
public int Control { get; set; }
|
||||||
|
public int CP { get; set; }
|
||||||
|
public string? Food { get; set; }
|
||||||
|
public string? Potion { get; set; }
|
||||||
|
[JsonPropertyName("recipe_job_level")]
|
||||||
|
public int RecipeJobLevel { get; set; }
|
||||||
|
public string? Recipe { get; set; }
|
||||||
|
// HqIngredients
|
||||||
|
public string? Actions { get; set; }
|
||||||
|
[JsonPropertyName("created_at")]
|
||||||
|
public long CreatedAt { get; set; }
|
||||||
|
public string? Error { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct RetrievedMacro(string Name, IReadOnlyList<ActionType> Actions);
|
||||||
|
|
||||||
|
private static async Task<RetrievedMacro> RetrieveTeamcraftUrl(Uri uri, CancellationToken token)
|
||||||
|
{
|
||||||
|
using var heCallback = new HappyEyeballsCallback();
|
||||||
|
using var client = new HttpClient(new SocketsHttpHandler
|
||||||
|
{
|
||||||
|
AutomaticDecompression = DecompressionMethods.All,
|
||||||
|
ConnectCallback = heCallback.ConnectCallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
var path = uri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped);
|
||||||
|
if (!path.StartsWith("simulator/", StringComparison.Ordinal))
|
||||||
|
throw new ArgumentException("Teamcraft macro url should start with /simulator", nameof(uri));
|
||||||
|
path = path[10..];
|
||||||
|
|
||||||
|
var lastSlash = path.LastIndexOf('/');
|
||||||
|
if (lastSlash == -1)
|
||||||
|
throw new ArgumentException("Teamcraft macro url is not in the right format", nameof(uri));
|
||||||
|
|
||||||
|
var id = path[(lastSlash + 1)..];
|
||||||
|
|
||||||
|
var resp = await client.GetFromJsonAsync<TeamcraftMacro>(
|
||||||
|
$"https://firestore.googleapis.com/v1beta1/projects/ffxivteamcraft/databases/(default)/documents/rotations/{id}",
|
||||||
|
token).
|
||||||
|
ConfigureAwait(false);
|
||||||
|
if (resp is null)
|
||||||
|
throw new Exception("Internal error; failed to retrieve macro");
|
||||||
|
if (resp.Error is { } error)
|
||||||
|
throw new Exception($"Internal server error ({error.Status}); {error.Message}");
|
||||||
|
if (resp.Fields is not { } rotation)
|
||||||
|
throw new Exception($"Internal error; No fields or error was returned");
|
||||||
|
// https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/67f453041c6b2b31d32fcf6e1fd53aa38ed7a12b/apps/client/src/app/model/other/crafting-rotation.ts#L49
|
||||||
|
var name = rotation.Name?.Value ??
|
||||||
|
(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>();
|
||||||
|
foreach (var action in rotation.Rotation.Value)
|
||||||
|
{
|
||||||
|
ActionType? actionType = action.Value switch
|
||||||
|
{
|
||||||
|
"BasicSynthesis" => ActionType.BasicSynthesis,
|
||||||
|
"CarefulSynthesis" => ActionType.CarefulSynthesis,
|
||||||
|
"PrudentSynthesis" => ActionType.PrudentSynthesis,
|
||||||
|
"RapidSynthesis" => ActionType.RapidSynthesis,
|
||||||
|
"Groundwork" => ActionType.Groundwork,
|
||||||
|
"FocusedSynthesis" => ActionType.FocusedSynthesis,
|
||||||
|
"MuscleMemory" => ActionType.MuscleMemory,
|
||||||
|
"IntensiveSynthesis" => ActionType.IntensiveSynthesis,
|
||||||
|
"BasicTouch" => ActionType.BasicTouch,
|
||||||
|
"StandardTouch" => ActionType.StandardTouch,
|
||||||
|
"AdvancedTouch" => ActionType.AdvancedTouch,
|
||||||
|
"HastyTouch" => ActionType.HastyTouch,
|
||||||
|
"ByregotsBlessing" => ActionType.ByregotsBlessing,
|
||||||
|
"PreciseTouch" => ActionType.PreciseTouch,
|
||||||
|
"FocusedTouch" => ActionType.FocusedTouch,
|
||||||
|
"PrudentTouch" => ActionType.PrudentTouch,
|
||||||
|
"TrainedEye" => ActionType.TrainedEye,
|
||||||
|
"PreparatoryTouch" => ActionType.PreparatoryTouch,
|
||||||
|
"Reflect" => ActionType.Reflect,
|
||||||
|
"TrainedFinesse" => ActionType.TrainedFinesse,
|
||||||
|
"TricksOfTheTrade" => ActionType.TricksOfTheTrade,
|
||||||
|
"MastersMend" => ActionType.MastersMend,
|
||||||
|
"Manipulation" => ActionType.Manipulation,
|
||||||
|
"WasteNot" => ActionType.WasteNot,
|
||||||
|
"WasteNotII" => ActionType.WasteNot2,
|
||||||
|
"GreatStrides" => ActionType.GreatStrides,
|
||||||
|
"Innovation" => ActionType.Innovation,
|
||||||
|
"Veneration" => ActionType.Veneration,
|
||||||
|
"FinalAppraisal" => ActionType.FinalAppraisal,
|
||||||
|
"Observe" => ActionType.Observe,
|
||||||
|
"HeartAndSoul" => ActionType.HeartAndSoul,
|
||||||
|
"CarefulObservation" => ActionType.CarefulObservation,
|
||||||
|
"DelicateSynthesis" => ActionType.DelicateSynthesis,
|
||||||
|
"RemoveFinalAppraisal" => throw new Exception("Removing Final Appraisal is an unsupported action"),
|
||||||
|
null => null,
|
||||||
|
{ } actionValue => throw new Exception($"Unknown action {actionValue}"),
|
||||||
|
};
|
||||||
|
if (actionType.HasValue)
|
||||||
|
actions.Add(actionType.Value);
|
||||||
|
}
|
||||||
|
return new(name, actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<RetrievedMacro> RetrieveCraftingwayUrl(Uri uri, CancellationToken token)
|
||||||
|
{
|
||||||
|
using var heCallback = new HappyEyeballsCallback();
|
||||||
|
using var client = new HttpClient(new SocketsHttpHandler
|
||||||
|
{
|
||||||
|
AutomaticDecompression = DecompressionMethods.All,
|
||||||
|
ConnectCallback = heCallback.ConnectCallback,
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://craftingway.app/rotation/variable-blueprint-KmrvS
|
||||||
|
|
||||||
|
var path = uri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped);
|
||||||
|
if (!path.StartsWith("rotation/", StringComparison.Ordinal))
|
||||||
|
throw new ArgumentException("Craftingway macro url should start with /rotation", nameof(uri));
|
||||||
|
path = path[9..];
|
||||||
|
|
||||||
|
var lastSlash = path.LastIndexOf('/');
|
||||||
|
if (lastSlash != -1)
|
||||||
|
throw new ArgumentException("Craftingway macro url is not in the right format", nameof(uri));
|
||||||
|
|
||||||
|
var id = path;
|
||||||
|
|
||||||
|
var resp = await client.GetFromJsonAsync<CraftingwayMacro>(
|
||||||
|
$"https://servingway.fly.dev/rotation/{id}",
|
||||||
|
token)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
if (resp is null)
|
||||||
|
throw new Exception("Internal error; failed to retrieve macro");
|
||||||
|
if (resp.Error is { } error)
|
||||||
|
throw new Exception($"Internal server error; {error}");
|
||||||
|
if (resp.Actions is not { } rotation)
|
||||||
|
throw new Exception($"Internal error; No actions or error was returned");
|
||||||
|
// https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/67f453041c6b2b31d32fcf6e1fd53aa38ed7a12b/apps/client/src/app/model/other/crafting-rotation.ts#L49
|
||||||
|
var name = resp.Slug ?? "New Craftinway Rotation";
|
||||||
|
var actions = new List<ActionType>();
|
||||||
|
foreach (var action in resp.Actions.Split(','))
|
||||||
|
{
|
||||||
|
ActionType? actionType = action switch
|
||||||
|
{
|
||||||
|
"BasicSynthesis" => ActionType.BasicSynthesis,
|
||||||
|
"BasicTouch" => ActionType.BasicTouch,
|
||||||
|
"MastersMend" => ActionType.MastersMend,
|
||||||
|
"Observe" => ActionType.Observe,
|
||||||
|
"WasteNot" => ActionType.WasteNot,
|
||||||
|
"Veneration" => ActionType.Veneration,
|
||||||
|
"StandardTouch" => ActionType.StandardTouch,
|
||||||
|
"GreatStrides" => ActionType.GreatStrides,
|
||||||
|
"Innovation" => ActionType.Innovation,
|
||||||
|
"BasicSynthesisTraited" => ActionType.BasicSynthesis,
|
||||||
|
"WasteNotII" => ActionType.WasteNot2,
|
||||||
|
"ByregotsBlessing" => ActionType.ByregotsBlessing,
|
||||||
|
"MuscleMemory" => ActionType.MuscleMemory,
|
||||||
|
"CarefulSynthesis" => ActionType.CarefulSynthesis,
|
||||||
|
"Manipulation" => ActionType.Manipulation,
|
||||||
|
"PrudentTouch" => ActionType.PrudentTouch,
|
||||||
|
"FocusedSynthesis" => ActionType.FocusedSynthesis,
|
||||||
|
"FocusedTouch" => ActionType.FocusedTouch,
|
||||||
|
"Reflect" => ActionType.Reflect,
|
||||||
|
"PreparatoryTouch" => ActionType.PreparatoryTouch,
|
||||||
|
"Groundwork" => ActionType.Groundwork,
|
||||||
|
"DelicateSynthesis" => ActionType.DelicateSynthesis,
|
||||||
|
"TrainedEye" => ActionType.TrainedEye,
|
||||||
|
"CarefulSynthesisTraited" => ActionType.CarefulSynthesis,
|
||||||
|
"AdvancedTouch" => ActionType.AdvancedTouch,
|
||||||
|
"GroundworkTraited" => ActionType.Groundwork,
|
||||||
|
"PrudentSynthesis" => ActionType.PrudentSynthesis,
|
||||||
|
"TrainedFinesse" => ActionType.TrainedFinesse,
|
||||||
|
{ } actionValue => throw new Exception($"Unknown action {actionValue}"),
|
||||||
|
};
|
||||||
|
if (actionType.HasValue)
|
||||||
|
actions.Add(actionType.Value);
|
||||||
|
}
|
||||||
|
return new(name, actions);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using Craftimizer.Plugin;
|
||||||
|
using Craftimizer.Simulator;
|
||||||
|
using Lumina.Excel.GeneratedSheets;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using ClassJob = Craftimizer.Simulator.ClassJob;
|
||||||
|
|
||||||
|
namespace Craftimizer.Utils;
|
||||||
|
|
||||||
|
public sealed record RecipeData
|
||||||
|
{
|
||||||
|
public ushort RecipeId { get; }
|
||||||
|
|
||||||
|
public Recipe Recipe { get; }
|
||||||
|
public RecipeLevelTable Table { get; }
|
||||||
|
|
||||||
|
public ClassJob ClassJob { get; }
|
||||||
|
public RecipeInfo RecipeInfo { get; }
|
||||||
|
public IReadOnlyList<(Item Item, int Amount)> Ingredients { get; }
|
||||||
|
public int MaxStartingQuality { get; }
|
||||||
|
private int TotalHqILvls { get; }
|
||||||
|
|
||||||
|
public RecipeData(ushort recipeId)
|
||||||
|
{
|
||||||
|
RecipeId = recipeId;
|
||||||
|
|
||||||
|
Recipe = LuminaSheets.RecipeSheet.GetRow(recipeId) ??
|
||||||
|
throw new ArgumentException($"Invalid recipe id {recipeId}", nameof(recipeId));
|
||||||
|
|
||||||
|
Table = Recipe.RecipeLevelTable.Value!;
|
||||||
|
ClassJob = (ClassJob)Recipe.CraftType.Row;
|
||||||
|
RecipeInfo = new()
|
||||||
|
{
|
||||||
|
IsExpert = Recipe.IsExpert,
|
||||||
|
ClassJobLevel = Table.ClassJobLevel,
|
||||||
|
RLvl = (int)Table.RowId,
|
||||||
|
ConditionsFlag = Table.ConditionsFlag,
|
||||||
|
MaxDurability = Table.Durability * Recipe.DurabilityFactor / 100,
|
||||||
|
MaxQuality = (Recipe.CanHq || Recipe.IsExpert) ? (int)Table.Quality * Recipe.QualityFactor / 100 : 0,
|
||||||
|
MaxProgress = Table.Difficulty * Recipe.DifficultyFactor / 100,
|
||||||
|
QualityModifier = Table.QualityModifier,
|
||||||
|
QualityDivider = Table.QualityDivider,
|
||||||
|
ProgressModifier = Table.ProgressModifier,
|
||||||
|
ProgressDivider = Table.ProgressDivider,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ingredients = Recipe.UnkData5.Take(6)
|
||||||
|
.Where(i => i != null && i.ItemIngredient != 0)
|
||||||
|
.Select(i => (LuminaSheets.ItemSheet.GetRow((uint)i.ItemIngredient)!, (int)i.AmountIngredient))
|
||||||
|
.Where(i => i.Item1 != null).ToList();
|
||||||
|
MaxStartingQuality = (int)Math.Floor(Recipe.MaterialQualityFactor * RecipeInfo.MaxQuality / 100f);
|
||||||
|
|
||||||
|
TotalHqILvls = (int)Ingredients.Where(i => i.Item.CanBeHq).Sum(i => i.Item.LevelItem.Row * i.Amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CalculateItemStartingQuality(int itemIdx, int amount)
|
||||||
|
{
|
||||||
|
if (itemIdx >= Ingredients.Count)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(itemIdx));
|
||||||
|
|
||||||
|
var ingredient = Ingredients[itemIdx];
|
||||||
|
if (amount > ingredient.Amount)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(amount));
|
||||||
|
|
||||||
|
if (!ingredient.Item.CanBeHq)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var iLvls = ingredient.Item.LevelItem.Row * amount;
|
||||||
|
return (int)Math.Floor((float)iLvls / TotalHqILvls * MaxStartingQuality);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CalculateStartingQuality(IEnumerable<int> hqQuantities)
|
||||||
|
{
|
||||||
|
if (TotalHqILvls == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var iLvls = Ingredients.Zip(hqQuantities).Sum(i => i.First.Item.LevelItem.Row * i.Second);
|
||||||
|
|
||||||
|
return (int)Math.Floor((float)iLvls / TotalHqILvls * MaxStartingQuality);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
using Craftimizer.Plugin;
|
|
||||||
using Craftimizer.Simulator;
|
|
||||||
using Dalamud.Game;
|
|
||||||
using Dalamud.Logging;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
|
||||||
using Lumina.Excel.GeneratedSheets;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using ActionType = Craftimizer.Simulator.Actions.ActionType;
|
|
||||||
using ClassJob = Craftimizer.Simulator.ClassJob;
|
|
||||||
using CSRecipeNote = FFXIVClientStructs.FFXIV.Client.Game.UI.RecipeNote;
|
|
||||||
|
|
||||||
namespace Craftimizer.Utils;
|
|
||||||
|
|
||||||
public sealed unsafe class RecipeNote : IDisposable
|
|
||||||
{
|
|
||||||
public AddonRecipeNote* AddonRecipe { get; private set; }
|
|
||||||
public AddonSynthesis* AddonSynthesis { get; private set; }
|
|
||||||
public bool IsCrafting { get; private set; }
|
|
||||||
public ushort RecipeId { get; private set; }
|
|
||||||
public Recipe Recipe { get; private set; } = null!;
|
|
||||||
public bool HasValidRecipe { get; private set; }
|
|
||||||
|
|
||||||
public RecipeLevelTable Table { get; private set; } = null!;
|
|
||||||
public RecipeInfo Info { get; private set; } = null!;
|
|
||||||
public ClassJob ClassJob { get; private set; }
|
|
||||||
public short CharacterLevel { get; private set; }
|
|
||||||
public bool CanUseManipulation { get; private set; }
|
|
||||||
public int HQIngredientCount { get; private set; }
|
|
||||||
public int MaxStartingQuality { get; private set; }
|
|
||||||
|
|
||||||
public RecipeNote()
|
|
||||||
{
|
|
||||||
Service.Framework.Update += FrameworkUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FrameworkUpdate(Framework f)
|
|
||||||
{
|
|
||||||
HasValidRecipe = false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
HasValidRecipe = Update();
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
PluginLog.LogError(e, "RecipeNote framework update failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Update()
|
|
||||||
{
|
|
||||||
if (Service.ClientState.LocalPlayer == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
AddonRecipe = (AddonRecipeNote*)Service.GameGui.GetAddonByName("RecipeNote");
|
|
||||||
AddonSynthesis = (AddonSynthesis*)Service.GameGui.GetAddonByName("Synthesis");
|
|
||||||
|
|
||||||
var recipeId = GetRecipeIdFromList();
|
|
||||||
if (recipeId == null)
|
|
||||||
{
|
|
||||||
recipeId = GetRecipeIdFromAgent();
|
|
||||||
if (recipeId == null)
|
|
||||||
return false;
|
|
||||||
else
|
|
||||||
IsCrafting = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
IsCrafting = false;
|
|
||||||
|
|
||||||
var isNewRecipe = RecipeId != recipeId.Value;
|
|
||||||
|
|
||||||
RecipeId = recipeId.Value;
|
|
||||||
|
|
||||||
var recipe = LuminaSheets.RecipeSheet.GetRow(RecipeId);
|
|
||||||
|
|
||||||
if (recipe == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
Recipe = recipe;
|
|
||||||
|
|
||||||
if (isNewRecipe)
|
|
||||||
CalculateStats();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ushort? GetRecipeIdFromList()
|
|
||||||
{
|
|
||||||
var instance = CSRecipeNote.Instance();
|
|
||||||
|
|
||||||
var list = instance->RecipeList;
|
|
||||||
|
|
||||||
if (list == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var recipeEntry = list->SelectedRecipe;
|
|
||||||
|
|
||||||
if (recipeEntry == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return recipeEntry->RecipeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ushort? GetRecipeIdFromAgent()
|
|
||||||
{
|
|
||||||
var instance = AgentRecipeNote.Instance();
|
|
||||||
|
|
||||||
var recipeId = instance->ActiveCraftRecipeId;
|
|
||||||
|
|
||||||
if (recipeId == 0)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return (ushort)recipeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CalculateStats()
|
|
||||||
{
|
|
||||||
Table = Recipe.RecipeLevelTable.Value!;
|
|
||||||
Info = CreateInfo();
|
|
||||||
ClassJob = (ClassJob)Recipe.CraftType.Row;
|
|
||||||
CharacterLevel = PlayerState.Instance()->ClassJobLevelArray[ClassJob.GetClassJobIndex()];
|
|
||||||
CanUseManipulation = ActionManager.CanUseActionOnTarget(ActionType.Manipulation.GetId(ClassJob), (GameObject*)Service.ClientState.LocalPlayer!.Address);
|
|
||||||
HQIngredientCount = Recipe.UnkData5
|
|
||||||
.Where(i =>
|
|
||||||
i != null &&
|
|
||||||
i.ItemIngredient != 0 &&
|
|
||||||
(LuminaSheets.ItemSheet.GetRow((uint)i.ItemIngredient)?.CanBeHq ?? false)
|
|
||||||
).Sum(i => i.AmountIngredient);
|
|
||||||
MaxStartingQuality = (int)Math.Floor(Recipe.MaterialQualityFactor * Info.MaxQuality / 100f);
|
|
||||||
}
|
|
||||||
|
|
||||||
private RecipeInfo CreateInfo() =>
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
IsExpert = Recipe.IsExpert,
|
|
||||||
ClassJobLevel = Table.ClassJobLevel,
|
|
||||||
RLvl = (int)Table.RowId,
|
|
||||||
ConditionsFlag = Table.ConditionsFlag,
|
|
||||||
MaxDurability = Table.Durability * Recipe.DurabilityFactor / 100,
|
|
||||||
MaxQuality = (int)Table.Quality * Recipe.QualityFactor / 100,
|
|
||||||
MaxProgress = Table.Difficulty * Recipe.DifficultyFactor / 100,
|
|
||||||
QualityModifier = Table.QualityModifier,
|
|
||||||
QualityDivider = Table.QualityDivider,
|
|
||||||
ProgressModifier = Table.ProgressModifier,
|
|
||||||
ProgressDivider = Table.ProgressDivider,
|
|
||||||
};
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Service.Framework.Update -= FrameworkUpdate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using Dalamud.Game.Text;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace Craftimizer.Utils;
|
||||||
|
|
||||||
|
public static class SqText
|
||||||
|
{
|
||||||
|
public static SeIconChar LevelPrefix => SeIconChar.LevelEn;
|
||||||
|
|
||||||
|
public static readonly ReadOnlyDictionary<char, SeIconChar> LevelNumReplacements = new(new Dictionary<char, SeIconChar>
|
||||||
|
{
|
||||||
|
['0'] = SeIconChar.Number0,
|
||||||
|
['1'] = SeIconChar.Number1,
|
||||||
|
['2'] = SeIconChar.Number2,
|
||||||
|
['3'] = SeIconChar.Number3,
|
||||||
|
['4'] = SeIconChar.Number4,
|
||||||
|
['5'] = SeIconChar.Number5,
|
||||||
|
['6'] = SeIconChar.Number6,
|
||||||
|
['7'] = SeIconChar.Number7,
|
||||||
|
['8'] = SeIconChar.Number8,
|
||||||
|
['9'] = SeIconChar.Number9,
|
||||||
|
});
|
||||||
|
|
||||||
|
public static string ToLevelString<T>(T value) where T : IBinaryInteger<T>
|
||||||
|
{
|
||||||
|
var str = value.ToString() ?? throw new FormatException("Failed to format value");
|
||||||
|
foreach(var (k, v) in LevelNumReplacements)
|
||||||
|
str = str.Replace(k, v.ToIconChar());
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParseLevelString(string str, out int result)
|
||||||
|
{
|
||||||
|
foreach(var (k, v) in LevelNumReplacements)
|
||||||
|
str = str.Replace(v.ToIconChar(), k);
|
||||||
|
return int.TryParse(str, out result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
using Craftimizer.Plugin.Utils;
|
|
||||||
using Craftimizer.Utils;
|
|
||||||
using Dalamud.Interface;
|
|
||||||
using Dalamud.Interface.Components;
|
|
||||||
using Dalamud.Interface.Windowing;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
||||||
using ImGuiNET;
|
|
||||||
using System;
|
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace Craftimizer.Plugin.Windows;
|
|
||||||
|
|
||||||
public sealed unsafe partial class Craft : Window, IDisposable
|
|
||||||
{
|
|
||||||
private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration
|
|
||||||
| ImGuiWindowFlags.AlwaysAutoResize
|
|
||||||
| ImGuiWindowFlags.NoSavedSettings
|
|
||||||
| ImGuiWindowFlags.NoFocusOnAppearing
|
|
||||||
| ImGuiWindowFlags.NoNavFocus;
|
|
||||||
|
|
||||||
private const float WindowWidth = 300;
|
|
||||||
private const int ActionsPerRow = 5;
|
|
||||||
private static readonly Vector2 CraftProgressBarSize = new(WindowWidth, 15);
|
|
||||||
|
|
||||||
private static Configuration Config => Service.Configuration;
|
|
||||||
|
|
||||||
private static Random Random { get; } = new();
|
|
||||||
private static RecipeNote RecipeUtils => Service.Plugin.RecipeNote;
|
|
||||||
|
|
||||||
private bool WasOpen { get; set; }
|
|
||||||
|
|
||||||
public Craft() : base("Craftimizer SynthesisHelper", WindowFlags, true)
|
|
||||||
{
|
|
||||||
Service.WindowSystem.AddWindow(this);
|
|
||||||
Service.Plugin.Hooks.OnActionUsed += OnActionUsed;
|
|
||||||
|
|
||||||
IsOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Draw()
|
|
||||||
{
|
|
||||||
SolveTick();
|
|
||||||
DequeueSolver();
|
|
||||||
|
|
||||||
DrawActions();
|
|
||||||
|
|
||||||
ImGui.SameLine(0, 0);
|
|
||||||
ImGui.Dummy(default);
|
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
|
||||||
|
|
||||||
Simulator.DrawAllProgressBars(SolverLatestState, CraftProgressBarSize);
|
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
|
||||||
|
|
||||||
ImGui.PushFont(UiBuilder.IconFont);
|
|
||||||
var cogWidth = ImGui.CalcTextSize(FontAwesomeIcon.Cog.ToIconString()).X + (ImGui.GetStyle().FramePadding.X * 2);
|
|
||||||
ImGui.PopFont();
|
|
||||||
|
|
||||||
DrawSolveButton(new(WindowWidth - ImGui.GetStyle().ItemSpacing.X - cogWidth, ImGuiUtils.ButtonHeight));
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (ImGuiComponents.IconButton("synthSettingsButton", FontAwesomeIcon.Cog))
|
|
||||||
Service.Plugin.OpenSettingsTab(Settings.TabSynthHelper);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawActions()
|
|
||||||
{
|
|
||||||
var actionSize = new Vector2((WindowWidth / ActionsPerRow) - (ImGui.GetStyle().ItemSpacing.X * ((ActionsPerRow - 1f) / ActionsPerRow)));
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero);
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero);
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero);
|
|
||||||
|
|
||||||
ImGui.Dummy(new(0, actionSize.Y));
|
|
||||||
ImGui.SameLine(0, 0);
|
|
||||||
for (var i = 0; i < SolverActions.Count; ++i)
|
|
||||||
{
|
|
||||||
var (action, tooltip, state) = SolverActions[i];
|
|
||||||
ImGui.PushID(i);
|
|
||||||
if (ImGui.ImageButton(action.GetIcon(RecipeUtils.ClassJob).ImGuiHandle, actionSize, Vector2.Zero, Vector2.One, 0))
|
|
||||||
{
|
|
||||||
if (i == 0)
|
|
||||||
Chat.SendMessage($"/ac \"{action.GetName(RecipeUtils.ClassJob)}\"");
|
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
ImGui.BeginTooltip();
|
|
||||||
ImGui.Text($"{action.GetName(RecipeUtils.ClassJob)}\n{tooltip}");
|
|
||||||
Simulator.DrawAllProgressTooltips(state);
|
|
||||||
if (i == 0)
|
|
||||||
ImGui.Text("Click to Execute");
|
|
||||||
ImGui.EndTooltip();
|
|
||||||
}
|
|
||||||
ImGui.PopID();
|
|
||||||
if (i % ActionsPerRow != (ActionsPerRow - 1))
|
|
||||||
ImGui.SameLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.PopStyleColor(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSolveButton(Vector2 buttonSize)
|
|
||||||
{
|
|
||||||
string buttonText;
|
|
||||||
string tooltipText;
|
|
||||||
bool isEnabled;
|
|
||||||
var taskCompleted = SolverTask?.IsCompleted ?? true;
|
|
||||||
var taskCancelled = SolverTaskToken?.IsCancellationRequested ?? false;
|
|
||||||
if (!taskCompleted)
|
|
||||||
{
|
|
||||||
if (taskCancelled)
|
|
||||||
{
|
|
||||||
buttonText = "Cancelling...";
|
|
||||||
tooltipText = "Cancelling action generation. This shouldn't take long.";
|
|
||||||
isEnabled = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
buttonText = "Cancel";
|
|
||||||
tooltipText = "Cancel action generation";
|
|
||||||
isEnabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
buttonText = "Retry";
|
|
||||||
tooltipText = "Retry and regenerate a new set of recommended actions to finish the craft.";
|
|
||||||
isEnabled = true;
|
|
||||||
}
|
|
||||||
ImGui.BeginDisabled(!isEnabled);
|
|
||||||
if (ImGui.Button(buttonText, buttonSize))
|
|
||||||
{
|
|
||||||
if (!taskCompleted)
|
|
||||||
{
|
|
||||||
if (!taskCancelled)
|
|
||||||
SolverTaskToken?.Cancel();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
QueueSolve(GetNextState()!.Value);
|
|
||||||
}
|
|
||||||
ImGui.EndDisabled();
|
|
||||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
|
||||||
ImGui.SetTooltip(tooltipText);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void PreDraw()
|
|
||||||
{
|
|
||||||
var addon = RecipeUtils.AddonSynthesis;
|
|
||||||
ref var unit = ref addon->AtkUnitBase;
|
|
||||||
var scale = unit.Scale;
|
|
||||||
var pos = new Vector2(unit.X, unit.Y);
|
|
||||||
var size = new Vector2(unit.WindowNode->AtkResNode.Width, unit.WindowNode->AtkResNode.Height) * scale;
|
|
||||||
|
|
||||||
var node = unit.GetNodeById(79);
|
|
||||||
|
|
||||||
Position = pos + new Vector2(size.X, node->Y * scale);
|
|
||||||
SizeConstraints = new WindowSizeConstraints
|
|
||||||
{
|
|
||||||
MinimumSize = new(-1),
|
|
||||||
MaximumSize = new(10000, 10000)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Input == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
base.PreDraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool DrawConditionsInner()
|
|
||||||
{
|
|
||||||
if (!RecipeUtils.HasValidRecipe)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!RecipeUtils.IsCrafting)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (RecipeUtils.AddonSynthesis == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Check if Synthesis addon is visible
|
|
||||||
if (RecipeUtils.AddonSynthesis->AtkUnitBase.WindowNode == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (RecipeUtils.AddonSynthesis->AtkUnitBase.GetNodeById(79) == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return base.DrawConditions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool DrawConditions()
|
|
||||||
{
|
|
||||||
if (!Config.EnableSynthHelper)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var ret = DrawConditionsInner();
|
|
||||||
if (ret && !WasOpen)
|
|
||||||
ResetSimulation();
|
|
||||||
|
|
||||||
WasOpen = ret;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ResetSimulation()
|
|
||||||
{
|
|
||||||
var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems);
|
|
||||||
if (container == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
CharacterStats = Gearsets.CalculateCharacterStats(Gearsets.CalculateGearsetCurrentStats(), Gearsets.GetGearsetItems(container), RecipeUtils.CharacterLevel, RecipeUtils.CanUseManipulation);
|
|
||||||
Input = new(CharacterStats, RecipeUtils.Info, 0, Random);
|
|
||||||
ActionCount = 0;
|
|
||||||
ActionStates = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
StopSolve();
|
|
||||||
SolverTask?.Wait();
|
|
||||||
SolverTask?.Dispose();
|
|
||||||
SolverTaskToken?.Dispose();
|
|
||||||
|
|
||||||
Service.Plugin.Hooks.OnActionUsed -= OnActionUsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
using Craftimizer.Simulator;
|
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
|
||||||
using Dalamud.Interface.Windowing;
|
|
||||||
using Dalamud.Memory;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
||||||
using System;
|
|
||||||
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
|
|
||||||
|
|
||||||
namespace Craftimizer.Plugin.Windows;
|
|
||||||
|
|
||||||
public sealed unsafe partial class Craft : Window, IDisposable
|
|
||||||
{
|
|
||||||
// State variables, manually kept track of outside of the addon
|
|
||||||
private CharacterStats CharacterStats = null!;
|
|
||||||
private SimulationInput Input = null!;
|
|
||||||
private int ActionCount;
|
|
||||||
private ActionStates ActionStates;
|
|
||||||
|
|
||||||
private sealed class AddonValues
|
|
||||||
{
|
|
||||||
public AddonSynthesis* Addon { get; }
|
|
||||||
public AtkValue* Values => Addon->AtkUnitBase.AtkValues;
|
|
||||||
public ushort ValueCount => Addon->AtkUnitBase.AtkValuesCount;
|
|
||||||
|
|
||||||
public AddonValues(AddonSynthesis* addon)
|
|
||||||
{
|
|
||||||
Addon = addon;
|
|
||||||
if (ValueCount != 26)
|
|
||||||
throw new ArgumentException("AddonSynthesis must have 26 AtkValues", nameof(addon));
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe AtkValue* this[int i] => Values + i;
|
|
||||||
|
|
||||||
// Always 0?
|
|
||||||
private uint Unk0 => GetUInt(0);
|
|
||||||
// Always true?
|
|
||||||
private bool Unk1 => GetBool(1);
|
|
||||||
|
|
||||||
public SeString ItemName => GetString(2);
|
|
||||||
public uint ItemIconId => GetUInt(3);
|
|
||||||
public uint ItemCount => GetUInt(4);
|
|
||||||
public uint Progress => GetUInt(5);
|
|
||||||
public uint MaxProgress => GetUInt(6);
|
|
||||||
public uint Durability => GetUInt(7);
|
|
||||||
public uint MaxDurability => GetUInt(8);
|
|
||||||
public uint Quality => GetUInt(9);
|
|
||||||
public uint HQChance => GetUInt(10);
|
|
||||||
private uint IsShowingCollectibleInfoValue => GetUInt(11);
|
|
||||||
private uint ConditionValue => GetUInt(12);
|
|
||||||
public SeString ConditionName => GetString(13);
|
|
||||||
public SeString ConditionNameAndTooltip => GetString(14);
|
|
||||||
public uint StepCount => GetUInt(15);
|
|
||||||
public uint ResultItemId => GetUInt(16);
|
|
||||||
public uint MaxQuality => GetUInt(17);
|
|
||||||
public uint RequiredQuality => GetUInt(18);
|
|
||||||
private uint IsCollectibleValue => GetUInt(19);
|
|
||||||
public uint Collectability => GetUInt(20);
|
|
||||||
public uint MaxCollectability => GetUInt(21);
|
|
||||||
public uint CollectabilityCheckpoint1 => GetUInt(22);
|
|
||||||
public uint CollectabilityCheckpoint2 => GetUInt(23);
|
|
||||||
public uint CollectabilityCheckpoint3 => GetUInt(24);
|
|
||||||
public bool IsExpertRecipe => GetBool(25);
|
|
||||||
|
|
||||||
public bool IsShowingCollectibleInfo => IsShowingCollectibleInfoValue != 0;
|
|
||||||
public Condition Condition => (Condition)(1 << (int)ConditionValue);
|
|
||||||
public bool IsCollectible => IsCollectibleValue != 0;
|
|
||||||
|
|
||||||
private uint GetUInt(int i)
|
|
||||||
{
|
|
||||||
var value = this[i];
|
|
||||||
return value->Type == ValueType.UInt ?
|
|
||||||
value->UInt :
|
|
||||||
throw new ArgumentException($"Value {i} is not a uint", nameof(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool GetBool(int i)
|
|
||||||
{
|
|
||||||
var value = this[i];
|
|
||||||
return value->Type == ValueType.Bool ?
|
|
||||||
value->Byte != 0 :
|
|
||||||
throw new ArgumentException($"Value {i} is not a boolean", nameof(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
private SeString GetString(int i)
|
|
||||||
{
|
|
||||||
var value = this[i];
|
|
||||||
return value->Type switch
|
|
||||||
{
|
|
||||||
ValueType.AllocatedString or
|
|
||||||
ValueType.String =>
|
|
||||||
MemoryHelper.ReadSeStringNullTerminated((nint)value->String),
|
|
||||||
_ => throw new ArgumentException($"Value {i} is not a string", nameof(i))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private const ushort StatusInnerQuiet = 251;
|
|
||||||
private const ushort StatusWasteNot = 252;
|
|
||||||
private const ushort StatusVeneration = 2226;
|
|
||||||
private const ushort StatusGreatStrides = 254;
|
|
||||||
private const ushort StatusInnovation = 2189;
|
|
||||||
private const ushort StatusFinalAppraisal = 2190;
|
|
||||||
private const ushort StatusWasteNot2 = 257;
|
|
||||||
private const ushort StatusMuscleMemory = 2191;
|
|
||||||
private const ushort StatusManipulation = 1164;
|
|
||||||
private const ushort StatusHeartAndSoul = 2665;
|
|
||||||
|
|
||||||
private SimulationState GetAddonSimulationState()
|
|
||||||
{
|
|
||||||
var player = Service.ClientState.LocalPlayer!;
|
|
||||||
var values = new AddonValues(RecipeUtils.AddonSynthesis);
|
|
||||||
var statusManager = ((Character*)player.Address)->GetStatusManager();
|
|
||||||
|
|
||||||
byte GetEffectStack(ushort id)
|
|
||||||
{
|
|
||||||
foreach (var status in statusManager->StatusSpan)
|
|
||||||
if (status.StatusID == id)
|
|
||||||
return status.StackCount;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
bool HasEffect(ushort id)
|
|
||||||
{
|
|
||||||
foreach (var status in statusManager->StatusSpan)
|
|
||||||
if (status.StatusID == id)
|
|
||||||
return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new(Input)
|
|
||||||
{
|
|
||||||
ActionCount = ActionCount,
|
|
||||||
StepCount = (int)values.StepCount - 1,
|
|
||||||
Progress = (int)values.Progress,
|
|
||||||
Quality = (int)values.Quality,
|
|
||||||
Durability = (int)values.Durability,
|
|
||||||
CP = (int)player.CurrentCp,
|
|
||||||
Condition = values.Condition,
|
|
||||||
ActiveEffects = new()
|
|
||||||
{
|
|
||||||
InnerQuiet = GetEffectStack(StatusInnerQuiet),
|
|
||||||
WasteNot = GetEffectStack(StatusWasteNot),
|
|
||||||
Veneration = GetEffectStack(StatusVeneration),
|
|
||||||
GreatStrides = GetEffectStack(StatusGreatStrides),
|
|
||||||
Innovation = GetEffectStack(StatusInnovation),
|
|
||||||
FinalAppraisal = GetEffectStack(StatusFinalAppraisal),
|
|
||||||
WasteNot2 = GetEffectStack(StatusWasteNot2),
|
|
||||||
MuscleMemory = GetEffectStack(StatusMuscleMemory),
|
|
||||||
Manipulation = GetEffectStack(StatusManipulation),
|
|
||||||
HeartAndSoul = HasEffect(StatusHeartAndSoul),
|
|
||||||
},
|
|
||||||
ActionStates = ActionStates
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
using Craftimizer.Simulator;
|
|
||||||
using Craftimizer.Simulator.Actions;
|
|
||||||
using Dalamud.Interface.Windowing;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Craftimizer.Plugin.Windows;
|
|
||||||
|
|
||||||
public sealed unsafe partial class Craft : Window, IDisposable
|
|
||||||
{
|
|
||||||
private SimulationState? SolverState { get; set; }
|
|
||||||
private Task? SolverTask { get; set; }
|
|
||||||
private CancellationTokenSource? SolverTaskToken { get; set; }
|
|
||||||
private ConcurrentQueue<ActionType> SolverActionQueue { get; } = new();
|
|
||||||
|
|
||||||
// State is the state of the simulation *after* its corresponding action is executed.
|
|
||||||
private List<(ActionType Action, string Tooltip, SimulationState State)> SolverActions { get; } = new();
|
|
||||||
private SimulatorNoRandom SolverSim { get; set; } = null!;
|
|
||||||
private SimulationState SolverLatestState => SolverActions.Count == 0 ? SolverState!.Value : SolverActions[^1].State;
|
|
||||||
|
|
||||||
private void StopSolve()
|
|
||||||
{
|
|
||||||
if (SolverTask == null || SolverTaskToken == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!SolverTask.IsCompleted)
|
|
||||||
SolverTaskToken.Cancel();
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SolverTaskToken.Dispose();
|
|
||||||
SolverTask.Dispose();
|
|
||||||
|
|
||||||
SolverTask = null;
|
|
||||||
SolverTaskToken = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void QueueSolve(SimulationState state)
|
|
||||||
{
|
|
||||||
StopSolve();
|
|
||||||
|
|
||||||
SolverActionQueue.Clear();
|
|
||||||
SolverActions.Clear();
|
|
||||||
SolverState = state;
|
|
||||||
SolverSim = new(state);
|
|
||||||
|
|
||||||
SolverTaskToken = new();
|
|
||||||
SolverTask = Task.Run(() => Config.SynthHelperSolverConfig.Invoke(state, SolverActionQueue.Enqueue, SolverTaskToken.Token));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SolveTick()
|
|
||||||
{
|
|
||||||
var newState = GetNextState();
|
|
||||||
if (SolverState == newState)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (newState == null)
|
|
||||||
StopSolve();
|
|
||||||
else
|
|
||||||
QueueSolve(newState.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DequeueSolver()
|
|
||||||
{
|
|
||||||
while (SolverActionQueue.TryDequeue(out var poppedAction))
|
|
||||||
AppendSolverAction(poppedAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AppendSolverAction(ActionType action)
|
|
||||||
{
|
|
||||||
var actionBase = action.Base();
|
|
||||||
if (actionBase is BaseComboAction comboActionBase)
|
|
||||||
{
|
|
||||||
AppendSolverAction(comboActionBase.ActionTypeA);
|
|
||||||
AppendSolverAction(comboActionBase.ActionTypeB);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (SolverActions.Count >= Config.SynthHelperStepCount)
|
|
||||||
{
|
|
||||||
StopSolve();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tooltip = actionBase.GetTooltip(SolverSim, false);
|
|
||||||
var (_, state) = SolverSim.Execute(SolverLatestState, action);
|
|
||||||
SolverActions.Add((action, tooltip, state));
|
|
||||||
|
|
||||||
if (SolverActions.Count >= Config.SynthHelperStepCount)
|
|
||||||
StopSolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
using Craftimizer.Simulator;
|
|
||||||
using Craftimizer.Simulator.Actions;
|
|
||||||
using Dalamud.Interface.Windowing;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace Craftimizer.Plugin.Windows;
|
|
||||||
|
|
||||||
public sealed unsafe partial class Craft : Window, IDisposable
|
|
||||||
{
|
|
||||||
private ConcurrentQueue<ActionType> UsedActionQueue { get; set; } = new();
|
|
||||||
private IEnumerator<SimulationState>? StateTicker { get; set; }
|
|
||||||
|
|
||||||
private SimulationState? GetNextState()
|
|
||||||
{
|
|
||||||
if (RecipeUtils.IsCrafting && StateTicker == null)
|
|
||||||
StateTicker = TickState();
|
|
||||||
if (!RecipeUtils.IsCrafting && StateTicker != null)
|
|
||||||
StateTicker = null;
|
|
||||||
|
|
||||||
if (StateTicker == null)
|
|
||||||
return null;
|
|
||||||
StateTicker.MoveNext();
|
|
||||||
return StateTicker.Current;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerator<SimulationState> TickState()
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
SimulationState state;
|
|
||||||
|
|
||||||
// Dequeue used actions
|
|
||||||
var sim = new SimulatorNoRandom(new());
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
state = GetAddonSimulationState();
|
|
||||||
|
|
||||||
var dequeued = false;
|
|
||||||
while (UsedActionQueue.TryDequeue(out var action))
|
|
||||||
{
|
|
||||||
dequeued = true;
|
|
||||||
(_, state) = sim.Execute(state, action);
|
|
||||||
ActionCount++;
|
|
||||||
ActionStates.MutateState(action.Base());
|
|
||||||
}
|
|
||||||
if (dequeued)
|
|
||||||
break;
|
|
||||||
|
|
||||||
// If nothing is dequeued and executed, just return the addon state
|
|
||||||
yield return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intermediate state, wait for addon change
|
|
||||||
var intermediateState = GetAddonSimulationState();
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
yield return state;
|
|
||||||
var newState = GetAddonSimulationState();
|
|
||||||
if (!IsStateInIntermediate(newState, intermediateState))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsStateInIntermediate(SimulationState a, SimulationState b)
|
|
||||||
{
|
|
||||||
b.CP = a.CP;
|
|
||||||
b.ActiveEffects = a.ActiveEffects;
|
|
||||||
return a == b;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnActionUsed(ActionType action)
|
|
||||||
{
|
|
||||||
if (!RecipeUtils.IsCrafting || RecipeUtils.AddonSynthesis == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
UsedActionQueue.Enqueue(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,553 +0,0 @@
|
|||||||
using Craftimizer.Plugin.Utils;
|
|
||||||
using Craftimizer.Simulator;
|
|
||||||
using Craftimizer.Simulator.Actions;
|
|
||||||
using Dalamud;
|
|
||||||
using Dalamud.Interface;
|
|
||||||
using Dalamud.Interface.Components;
|
|
||||||
using Dalamud.Interface.Windowing;
|
|
||||||
using Dalamud.Utility;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
||||||
using ImGuiNET;
|
|
||||||
using Lumina.Excel.GeneratedSheets;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Numerics;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using ActionType = Craftimizer.Simulator.Actions.ActionType;
|
|
||||||
using RecipeNote = Craftimizer.Utils.RecipeNote;
|
|
||||||
|
|
||||||
namespace Craftimizer.Plugin.Windows;
|
|
||||||
|
|
||||||
public unsafe class CraftingLog : Window
|
|
||||||
{
|
|
||||||
private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration
|
|
||||||
| ImGuiWindowFlags.AlwaysAutoResize
|
|
||||||
| ImGuiWindowFlags.NoSavedSettings
|
|
||||||
| ImGuiWindowFlags.NoFocusOnAppearing
|
|
||||||
| ImGuiWindowFlags.NoNavFocus;
|
|
||||||
|
|
||||||
private static Configuration Config => Service.Configuration;
|
|
||||||
|
|
||||||
private const int LeftSideWidth = 350;
|
|
||||||
|
|
||||||
// If relative, increase stat by Value's % (rounded down), and cap increase to Max
|
|
||||||
// If not relative, increase stat by Value, and ignore Max
|
|
||||||
[StructLayout(LayoutKind.Auto)]
|
|
||||||
private record struct FoodStat(bool IsRelative, sbyte Value, short Max, sbyte ValueHQ, short MaxHQ);
|
|
||||||
private sealed record Food(Item Item, string Name, FoodStat? Craftsmanship, FoodStat? Control, FoodStat? CP);
|
|
||||||
|
|
||||||
private static Food[] FoodItems { get; }
|
|
||||||
private static Food[] MedicineItems { get; }
|
|
||||||
private static Random Random { get; }
|
|
||||||
|
|
||||||
private static RecipeNote RecipeUtils => Service.Plugin.RecipeNote;
|
|
||||||
private ushort OldRecipeId { get; set; }
|
|
||||||
|
|
||||||
// Set in CalculateCharacterStats (in PreDraw)
|
|
||||||
private Gearsets.GearsetItem[] CharacterEquipment { get; set; } = null!;
|
|
||||||
private CharacterStats CharacterStatsNoConsumable { get; set; } = null!;
|
|
||||||
private Gearsets.GearsetStats CharacterConsumableBonus { get; set; }
|
|
||||||
private CharacterStats CharacterStatsConsumable { get; set; } = null!;
|
|
||||||
private CannotCraftReason CharacterCannotCraftReason { get; set; }
|
|
||||||
private SimulationInput CharacterSimulationInput { get; set; } = null!;
|
|
||||||
|
|
||||||
// Set in UI
|
|
||||||
private int QualityNotches { get; set; }
|
|
||||||
private int StartingQuality =>
|
|
||||||
RecipeUtils.HQIngredientCount == 0 ?
|
|
||||||
0 :
|
|
||||||
(int)((float)QualityNotches * RecipeUtils.MaxStartingQuality / RecipeUtils.HQIngredientCount);
|
|
||||||
|
|
||||||
private Food? SelectedFood { get; set; }
|
|
||||||
private bool SelectedFoodHQ { get; set; }
|
|
||||||
|
|
||||||
private Food? SelectedMedicine { get; set; }
|
|
||||||
private bool SelectedMedicineHQ { get; set; }
|
|
||||||
|
|
||||||
static CraftingLog()
|
|
||||||
{
|
|
||||||
var foods = new List<Food>();
|
|
||||||
var medicines = new List<Food>();
|
|
||||||
foreach (var item in LuminaSheets.ItemSheet)
|
|
||||||
{
|
|
||||||
var isFood = item.ItemUICategory.Row == 46;
|
|
||||||
var isMedicine = item.ItemUICategory.Row == 44;
|
|
||||||
if (!isFood && !isMedicine)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (item.ItemAction.Value == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!(item.ItemAction.Value.Type is 844 or 845 or 846))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var itemFood = LuminaSheets.ItemFoodSheet.GetRow(item.ItemAction.Value.Data[1]);
|
|
||||||
if (itemFood == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
FoodStat? craftsmanship = null, control = null, cp = null;
|
|
||||||
foreach (var stat in itemFood.UnkData1)
|
|
||||||
{
|
|
||||||
if (stat.BaseParam == 0)
|
|
||||||
continue;
|
|
||||||
var foodStat = new FoodStat(stat.IsRelative, stat.Value, stat.Max, stat.ValueHQ, stat.MaxHQ);
|
|
||||||
switch (stat.BaseParam)
|
|
||||||
{
|
|
||||||
case Gearsets.ParamCraftsmanship: craftsmanship = foodStat; break;
|
|
||||||
case Gearsets.ParamControl: control = foodStat; break;
|
|
||||||
case Gearsets.ParamCP: cp = foodStat; break;
|
|
||||||
default: continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (craftsmanship != null || control != null || cp != null)
|
|
||||||
{
|
|
||||||
var food = new Food(item, item.Name.ToDalamudString().TextValue ?? $"Unknown ({item.RowId})", craftsmanship, control, cp);
|
|
||||||
if (isFood)
|
|
||||||
foods.Add(food);
|
|
||||||
if (isMedicine)
|
|
||||||
medicines.Add(food);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foods.Sort((a, b) => b.Item.LevelItem.Row.CompareTo(a.Item.LevelItem.Row));
|
|
||||||
medicines.Sort((a, b) => b.Item.LevelItem.Row.CompareTo(a.Item.LevelItem.Row));
|
|
||||||
FoodItems = foods.ToArray();
|
|
||||||
MedicineItems = medicines.ToArray();
|
|
||||||
|
|
||||||
Random = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
public CraftingLog() : base("Craftimizer RecipeNoteHelper", WindowFlags, true)
|
|
||||||
{
|
|
||||||
Service.WindowSystem.AddWindow(this);
|
|
||||||
|
|
||||||
IsOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CalculateCharacterStats()
|
|
||||||
{
|
|
||||||
var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems);
|
|
||||||
if (container == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
CharacterEquipment = Gearsets.GetGearsetItems(container);
|
|
||||||
CharacterStatsNoConsumable = Gearsets.CalculateCharacterStats(CharacterEquipment, RecipeUtils.CharacterLevel, RecipeUtils.CanUseManipulation);
|
|
||||||
CharacterConsumableBonus = CalculateConsumableBonus(CharacterStatsNoConsumable);
|
|
||||||
CharacterStatsConsumable = CharacterStatsNoConsumable with
|
|
||||||
{
|
|
||||||
Craftsmanship = CharacterStatsNoConsumable.Craftsmanship + CharacterConsumableBonus.Craftsmanship,
|
|
||||||
Control = CharacterStatsNoConsumable.Control + CharacterConsumableBonus.Control,
|
|
||||||
CP = CharacterStatsNoConsumable.CP + CharacterConsumableBonus.CP,
|
|
||||||
};
|
|
||||||
CharacterCannotCraftReason = Config.OverrideUncraftability ? CannotCraftReason.OK : CanCraftRecipe(CharacterEquipment, CharacterStatsConsumable);
|
|
||||||
|
|
||||||
if (CharacterCannotCraftReason == CannotCraftReason.OK)
|
|
||||||
CharacterSimulationInput = new(CharacterStatsConsumable, RecipeUtils.Info, StartingQuality, Random);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Draw()
|
|
||||||
{
|
|
||||||
ImGui.BeginTable("craftlog", 2, ImGuiTableFlags.BordersInnerV);
|
|
||||||
|
|
||||||
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, LeftSideWidth);
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
DrawCraftInfo();
|
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
DrawGearsets();
|
|
||||||
|
|
||||||
ImGui.EndTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawCraftInfo()
|
|
||||||
{
|
|
||||||
ImGui.BeginTable("craftinfo", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame);
|
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
DrawRecipeInfo();
|
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
DrawCharacterInfo();
|
|
||||||
ImGui.EndTable();
|
|
||||||
|
|
||||||
ImGui.Separator();
|
|
||||||
|
|
||||||
DrawCraftParameters();
|
|
||||||
DrawMacros();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawRecipeInfo()
|
|
||||||
{
|
|
||||||
var s = new StringBuilder();
|
|
||||||
s.AppendLine($"{RecipeUtils.ClassJob.GetName()} {new string('★', RecipeUtils.Table.Stars)}");
|
|
||||||
s.AppendLine($"Level {RecipeUtils.Table.ClassJobLevel} (RLvl {RecipeUtils.Info.RLvl})");
|
|
||||||
s.AppendLine($"Durability: {RecipeUtils.Info.MaxDurability}");
|
|
||||||
s.AppendLine($"Progress: {RecipeUtils.Info.MaxProgress}");
|
|
||||||
s.AppendLine($"Quality: {RecipeUtils.Info.MaxQuality}");
|
|
||||||
ImGui.Text(s.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawCharacterInfo()
|
|
||||||
{
|
|
||||||
if (CharacterCannotCraftReason != CannotCraftReason.OK)
|
|
||||||
{
|
|
||||||
ImGui.TextWrapped(GetCannotCraftReasonText(CharacterCannotCraftReason));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.Text(GetCharacterStatsText(CharacterStatsConsumable));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawCraftParameters()
|
|
||||||
{
|
|
||||||
ImGui.BeginDisabled(RecipeUtils.HQIngredientCount == 0);
|
|
||||||
var qualityNotches = QualityNotches;
|
|
||||||
ImGui.SetNextItemWidth(LeftSideWidth - 115);
|
|
||||||
if (ImGui.SliderInt("Starting Quality", ref qualityNotches, 0, RecipeUtils.HQIngredientCount, StartingQuality.ToString(), ImGuiSliderFlags.NoInput | ImGuiSliderFlags.AlwaysClamp))
|
|
||||||
QualityNotches = qualityNotches;
|
|
||||||
ImGui.EndDisabled();
|
|
||||||
|
|
||||||
ImGui.BeginTable("craftfood", 2, ImGuiTableFlags.BordersInnerV);
|
|
||||||
|
|
||||||
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, LeftSideWidth - 120);
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
|
|
||||||
if (ImGui.BeginCombo("Food", SelectedFood?.Name ?? "None"))
|
|
||||||
{
|
|
||||||
if (ImGui.Selectable("None", SelectedFood == null))
|
|
||||||
SelectedFood = null;
|
|
||||||
|
|
||||||
foreach (var food in FoodItems)
|
|
||||||
if (ImGui.Selectable(food.Name, food == SelectedFood))
|
|
||||||
SelectedFood = food;
|
|
||||||
|
|
||||||
ImGui.EndCombo();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ImGui.BeginCombo("Medicine", SelectedMedicine?.Name ?? "None"))
|
|
||||||
{
|
|
||||||
if (ImGui.Selectable("None", SelectedMedicine == null))
|
|
||||||
SelectedMedicine = null;
|
|
||||||
|
|
||||||
foreach (var food in MedicineItems)
|
|
||||||
if (ImGui.Selectable(food.Name, food == SelectedMedicine))
|
|
||||||
SelectedMedicine = food;
|
|
||||||
|
|
||||||
ImGui.EndCombo();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
|
|
||||||
var s = new StringBuilder();
|
|
||||||
s.AppendLine($"+{CharacterConsumableBonus.Craftsmanship} Craftsmanship");
|
|
||||||
s.AppendLine($"+{CharacterConsumableBonus.Control} Control");
|
|
||||||
s.AppendLine($"+{CharacterConsumableBonus.CP} CP");
|
|
||||||
ImGui.Text(s.ToString());
|
|
||||||
|
|
||||||
ImGui.EndTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawMacros()
|
|
||||||
{
|
|
||||||
var padding = ImGui.GetStyle().FramePadding;
|
|
||||||
|
|
||||||
var fontSize = ImGui.GetFontSize();
|
|
||||||
var height = fontSize + (padding.Y * 2);
|
|
||||||
var width = ImGui.GetContentRegionAvail().X;
|
|
||||||
var size = new Vector2(width, height);
|
|
||||||
var infoColWidth = Simulator.TooltipProgressBarSize.X;
|
|
||||||
var infoButtonCount = 3;
|
|
||||||
var infoButtonWidth = (infoColWidth - ImGui.GetStyle().ItemSpacing.X * (infoButtonCount - 1)) / infoButtonCount;
|
|
||||||
var infoButtonSize = new Vector2(infoButtonWidth, height);
|
|
||||||
var actionColWidth = width - infoColWidth - ImGui.GetStyle().FramePadding.X * 2;
|
|
||||||
var actionCount = 6;
|
|
||||||
var actionSize = new Vector2((actionColWidth - (ImGui.GetStyle().ItemSpacing.X * (actionCount - 1))) / actionCount);
|
|
||||||
|
|
||||||
if (ImGui.Button("Open Simulator", size))
|
|
||||||
OpenSimulatorWindow(null);
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.Button("Generate a new macro", size);
|
|
||||||
|
|
||||||
ImGui.BeginTable("macrotable", 2, ImGuiTableFlags.BordersInner);
|
|
||||||
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, infoColWidth);
|
|
||||||
ImGui.TableSetupColumn("");
|
|
||||||
var simulation = new SimulatorNoRandom(new(CharacterSimulationInput));
|
|
||||||
for (var i = 0; i < Config.Macros.Count; ++i)
|
|
||||||
{
|
|
||||||
var macro = Config.Macros[i];
|
|
||||||
ImGui.PushID(i);
|
|
||||||
ImGui.TableNextRow();
|
|
||||||
|
|
||||||
SimulationState? state = null;
|
|
||||||
if (CharacterCannotCraftReason == CannotCraftReason.OK)
|
|
||||||
{
|
|
||||||
state = new(CharacterSimulationInput);
|
|
||||||
foreach (var action in macro.Actions)
|
|
||||||
(_, state) = simulation.Execute(state.Value, action);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
ImGui.TextWrapped(macro.Name);
|
|
||||||
if (state.HasValue)
|
|
||||||
Simulator.DrawAllProgressTooltips(state!.Value);
|
|
||||||
|
|
||||||
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Copy, infoButtonSize))
|
|
||||||
CopyMacroToClipboard(macro);
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip("Copy macro to clipboard\nHold Shift to exclude wait modifiers");
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.ShareSquare, infoButtonSize))
|
|
||||||
OpenSimulatorWindow(macro);
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip("Open macro in simulator");
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Trash, infoButtonSize))
|
|
||||||
Config.Macros.RemoveAt(i);
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip("Delete macro");
|
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
var j = 0;
|
|
||||||
foreach (var action in macro.Actions)
|
|
||||||
{
|
|
||||||
ImGui.Image(action.GetIcon(RecipeUtils.ClassJob).ImGuiHandle, actionSize);
|
|
||||||
if (j++ % actionCount != actionCount - 1)
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (j == actionCount * 2)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
ImGui.Dummy(Vector2.Zero);
|
|
||||||
ImGui.PopID();
|
|
||||||
}
|
|
||||||
ImGui.EndTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OpenSimulatorWindow(Macro? macro)
|
|
||||||
{
|
|
||||||
Service.Plugin.OpenSimulatorWindow(RecipeUtils.Recipe.ItemResult.Value!, RecipeUtils.Recipe.IsExpert, CharacterSimulationInput, RecipeUtils.ClassJob, macro);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetMacroCommand(ActionType action, bool addWaitTimes)
|
|
||||||
{
|
|
||||||
var actionBase = action.Base();
|
|
||||||
if (actionBase is BaseComboAction comboActionBase)
|
|
||||||
return $"{GetMacroCommand(comboActionBase.ActionTypeA, addWaitTimes)}\n{GetMacroCommand(comboActionBase.ActionTypeB, addWaitTimes)}";
|
|
||||||
if (addWaitTimes)
|
|
||||||
return $"/ac \"{action.GetName(RecipeUtils.ClassJob)}\" <wait.{actionBase.MacroWaitTime}>";
|
|
||||||
else
|
|
||||||
return $"/ac \"{action.GetName(RecipeUtils.ClassJob)}\"";
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CopyMacroToClipboard(Macro macro)
|
|
||||||
{
|
|
||||||
var s = new StringBuilder();
|
|
||||||
if (ImGui.IsKeyDown(ImGuiKey.ModShift))
|
|
||||||
{
|
|
||||||
foreach (var action in macro.Actions)
|
|
||||||
s.AppendLine(GetMacroCommand(action, false));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var action in macro.Actions)
|
|
||||||
s.AppendLine(GetMacroCommand(action, true));
|
|
||||||
s.AppendLine($"/echo Macro Complete! <se.1>");
|
|
||||||
}
|
|
||||||
ImGui.SetClipboardText(s.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawGearsets()
|
|
||||||
{
|
|
||||||
ImGui.Text("Available Gearsets");
|
|
||||||
|
|
||||||
var inst = RaptureGearsetModule.Instance();
|
|
||||||
|
|
||||||
for (var i = 0; i < 100; i++)
|
|
||||||
{
|
|
||||||
var gearset = inst->Gearset[i];
|
|
||||||
if (gearset == null)
|
|
||||||
continue;
|
|
||||||
if (gearset->ID != i)
|
|
||||||
continue;
|
|
||||||
if (!gearset->Flags.HasFlag(RaptureGearsetModule.GearsetFlag.Exists))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (ClassJobUtils.GetClassJobFromIdx(gearset->ClassJob) != RecipeUtils.ClassJob)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var items = Gearsets.GetGearsetItems(gearset);
|
|
||||||
var stats = Gearsets.CalculateCharacterStats(items, RecipeUtils.CharacterLevel, RecipeUtils.CanUseManipulation);
|
|
||||||
var gearsetId = gearset->ID + 1;
|
|
||||||
|
|
||||||
ImGuiUtils.BeginGroupPanel($"{SafeMemory.ReadString((nint)gearset->Name, 47)} ({gearsetId})");
|
|
||||||
ImGui.Text(GetCharacterStatsText(stats));
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (ImGuiComponents.IconButton($"SwapGearset{gearsetId}", FontAwesomeIcon.SyncAlt))
|
|
||||||
Chat.SendMessage($"/gearset change {gearsetId}");
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip($"Swap to gearset {gearsetId}");
|
|
||||||
ImGuiUtils.EndGroupPanel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool DrawConditions()
|
|
||||||
{
|
|
||||||
if (!RecipeUtils.HasValidRecipe)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (OldRecipeId != RecipeUtils.RecipeId)
|
|
||||||
QualityNotches = 0;
|
|
||||||
OldRecipeId = RecipeUtils.RecipeId;
|
|
||||||
|
|
||||||
if (RecipeUtils.AddonRecipe == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Check if RecipeNote addon is visible
|
|
||||||
if (RecipeUtils.AddonRecipe->AtkUnitBase.WindowNode == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Check if RecipeNote has a visible selected recipe
|
|
||||||
if (!RecipeUtils.AddonRecipe->Unk258->IsVisible)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return base.DrawConditions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override unsafe void PreDraw()
|
|
||||||
{
|
|
||||||
var addon = RecipeUtils.AddonRecipe;
|
|
||||||
ref var unit = ref addon->AtkUnitBase;
|
|
||||||
var scale = unit.Scale;
|
|
||||||
var pos = new Vector2(unit.X, unit.Y);
|
|
||||||
var size = new Vector2(unit.WindowNode->AtkResNode.Width, unit.WindowNode->AtkResNode.Height) * scale;
|
|
||||||
|
|
||||||
var node = (AtkResNode*)addon->Unk458; // unit.GetNodeById(59);
|
|
||||||
var nodeParent = addon->Unk258; // unit.GetNodeById(57);
|
|
||||||
|
|
||||||
Position = pos + new Vector2(size.X, (nodeParent->Y + node->Y) * scale);
|
|
||||||
SizeConstraints = new WindowSizeConstraints
|
|
||||||
{
|
|
||||||
MinimumSize = new(-1),
|
|
||||||
MaximumSize = new(10000, 10000)
|
|
||||||
};
|
|
||||||
|
|
||||||
CalculateCharacterStats();
|
|
||||||
|
|
||||||
base.PreDraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Gearsets.GearsetStats CalculateConsumableBonus(CharacterStats stats)
|
|
||||||
{
|
|
||||||
static int CalculateBonus(int param, bool isHq, FoodStat? stat)
|
|
||||||
{
|
|
||||||
if (stat == null)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
var foodStat = stat.Value;
|
|
||||||
var (value, max) = isHq ? (foodStat.ValueHQ, foodStat.MaxHQ) : (foodStat.Value, foodStat.Max);
|
|
||||||
|
|
||||||
if (!foodStat.IsRelative)
|
|
||||||
return value;
|
|
||||||
|
|
||||||
return Math.Min((int)MathF.Floor((float)value * param), max);
|
|
||||||
}
|
|
||||||
|
|
||||||
Gearsets.GearsetStats ret = new();
|
|
||||||
|
|
||||||
if (SelectedFood != null)
|
|
||||||
{
|
|
||||||
ret.CP += CalculateBonus(stats.CP, SelectedFoodHQ, SelectedFood.CP);
|
|
||||||
ret.Craftsmanship += CalculateBonus(stats.Craftsmanship, SelectedFoodHQ, SelectedFood.Craftsmanship);
|
|
||||||
ret.Control += CalculateBonus(stats.Control, SelectedFoodHQ, SelectedFood.Control);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SelectedMedicine != null)
|
|
||||||
{
|
|
||||||
ret.CP += CalculateBonus(stats.CP, SelectedMedicineHQ, SelectedMedicine.CP);
|
|
||||||
ret.Craftsmanship += CalculateBonus(stats.Craftsmanship, SelectedMedicineHQ, SelectedMedicine.Craftsmanship);
|
|
||||||
ret.Control += CalculateBonus(stats.Control, SelectedMedicineHQ, SelectedMedicine.Control);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum CannotCraftReason
|
|
||||||
{
|
|
||||||
OK,
|
|
||||||
WrongClassJob,
|
|
||||||
SpecialistRequired,
|
|
||||||
RequiredItem,
|
|
||||||
RequiredStatus,
|
|
||||||
CraftsmanshipTooLow,
|
|
||||||
ControlTooLow,
|
|
||||||
}
|
|
||||||
|
|
||||||
private CannotCraftReason CanCraftRecipe(Gearsets.GearsetItem[] items, CharacterStats stats)
|
|
||||||
{
|
|
||||||
if (ClassJobUtils.GetClassJobFromIdx((byte)Service.ClientState.LocalPlayer!.ClassJob.Id) != RecipeUtils.ClassJob)
|
|
||||||
return CannotCraftReason.WrongClassJob;
|
|
||||||
|
|
||||||
var recipe = RecipeUtils.Recipe;
|
|
||||||
|
|
||||||
if (recipe.IsSpecializationRequired && !stats.IsSpecialist)
|
|
||||||
return CannotCraftReason.SpecialistRequired;
|
|
||||||
|
|
||||||
if (recipe.ItemRequired.Row != 0)
|
|
||||||
{
|
|
||||||
if (recipe.ItemRequired.Value != null)
|
|
||||||
{
|
|
||||||
if (!items.Any(i => Gearsets.IsItem(i, recipe.ItemRequired.Row)))
|
|
||||||
{
|
|
||||||
return CannotCraftReason.RequiredItem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recipe.StatusRequired.Row != 0)
|
|
||||||
{
|
|
||||||
if (recipe.StatusRequired.Value != null)
|
|
||||||
{
|
|
||||||
if (!Service.ClientState.LocalPlayer.StatusList.Any(s => s.StatusId == recipe.StatusRequired.Row))
|
|
||||||
return CannotCraftReason.RequiredStatus;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recipe.RequiredCraftsmanship > stats.Craftsmanship)
|
|
||||||
return CannotCraftReason.CraftsmanshipTooLow;
|
|
||||||
|
|
||||||
if (recipe.RequiredControl > stats.Control)
|
|
||||||
return CannotCraftReason.ControlTooLow;
|
|
||||||
|
|
||||||
return CannotCraftReason.OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetCannotCraftReasonText(CannotCraftReason reason) =>
|
|
||||||
reason switch
|
|
||||||
{
|
|
||||||
CannotCraftReason.OK => "You can craft this recipe.",
|
|
||||||
CannotCraftReason.WrongClassJob => "Your current class cannot craft this recipe.",
|
|
||||||
CannotCraftReason.SpecialistRequired => "You must be a specialist to craft this recipe.",
|
|
||||||
CannotCraftReason.RequiredItem => "You do not have the required item to craft this recipe.",
|
|
||||||
CannotCraftReason.RequiredStatus => "You do not have the required status effect to craft this recipe.",
|
|
||||||
CannotCraftReason.CraftsmanshipTooLow => "Your craftsmanship is too low to craft this recipe.",
|
|
||||||
CannotCraftReason.ControlTooLow => "Your control is too low to craft this recipe.",
|
|
||||||
_ => "Unknown reason.",
|
|
||||||
};
|
|
||||||
|
|
||||||
private static string GetCharacterStatsText(CharacterStats stats)
|
|
||||||
{
|
|
||||||
var s = new StringBuilder();
|
|
||||||
s.AppendLine($"Level {stats.Level} (CLvl {stats.CLvl})");
|
|
||||||
s.AppendLine($"Craftsmanship {stats.Craftsmanship}");
|
|
||||||
s.AppendLine($"Control {stats.Control}");
|
|
||||||
s.AppendLine($"CP {stats.CP}");
|
|
||||||
if (stats.IsSpecialist)
|
|
||||||
s.AppendLine($" + Specialist");
|
|
||||||
if (stats.HasSplendorousBuff)
|
|
||||||
s.AppendLine($" + Splendorous Tool");
|
|
||||||
return s.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using Craftimizer.Plugin;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Internal.Notifications;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using Dalamud.Interface.Windowing;
|
||||||
|
using ImGuiNET;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Craftimizer.Windows;
|
||||||
|
|
||||||
|
public sealed class MacroClipboard : Window, IDisposable
|
||||||
|
{
|
||||||
|
private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.None;
|
||||||
|
|
||||||
|
private List<string> Macros { get; }
|
||||||
|
|
||||||
|
public MacroClipboard(IEnumerable<string> macros) : base("Macro Clipboard", WindowFlags)
|
||||||
|
{
|
||||||
|
Macros = new(macros);
|
||||||
|
|
||||||
|
IsOpen = true;
|
||||||
|
|
||||||
|
Service.WindowSystem.AddWindow(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Draw()
|
||||||
|
{
|
||||||
|
var idx = 0;
|
||||||
|
foreach(var macro in Macros)
|
||||||
|
DrawMacro(idx++, macro);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMacro(int idx, string macro)
|
||||||
|
{
|
||||||
|
using var id = ImRaii.PushId(idx);
|
||||||
|
using var panel = ImGuiUtils.GroupPanel($"Macro {idx + 1}", -1, out var availWidth);
|
||||||
|
|
||||||
|
var cursor = ImGui.GetCursorPos();
|
||||||
|
|
||||||
|
ImGuiUtils.AlignRight(ImGui.GetFrameHeight(), availWidth);
|
||||||
|
var buttonCursor = ImGui.GetCursorPos();
|
||||||
|
ImGui.InvisibleButton("##copyInvButton", new(ImGui.GetFrameHeight()));
|
||||||
|
var buttonHovered = ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenOverlapped | ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
|
||||||
|
var buttonActive = buttonHovered && ImGui.GetIO().MouseDown[(int)ImGuiMouseButton.Left];
|
||||||
|
var buttonClicked = buttonHovered && ImGui.GetIO().MouseReleased[(int)ImGuiMouseButton.Left];
|
||||||
|
ImGui.SetCursorPos(buttonCursor);
|
||||||
|
{
|
||||||
|
using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(buttonActive ? ImGuiCol.ButtonActive : ImGuiCol.ButtonHovered), buttonHovered);
|
||||||
|
ImGuiUtils.IconButtonSized(FontAwesomeIcon.Paste, new(ImGui.GetFrameHeight()));
|
||||||
|
if (buttonClicked)
|
||||||
|
{
|
||||||
|
ImGui.SetClipboardText(macro);
|
||||||
|
Service.PluginInterface.UiBuilder.AddNotification($"Macro {idx + 1} copied to clipboard.", "Craftimizer Macro Copied", NotificationType.Success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (buttonHovered)
|
||||||
|
ImGui.SetTooltip("Copy to Clipboard");
|
||||||
|
|
||||||
|
ImGui.SetCursorPos(cursor);
|
||||||
|
{
|
||||||
|
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
|
||||||
|
using var padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero);
|
||||||
|
using var bg = ImRaii.PushColor(ImGuiCol.FrameBg, Vector4.Zero);
|
||||||
|
var lineCount = macro.Count(c => c == '\n') + 1;
|
||||||
|
ImGui.InputTextMultiline("", ref macro, (uint)macro.Length + 1, new(availWidth, ImGui.GetTextLineHeight() * Math.Max(15, lineCount) + ImGui.GetStyle().FramePadding.Y), ImGuiInputTextFlags.ReadOnly | ImGuiInputTextFlags.AutoSelectAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttonHovered)
|
||||||
|
ImGui.SetMouseCursor(ImGuiMouseCursor.Arrow);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Service.WindowSystem.RemoveWindow(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,344 @@
|
|||||||
|
using Craftimizer.Plugin;
|
||||||
|
using Craftimizer.Utils;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Windowing;
|
||||||
|
using ImGuiNET;
|
||||||
|
using System;
|
||||||
|
using Craftimizer.Simulator;
|
||||||
|
using Craftimizer.Simulator.Actions;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using Sim = Craftimizer.Simulator.SimulatorNoRandom;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
|
||||||
|
namespace Craftimizer.Windows;
|
||||||
|
|
||||||
|
public sealed class MacroList : Window, IDisposable
|
||||||
|
{
|
||||||
|
private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.None;
|
||||||
|
|
||||||
|
public CharacterStats? CharacterStats { get; private set; }
|
||||||
|
public RecipeData? RecipeData { get; private set; }
|
||||||
|
|
||||||
|
private IReadOnlyList<Macro> Macros => Service.Configuration.Macros;
|
||||||
|
private Dictionary<Macro, SimulationState> MacroStateCache { get; } = new();
|
||||||
|
|
||||||
|
public MacroList() : base("Craftimizer Macro List", WindowFlags, false)
|
||||||
|
{
|
||||||
|
RefreshSearch();
|
||||||
|
|
||||||
|
Macro.OnMacroChanged += OnMacroChanged;
|
||||||
|
Configuration.OnMacroListChanged += OnMacroListChanged;
|
||||||
|
|
||||||
|
CollapsedCondition = ImGuiCond.Appearing;
|
||||||
|
Collapsed = false;
|
||||||
|
|
||||||
|
SizeConstraints = new() { MinimumSize = new(500, 520), MaximumSize = new(float.PositiveInfinity) };
|
||||||
|
|
||||||
|
Service.WindowSystem.AddWindow(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool DrawConditions()
|
||||||
|
{
|
||||||
|
return Service.ClientState.LocalPlayer != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void PreDraw()
|
||||||
|
{
|
||||||
|
var oldCharacterStats = CharacterStats;
|
||||||
|
var oldRecipeData = RecipeData;
|
||||||
|
|
||||||
|
(CharacterStats, RecipeData, _) = Service.Plugin.GetOpenedStats();
|
||||||
|
|
||||||
|
if (oldCharacterStats != CharacterStats || oldRecipeData != RecipeData)
|
||||||
|
RecalculateStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Draw()
|
||||||
|
{
|
||||||
|
DrawSearchBar();
|
||||||
|
using var group = ImRaii.Child("macros", new(-1, -1));
|
||||||
|
if (sortedMacros.Count > 0)
|
||||||
|
{
|
||||||
|
var macros = new List<Macro>(sortedMacros);
|
||||||
|
foreach (var macro in macros)
|
||||||
|
DrawMacro(macro);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var text1 = "You have no macros! Create one by opening";
|
||||||
|
var text2 = "the Macro Editor here or from the Crafting Log.";
|
||||||
|
var text3 = "Open Crafting Log";
|
||||||
|
var text4 = "Open Macro Editor";
|
||||||
|
var buttonRowWidth = ImGui.CalcTextSize(text3).X + ImGui.CalcTextSize(text4).X + ImGui.GetStyle().ItemSpacing.X * 5;
|
||||||
|
var size = new Vector2(
|
||||||
|
Math.Max(
|
||||||
|
Math.Max(ImGui.CalcTextSize(text1).X, ImGui.CalcTextSize(text2).X),
|
||||||
|
buttonRowWidth
|
||||||
|
),
|
||||||
|
ImGui.GetTextLineHeightWithSpacing() * 2 + ImGui.GetFrameHeight()
|
||||||
|
);
|
||||||
|
ImGuiUtils.AlignMiddle(size);
|
||||||
|
using var child = ImRaii.Child("##macroMessage", size);
|
||||||
|
ImGuiUtils.TextCentered(text1);
|
||||||
|
ImGuiUtils.TextCentered(text2);
|
||||||
|
ImGuiUtils.AlignCentered(buttonRowWidth);
|
||||||
|
if (ImGui.Button(text3))
|
||||||
|
Service.Plugin.OpenCraftingLog();
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGui.Button(text4))
|
||||||
|
OpenEditor(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string searchText = string.Empty;
|
||||||
|
private List<Macro> sortedMacros = null!;
|
||||||
|
private void DrawSearchBar()
|
||||||
|
{
|
||||||
|
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
|
||||||
|
if (ImGui.InputTextWithHint("##search", "Search", ref searchText, 100))
|
||||||
|
RefreshSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMacro(Macro macro)
|
||||||
|
{
|
||||||
|
var windowHeight = 2 * ImGui.GetFrameHeightWithSpacing();
|
||||||
|
|
||||||
|
if (macro.Actions.Any(a => a.Category() == ActionCategory.Combo))
|
||||||
|
throw new InvalidOperationException("Combo actions should be sanitized away");
|
||||||
|
|
||||||
|
var stateNullable = GetMacroState(macro);
|
||||||
|
|
||||||
|
using var panel = ImGuiUtils.GroupPanel(macro.Name, -1, out var availWidth);
|
||||||
|
var stepsAvailWidthOffset = ImGui.GetContentRegionAvail().X - availWidth;
|
||||||
|
var spacing = ImGui.GetStyle().ItemSpacing.Y;
|
||||||
|
var miniRowHeight = (windowHeight - spacing) / 2f;
|
||||||
|
|
||||||
|
using var table = ImRaii.Table("table", stateNullable.HasValue ? 3 : 2, ImGuiTableFlags.BordersInnerV);
|
||||||
|
if (table)
|
||||||
|
{
|
||||||
|
if (stateNullable.HasValue)
|
||||||
|
ImGui.TableSetupColumn("stats", ImGuiTableColumnFlags.WidthFixed, 0);
|
||||||
|
ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, 0);
|
||||||
|
ImGui.TableSetupColumn("steps", ImGuiTableColumnFlags.WidthStretch, 0);
|
||||||
|
|
||||||
|
ImGui.TableNextRow(ImGuiTableRowFlags.None, windowHeight);
|
||||||
|
if (stateNullable is { } state)
|
||||||
|
{
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
if (Service.Configuration.ShowOptimalMacroStat)
|
||||||
|
{
|
||||||
|
var progressHeight = windowHeight;
|
||||||
|
if (state.Progress >= state.Input.Recipe.MaxProgress && state.Input.Recipe.MaxQuality > 0)
|
||||||
|
{
|
||||||
|
ImGuiUtils.ArcProgress(
|
||||||
|
(float)state.Quality / state.Input.Recipe.MaxQuality,
|
||||||
|
progressHeight / 2f,
|
||||||
|
.5f,
|
||||||
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
|
ImGui.GetColorU32(Colors.Quality));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Quality: {state.Quality} / {state.Input.Recipe.MaxQuality}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGuiUtils.ArcProgress(
|
||||||
|
(float)state.Progress / state.Input.Recipe.MaxProgress,
|
||||||
|
progressHeight / 2f,
|
||||||
|
.5f,
|
||||||
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
|
ImGui.GetColorU32(Colors.Progress));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Progress: {state.Progress} / {state.Input.Recipe.MaxProgress}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGuiUtils.ArcProgress(
|
||||||
|
(float)state.Progress / state.Input.Recipe.MaxProgress,
|
||||||
|
miniRowHeight / 2f,
|
||||||
|
.5f,
|
||||||
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
|
ImGui.GetColorU32(Colors.Progress));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Progress: {state.Progress} / {state.Input.Recipe.MaxProgress}");
|
||||||
|
|
||||||
|
ImGui.SameLine(0, spacing);
|
||||||
|
ImGuiUtils.ArcProgress(
|
||||||
|
(float)state.Quality / state.Input.Recipe.MaxQuality,
|
||||||
|
miniRowHeight / 2f,
|
||||||
|
.5f,
|
||||||
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
|
ImGui.GetColorU32(Colors.Quality));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Quality: {state.Quality} / {state.Input.Recipe.MaxQuality}");
|
||||||
|
|
||||||
|
ImGuiUtils.ArcProgress((float)state.Durability / state.Input.Recipe.MaxDurability,
|
||||||
|
miniRowHeight / 2f,
|
||||||
|
.5f,
|
||||||
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
|
ImGui.GetColorU32(Colors.Durability));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Remaining Durability: {state.Durability} / {state.Input.Recipe.MaxDurability}");
|
||||||
|
|
||||||
|
ImGui.SameLine(0, spacing);
|
||||||
|
ImGuiUtils.ArcProgress(
|
||||||
|
(float)state.CP / state.Input.Stats.CP,
|
||||||
|
miniRowHeight / 2f,
|
||||||
|
.5f,
|
||||||
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
|
ImGui.GetColorU32(Colors.CP));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Remaining CP: {state.CP} / {state.Input.Stats.CP}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
{
|
||||||
|
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Paste, new(miniRowHeight)))
|
||||||
|
Service.Plugin.CopyMacro(macro.Actions);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Copy to Clipboard");
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Trash, new(miniRowHeight)) && ImGui.GetIO().KeyShift)
|
||||||
|
Service.Configuration.RemoveMacro(macro);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Delete (Hold Shift)");
|
||||||
|
|
||||||
|
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.PencilAlt, new(miniRowHeight)))
|
||||||
|
ShowRenamePopup(macro);
|
||||||
|
DrawRenamePopup(macro);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Rename");
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Edit, new(miniRowHeight)))
|
||||||
|
OpenEditor(macro);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Open in Simulator");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
{
|
||||||
|
var itemsPerRow = (int)MathF.Floor((ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset + spacing) / (miniRowHeight + spacing));
|
||||||
|
var itemCount = macro.Actions.Count;
|
||||||
|
for (var i = 0; i < itemsPerRow * 2; i++)
|
||||||
|
{
|
||||||
|
if (i % itemsPerRow != 0)
|
||||||
|
ImGui.SameLine(0, spacing);
|
||||||
|
if (i < itemCount)
|
||||||
|
{
|
||||||
|
var shouldShowMore = i + 1 == itemsPerRow * 2 && i + 1 < itemCount;
|
||||||
|
if (!shouldShowMore)
|
||||||
|
{
|
||||||
|
ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip(macro.Actions[i].GetName(RecipeData!.ClassJob));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var amtMore = itemCount - itemsPerRow * 2;
|
||||||
|
var pos = ImGui.GetCursorPos();
|
||||||
|
ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight), default, Vector2.One, new(1, 1, 1, .5f));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"{macro.Actions[i].GetName(RecipeData!.ClassJob)}\nand {amtMore} more");
|
||||||
|
ImGui.SetCursorPos(pos);
|
||||||
|
ImGui.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
|
||||||
|
ImGui.Dummy(new(miniRowHeight));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string popupMacroName = string.Empty;
|
||||||
|
private Macro? popupMacro;
|
||||||
|
private void ShowRenamePopup(Macro macro)
|
||||||
|
{
|
||||||
|
ImGui.OpenPopup($"##renamePopup-{macro.GetHashCode()}");
|
||||||
|
popupMacro = macro;
|
||||||
|
popupMacroName = macro.Name;
|
||||||
|
ImGui.SetNextWindowPos(ImGui.GetMousePos() - new Vector2(ImGui.CalcItemWidth() * .25f, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.Y * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawRenamePopup(Macro macro)
|
||||||
|
{
|
||||||
|
using var popup = ImRaii.Popup($"##renamePopup-{macro.GetHashCode()}");
|
||||||
|
if (popup)
|
||||||
|
{
|
||||||
|
if (ImGui.IsWindowAppearing())
|
||||||
|
ImGui.SetKeyboardFocusHere();
|
||||||
|
ImGui.SetNextItemWidth(ImGui.CalcItemWidth());
|
||||||
|
if (ImGui.InputTextWithHint($"##setName", "Name", ref popupMacroName, 100, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(popupMacroName))
|
||||||
|
{
|
||||||
|
popupMacro!.Name = popupMacroName;
|
||||||
|
ImGui.CloseCurrentPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecalculateStats()
|
||||||
|
{
|
||||||
|
MacroStateCache.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshSearch()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(searchText))
|
||||||
|
{
|
||||||
|
sortedMacros = new(Macros);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var matcher = new FuzzyMatcher(searchText.ToLowerInvariant(), MatchMode.FuzzyParts);
|
||||||
|
var query = Macros.AsParallel().Select(i => (Item: i, Score: matcher.Matches(i.Name.ToLowerInvariant())))
|
||||||
|
.Where(t => t.Score > 0)
|
||||||
|
.OrderByDescending(t => t.Score)
|
||||||
|
.Select(t => t.Item);
|
||||||
|
sortedMacros = query.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenEditor(Macro? macro)
|
||||||
|
{
|
||||||
|
var stats = Service.Plugin.GetDefaultStats();
|
||||||
|
Service.Plugin.OpenMacroEditor(stats.Character, stats.Recipe, stats.Buffs, macro?.Actions ?? Enumerable.Empty<ActionType>(), macro != null ? (actions => { macro.ActionEnumerable = actions; Service.Configuration.Save(); }) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMacroChanged(Macro macro)
|
||||||
|
{
|
||||||
|
MacroStateCache.Remove(macro);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMacroListChanged()
|
||||||
|
{
|
||||||
|
RefreshSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SimulationState? GetMacroState(Macro macro)
|
||||||
|
{
|
||||||
|
if (CharacterStats == null || RecipeData == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (MacroStateCache.TryGetValue(macro, out var state))
|
||||||
|
return state;
|
||||||
|
|
||||||
|
state = new SimulationState(new(CharacterStats, RecipeData.RecipeInfo));
|
||||||
|
var sim = new Sim(state);
|
||||||
|
(_, state, _) = sim.ExecuteMultiple(state, macro.Actions);
|
||||||
|
return MacroStateCache[macro] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Macro.OnMacroChanged -= OnMacroChanged;
|
||||||
|
Configuration.OnMacroListChanged -= OnMacroListChanged;
|
||||||
|
|
||||||
|
Service.WindowSystem.RemoveWindow(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,874 @@
|
|||||||
|
using Craftimizer.Plugin;
|
||||||
|
using Craftimizer.Plugin.Utils;
|
||||||
|
using Craftimizer.Simulator;
|
||||||
|
using Craftimizer.Simulator.Actions;
|
||||||
|
using Craftimizer.Solver;
|
||||||
|
using Craftimizer.Utils;
|
||||||
|
using Dalamud.Game.Text;
|
||||||
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
|
using Dalamud.Interface.Components;
|
||||||
|
using Dalamud.Interface.GameFonts;
|
||||||
|
using Dalamud.Interface.Internal;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using Dalamud.Interface.Windowing;
|
||||||
|
using Dalamud.Utility;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||||
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
using ImGuiNET;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ActionType = Craftimizer.Simulator.Actions.ActionType;
|
||||||
|
using ClassJob = Craftimizer.Simulator.ClassJob;
|
||||||
|
using CSRecipeNote = FFXIVClientStructs.FFXIV.Client.Game.UI.RecipeNote;
|
||||||
|
|
||||||
|
namespace Craftimizer.Windows;
|
||||||
|
|
||||||
|
public sealed unsafe class RecipeNote : Window, IDisposable
|
||||||
|
{
|
||||||
|
private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration
|
||||||
|
| ImGuiWindowFlags.AlwaysAutoResize
|
||||||
|
| ImGuiWindowFlags.NoSavedSettings
|
||||||
|
| ImGuiWindowFlags.NoFocusOnAppearing
|
||||||
|
| ImGuiWindowFlags.NoNavFocus;
|
||||||
|
|
||||||
|
public enum CraftableStatus
|
||||||
|
{
|
||||||
|
OK,
|
||||||
|
LockedClassJob,
|
||||||
|
WrongClassJob,
|
||||||
|
SpecialistRequired,
|
||||||
|
RequiredItem,
|
||||||
|
RequiredStatus,
|
||||||
|
CraftsmanshipTooLow,
|
||||||
|
ControlTooLow,
|
||||||
|
}
|
||||||
|
|
||||||
|
public AddonRecipeNote* Addon { get; private set; }
|
||||||
|
public RecipeData? RecipeData { get; private set; }
|
||||||
|
public CharacterStats? CharacterStats { get; private set; }
|
||||||
|
public CraftableStatus CraftStatus { get; private set; }
|
||||||
|
|
||||||
|
private CancellationTokenSource? BestMacroTokenSource { get; set; }
|
||||||
|
private Exception? BestMacroException { get; set; }
|
||||||
|
public (Macro, SimulationState)? BestSavedMacro { get; private set; }
|
||||||
|
public bool HasSavedMacro { get; private set; }
|
||||||
|
public SolverSolution? BestSuggestedMacro { get; private set; }
|
||||||
|
|
||||||
|
private IDalamudTextureWrap ExpertBadge { get; }
|
||||||
|
private IDalamudTextureWrap CollectibleBadge { get; }
|
||||||
|
private IDalamudTextureWrap SplendorousBadge { get; }
|
||||||
|
private IDalamudTextureWrap SpecialistBadge { get; }
|
||||||
|
private IDalamudTextureWrap NoManipulationBadge { get; }
|
||||||
|
private GameFontHandle AxisFont { get; }
|
||||||
|
|
||||||
|
public RecipeNote() : base("Craftimizer RecipeNote", WindowFlags)
|
||||||
|
{
|
||||||
|
ExpertBadge = Service.IconManager.GetAssemblyTexture("Graphics.expert_badge.png");
|
||||||
|
CollectibleBadge = Service.IconManager.GetAssemblyTexture("Graphics.collectible_badge.png");
|
||||||
|
SplendorousBadge = Service.IconManager.GetAssemblyTexture("Graphics.splendorous.png");
|
||||||
|
SpecialistBadge = Service.IconManager.GetAssemblyTexture("Graphics.specialist.png");
|
||||||
|
NoManipulationBadge = Service.IconManager.GetAssemblyTexture("Graphics.no_manip.png");
|
||||||
|
AxisFont = Service.PluginInterface.UiBuilder.GetGameFontHandle(new(GameFontFamilyAndSize.Axis14));
|
||||||
|
|
||||||
|
RespectCloseHotkey = false;
|
||||||
|
DisableWindowSounds = true;
|
||||||
|
ShowCloseButton = false;
|
||||||
|
IsOpen = true;
|
||||||
|
|
||||||
|
SizeConstraints = new WindowSizeConstraints
|
||||||
|
{
|
||||||
|
MinimumSize = new(-1),
|
||||||
|
MaximumSize = new(10000, 10000)
|
||||||
|
};
|
||||||
|
|
||||||
|
Service.WindowSystem.AddWindow(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool wasOpen;
|
||||||
|
public override bool DrawConditions()
|
||||||
|
{
|
||||||
|
var isOpen = ShouldDraw();
|
||||||
|
if (isOpen != wasOpen)
|
||||||
|
{
|
||||||
|
if (wasOpen)
|
||||||
|
BestMacroTokenSource?.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
wasOpen = isOpen;
|
||||||
|
return isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ShouldDraw()
|
||||||
|
{
|
||||||
|
if (Service.ClientState.LocalPlayer == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
{
|
||||||
|
Addon = (AddonRecipeNote*)Service.GameGui.GetAddonByName("RecipeNote");
|
||||||
|
if (Addon == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Check if RecipeNote addon is visible
|
||||||
|
if (Addon->AtkUnitBase.WindowNode == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Check if RecipeNote has a visible selected recipe
|
||||||
|
if (!Addon->Unk258->IsVisible)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var statsChanged = false;
|
||||||
|
{
|
||||||
|
var instance = CSRecipeNote.Instance();
|
||||||
|
|
||||||
|
var list = instance->RecipeList;
|
||||||
|
if (list == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var recipeEntry = list->SelectedRecipe;
|
||||||
|
if (recipeEntry == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var recipeId = recipeEntry->RecipeId;
|
||||||
|
if (recipeId != RecipeData?.RecipeId)
|
||||||
|
{
|
||||||
|
RecipeData = new(recipeId);
|
||||||
|
statsChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Gearsets.GearsetItem[] gearItems;
|
||||||
|
{
|
||||||
|
var gearStats = Gearsets.CalculateGearsetCurrentStats();
|
||||||
|
|
||||||
|
var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems);
|
||||||
|
if (container == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
gearItems = Gearsets.GetGearsetItems(container);
|
||||||
|
|
||||||
|
var characterStats = Gearsets.CalculateCharacterStats(gearStats, gearItems, RecipeData.ClassJob.GetPlayerLevel(), RecipeData.ClassJob.CanPlayerUseManipulation());
|
||||||
|
if (characterStats != CharacterStats)
|
||||||
|
{
|
||||||
|
CharacterStats = characterStats;
|
||||||
|
statsChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var craftStatus = CalculateCraftStatus(gearItems);
|
||||||
|
if (craftStatus != CraftStatus)
|
||||||
|
{
|
||||||
|
CraftStatus = craftStatus;
|
||||||
|
statsChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((statsChanged || (BestMacroTokenSource?.IsCancellationRequested ?? false)) && CraftStatus == CraftableStatus.OK)
|
||||||
|
CalculateBestMacros();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void PreDraw()
|
||||||
|
{
|
||||||
|
ref var unit = ref Addon->AtkUnitBase;
|
||||||
|
var scale = unit.Scale;
|
||||||
|
var pos = new Vector2(unit.X, unit.Y);
|
||||||
|
var size = new Vector2(unit.WindowNode->AtkResNode.Width, unit.WindowNode->AtkResNode.Height) * scale;
|
||||||
|
|
||||||
|
var node = (AtkResNode*)Addon->Unk458; // unit.GetNodeById(59);
|
||||||
|
var nodeParent = Addon->Unk258; // unit.GetNodeById(57);
|
||||||
|
|
||||||
|
Position = ImGuiHelpers.MainViewport.Pos + pos + new Vector2(size.X, (nodeParent->Y + node->Y) * scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Draw()
|
||||||
|
{
|
||||||
|
var availWidth = ImGui.GetContentRegionAvail().X;
|
||||||
|
using (var table = ImRaii.Table("stats", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingFixedSame))
|
||||||
|
{
|
||||||
|
if (table)
|
||||||
|
{
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
DrawCharacterStats();
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
DrawRecipeStats();
|
||||||
|
|
||||||
|
// Ensure that we know the window should be the same size as this table. Any more and it'll grow slowly and won't shrink when it could
|
||||||
|
ImGui.SameLine(0, 0);
|
||||||
|
// The -1 is to account for the extra vertical separator on the right that ImGui draws for some reason
|
||||||
|
availWidth = ImGui.GetCursorPosX() - ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().CellPadding.X - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CraftStatus != CraftableStatus.OK)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
|
||||||
|
var panelWidth = availWidth - ImGui.GetStyle().ItemSpacing.X * 2;
|
||||||
|
using (var panel = ImGuiUtils.GroupPanel("Best Saved Macro", panelWidth, out _))
|
||||||
|
{
|
||||||
|
var stepsPanelWidthOffset = ImGui.GetContentRegionAvail().X - panelWidth;
|
||||||
|
if (BestSavedMacro is { } savedMacro)
|
||||||
|
{
|
||||||
|
ImGuiUtils.TextCentered(savedMacro.Item1.Name, panelWidth);
|
||||||
|
DrawMacro((savedMacro.Item1.Actions, savedMacro.Item2), a => { savedMacro.Item1.ActionEnumerable = a; Service.Configuration.Save(); }, stepsPanelWidthOffset, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
DrawMacro(null, null, stepsPanelWidthOffset, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var panel = ImGuiUtils.GroupPanel("Suggested Macro", panelWidth, out _))
|
||||||
|
{
|
||||||
|
var stepsPanelWidthOffset = ImGui.GetContentRegionAvail().X - panelWidth;
|
||||||
|
if (BestSuggestedMacro is { } suggestedMacro)
|
||||||
|
DrawMacro((suggestedMacro.Actions, suggestedMacro.State), null, stepsPanelWidthOffset, false);
|
||||||
|
else
|
||||||
|
DrawMacro(null, null, stepsPanelWidthOffset, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
|
||||||
|
if (ImGui.Button("View Saved Macros", new(availWidth, 0)))
|
||||||
|
Service.Plugin.OpenMacroListWindow();
|
||||||
|
|
||||||
|
if (ImGui.Button("Open in Simulator", new(availWidth, 0)))
|
||||||
|
Service.Plugin.OpenMacroEditor(CharacterStats!, RecipeData!, new(Service.ClientState.LocalPlayer!.StatusList), Enumerable.Empty<ActionType>(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCharacterStats()
|
||||||
|
{
|
||||||
|
ImGuiUtils.TextCentered("Crafter");
|
||||||
|
|
||||||
|
var level = RecipeData!.ClassJob.GetPlayerLevel();
|
||||||
|
{
|
||||||
|
var textClassName = RecipeData.ClassJob.GetAbbreviation();
|
||||||
|
Vector2 textClassSize;
|
||||||
|
{
|
||||||
|
var layout = AxisFont.LayoutBuilder(textClassName).Build();
|
||||||
|
textClassSize = new(layout.Width, layout.Height);
|
||||||
|
}
|
||||||
|
var levelText = string.Empty;
|
||||||
|
if (level != 0)
|
||||||
|
levelText = SqText.LevelPrefix.ToIconChar() + SqText.ToLevelString(level);
|
||||||
|
var imageSize = ImGui.GetFrameHeight();
|
||||||
|
bool hasSplendorous = false, hasSpecialist = false, shouldHaveManip = false;
|
||||||
|
if (CraftStatus is not (CraftableStatus.LockedClassJob or CraftableStatus.WrongClassJob))
|
||||||
|
{
|
||||||
|
hasSplendorous = CharacterStats!.HasSplendorousBuff;
|
||||||
|
hasSpecialist = CharacterStats!.IsSpecialist;
|
||||||
|
shouldHaveManip = !CharacterStats.CanUseManipulation && CharacterStats.Level >= ActionType.Manipulation.Level();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiUtils.AlignCentered(
|
||||||
|
imageSize + 5 +
|
||||||
|
textClassSize.X +
|
||||||
|
(level == 0 ? 0 : (3 + ImGui.CalcTextSize(levelText).X)) +
|
||||||
|
(hasSplendorous ? (3 + imageSize) : 0) +
|
||||||
|
(hasSpecialist ? (3 + imageSize) : 0) +
|
||||||
|
(shouldHaveManip ? (3 + imageSize) : 0)
|
||||||
|
);
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
|
||||||
|
var uv0 = new Vector2(6, 3);
|
||||||
|
var uv1 = uv0 + new Vector2(44);
|
||||||
|
uv0 /= new Vector2(56);
|
||||||
|
uv1 /= new Vector2(56);
|
||||||
|
|
||||||
|
ImGui.Image(Service.IconManager.GetIcon(RecipeData.ClassJob.GetIconId()).ImGuiHandle, new Vector2(imageSize), uv0, uv1);
|
||||||
|
ImGui.SameLine(0, 5);
|
||||||
|
|
||||||
|
if (level != 0)
|
||||||
|
{
|
||||||
|
ImGui.Text(levelText);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"CLvl {Gearsets.CalculateCLvl(level)}");
|
||||||
|
ImGui.SameLine(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize - textClassSize.Y) / 2);
|
||||||
|
AxisFont.Text(textClassName);
|
||||||
|
|
||||||
|
if (hasSplendorous)
|
||||||
|
{
|
||||||
|
ImGui.SameLine(0, 3);
|
||||||
|
ImGui.Image(SplendorousBadge.ImGuiHandle, new Vector2(imageSize));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Splendorous Tool");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasSpecialist)
|
||||||
|
{
|
||||||
|
ImGui.SameLine(0, 3);
|
||||||
|
ImGui.Image(SpecialistBadge.ImGuiHandle, new Vector2(imageSize), Vector2.Zero, Vector2.One, new(0.99f, 0.97f, 0.62f, 1f));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Specialist");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldHaveManip)
|
||||||
|
{
|
||||||
|
ImGui.SameLine(0, 3);
|
||||||
|
ImGui.Image(NoManipulationBadge.ImGuiHandle, new Vector2(imageSize));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"No Manipulation (Missing Job Quest)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
|
||||||
|
switch (CraftStatus)
|
||||||
|
{
|
||||||
|
case CraftableStatus.LockedClassJob:
|
||||||
|
{
|
||||||
|
ImGuiUtils.TextCentered($"You do not have {RecipeData.ClassJob.GetName().ToLowerInvariant()} unlocked.");
|
||||||
|
ImGui.Separator();
|
||||||
|
var unlockQuest = RecipeData.ClassJob.GetUnlockQuest();
|
||||||
|
var (questGiver, questTerritory, questLocation, mapPayload) = ResolveLevelData(unlockQuest.IssuerLocation.Row);
|
||||||
|
|
||||||
|
var unlockText = $"Unlock it from {questGiver}";
|
||||||
|
ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGui.GetFrameHeight());
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.Text(unlockText);
|
||||||
|
ImGui.SameLine(0, 5);
|
||||||
|
if (ImGuiComponents.IconButton(FontAwesomeIcon.Flag))
|
||||||
|
Service.GameGui.OpenMapWithMapLink(mapPayload);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Open in map");
|
||||||
|
|
||||||
|
ImGuiUtils.TextCentered($"{questTerritory} ({questLocation.X:0.0}, {questLocation.Y:0.0})");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CraftableStatus.WrongClassJob:
|
||||||
|
{
|
||||||
|
ImGuiUtils.TextCentered($"You are not a {RecipeData.ClassJob.GetName().ToLowerInvariant()}.");
|
||||||
|
var gearsetId = GetGearsetForJob(RecipeData.ClassJob);
|
||||||
|
if (gearsetId.HasValue)
|
||||||
|
{
|
||||||
|
if (ImGuiUtils.ButtonCentered("Switch Job"))
|
||||||
|
Chat.SendMessage($"/gearset change {gearsetId + 1}");
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Swap to gearset {gearsetId + 1}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
ImGuiUtils.TextCentered($"You do not have any {RecipeData.ClassJob.GetName().ToLowerInvariant()} gearsets.");
|
||||||
|
ImGui.Dummy(default);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CraftableStatus.SpecialistRequired:
|
||||||
|
{
|
||||||
|
ImGuiUtils.TextCentered($"You need to be a specialist to craft this recipe.");
|
||||||
|
|
||||||
|
var (vendorName, vendorTerritory, vendorLoation, mapPayload) = ResolveLevelData(5891399);
|
||||||
|
|
||||||
|
var unlockText = $"Trade a Soul of the Crafter to {vendorName}";
|
||||||
|
ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGui.GetFrameHeight());
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.Text(unlockText);
|
||||||
|
ImGui.SameLine(0, 5);
|
||||||
|
if (ImGuiComponents.IconButton(FontAwesomeIcon.Flag))
|
||||||
|
Service.GameGui.OpenMapWithMapLink(mapPayload);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Open in map");
|
||||||
|
|
||||||
|
ImGuiUtils.TextCentered($"{vendorTerritory} ({vendorLoation.X:0.0}, {vendorLoation.Y:0.0})");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CraftableStatus.RequiredItem:
|
||||||
|
{
|
||||||
|
var item = RecipeData.Recipe.ItemRequired.Value!;
|
||||||
|
var itemName = item.Name.ToDalamudString().ToString();
|
||||||
|
var imageSize = ImGui.GetFrameHeight();
|
||||||
|
|
||||||
|
ImGuiUtils.TextCentered($"You are missing the required equipment.");
|
||||||
|
ImGuiUtils.AlignCentered(imageSize + 5 + ImGui.CalcTextSize(itemName).X);
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.Image(Service.IconManager.GetIcon(item.Icon).ImGuiHandle, new(imageSize));
|
||||||
|
ImGui.SameLine(0, 5);
|
||||||
|
ImGui.Text(itemName);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CraftableStatus.RequiredStatus:
|
||||||
|
{
|
||||||
|
var status = RecipeData.Recipe.StatusRequired.Value!;
|
||||||
|
var statusName = status.Name.ToDalamudString().ToString();
|
||||||
|
var statusIcon = Service.IconManager.GetIcon(status.Icon);
|
||||||
|
var imageSize = new Vector2(ImGui.GetFrameHeight() * statusIcon.Width / statusIcon.Height, ImGui.GetFrameHeight());
|
||||||
|
|
||||||
|
ImGuiUtils.TextCentered($"You are missing the required status effect.");
|
||||||
|
ImGuiUtils.AlignCentered(imageSize.X + 5 + ImGui.CalcTextSize(statusName).X);
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.Image(statusIcon.ImGuiHandle, imageSize);
|
||||||
|
ImGui.SameLine(0, 5);
|
||||||
|
ImGui.Text(statusName);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CraftableStatus.CraftsmanshipTooLow:
|
||||||
|
{
|
||||||
|
ImGuiUtils.TextCentered("Your Craftsmanship is too low.");
|
||||||
|
|
||||||
|
DrawRequiredStatsTable(CharacterStats!.Craftsmanship, RecipeData.Recipe.RequiredCraftsmanship);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CraftableStatus.ControlTooLow:
|
||||||
|
{
|
||||||
|
ImGuiUtils.TextCentered("Your Control is too low.");
|
||||||
|
|
||||||
|
DrawRequiredStatsTable(CharacterStats!.Control, RecipeData.Recipe.RequiredControl);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CraftableStatus.OK:
|
||||||
|
{
|
||||||
|
using var table = ImRaii.Table("characterStats", 2);
|
||||||
|
if (table)
|
||||||
|
{
|
||||||
|
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 100);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text("Craftsmanship");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGuiUtils.TextRight($"{CharacterStats!.Craftsmanship}");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text("Control");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGuiUtils.TextRight($"{CharacterStats.Control}");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text("CP");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGuiUtils.TextRight($"{CharacterStats.CP}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawRecipeStats()
|
||||||
|
{
|
||||||
|
ImGuiUtils.TextCentered("Recipe");
|
||||||
|
|
||||||
|
{
|
||||||
|
var textStars = new string('★', RecipeData!.Table.Stars);
|
||||||
|
var textStarsSize = Vector2.Zero;
|
||||||
|
if (!string.IsNullOrEmpty(textStars)) {
|
||||||
|
var layout = AxisFont.LayoutBuilder(textStars).Build();
|
||||||
|
textStarsSize = new(layout.Width, layout.Height);
|
||||||
|
}
|
||||||
|
var textLevel = SqText.LevelPrefix.ToIconChar() + SqText.ToLevelString(RecipeData.RecipeInfo.ClassJobLevel);
|
||||||
|
var isExpert = RecipeData.RecipeInfo.IsExpert;
|
||||||
|
var isCollectable = RecipeData.Recipe.ItemResult.Value!.IsCollectable;
|
||||||
|
var imageSize = ImGui.GetFrameHeight();
|
||||||
|
var textSize = ImGui.GetFontSize();
|
||||||
|
var badgeSize = new Vector2(textSize * ExpertBadge.Width / ExpertBadge.Height, textSize);
|
||||||
|
var badgeOffset = (imageSize - badgeSize.Y) / 2;
|
||||||
|
|
||||||
|
ImGuiUtils.AlignCentered(
|
||||||
|
imageSize + 5 +
|
||||||
|
ImGui.CalcTextSize(textLevel).X +
|
||||||
|
(textStarsSize != Vector2.Zero ? textStarsSize.X + 3 : 0) +
|
||||||
|
(isCollectable ? badgeSize.X + 3 : 0) +
|
||||||
|
(isExpert ? badgeSize.X + 3 : 0)
|
||||||
|
);
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
|
||||||
|
ImGui.Image(Service.IconManager.GetIcon(RecipeData.Recipe.ItemResult.Value!.Icon).ImGuiHandle, new Vector2(imageSize));
|
||||||
|
|
||||||
|
ImGui.SameLine(0, 5);
|
||||||
|
ImGui.Text(textLevel);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"RLvl {RecipeData.RecipeInfo.RLvl}");
|
||||||
|
|
||||||
|
if (textStarsSize != Vector2.Zero)
|
||||||
|
{
|
||||||
|
ImGui.SameLine(0, 3);
|
||||||
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize - textStarsSize.Y) / 2);
|
||||||
|
AxisFont.Text(textStars);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCollectable)
|
||||||
|
{
|
||||||
|
ImGui.SameLine(0, 3);
|
||||||
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + badgeOffset);
|
||||||
|
ImGui.Image(CollectibleBadge.ImGuiHandle, badgeSize);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Collectible");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExpert)
|
||||||
|
{
|
||||||
|
ImGui.SameLine(0, 3);
|
||||||
|
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + badgeOffset);
|
||||||
|
ImGui.Image(ExpertBadge.ImGuiHandle, badgeSize);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Expert Recipe");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.Separator();
|
||||||
|
|
||||||
|
using var table = ImRaii.Table("recipeStats", 2);
|
||||||
|
if (table)
|
||||||
|
{
|
||||||
|
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 100);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text("Progress");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxProgress}");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text("Quality");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxQuality}");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text("Durability");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxDurability}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawMacro((IReadOnlyList<ActionType> Actions, SimulationState State)? macroValue, Action<IEnumerable<ActionType>>? setter, float stepsAvailWidthOffset, bool isSavedMacro)
|
||||||
|
{
|
||||||
|
var windowHeight = 2 * ImGui.GetFrameHeightWithSpacing();
|
||||||
|
|
||||||
|
if (macroValue is not { } macro)
|
||||||
|
{
|
||||||
|
if (isSavedMacro && !HasSavedMacro)
|
||||||
|
ImGuiUtils.TextMiddleNewLine("You have no macros!", new(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight + 1));
|
||||||
|
else if (BestMacroException == null)
|
||||||
|
ImGuiUtils.TextMiddleNewLine("Calculating...", new(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight + 1));
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed))
|
||||||
|
ImGuiUtils.TextCentered("An exception occurred");
|
||||||
|
if (ImGuiUtils.ButtonCentered("Copy Error Message"))
|
||||||
|
ImGui.SetClipboardText(BestMacroException.ToString());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (macro.Actions.Any(a => a.Category() == ActionCategory.Combo))
|
||||||
|
throw new InvalidOperationException("Combo actions should be sanitized away");
|
||||||
|
|
||||||
|
using var table = ImRaii.Table("table", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame);
|
||||||
|
if (table)
|
||||||
|
{
|
||||||
|
ImGui.TableSetupColumn("desc", ImGuiTableColumnFlags.WidthFixed, 0);
|
||||||
|
ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, 0);
|
||||||
|
ImGui.TableSetupColumn("steps", ImGuiTableColumnFlags.WidthStretch);
|
||||||
|
|
||||||
|
ImGui.TableNextRow(ImGuiTableRowFlags.None, windowHeight);
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
|
||||||
|
var spacing = ImGui.GetStyle().ItemSpacing.Y;
|
||||||
|
var miniRowHeight = (windowHeight - spacing) / 2f;
|
||||||
|
|
||||||
|
{
|
||||||
|
if (Service.Configuration.ShowOptimalMacroStat)
|
||||||
|
{
|
||||||
|
var progressHeight = windowHeight;
|
||||||
|
if (macro.State.Progress >= macro.State.Input.Recipe.MaxProgress && macro.State.Input.Recipe.MaxQuality > 0)
|
||||||
|
{
|
||||||
|
ImGuiUtils.ArcProgress(
|
||||||
|
(float)macro.State.Quality / macro.State.Input.Recipe.MaxQuality,
|
||||||
|
progressHeight / 2f,
|
||||||
|
.5f,
|
||||||
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
|
ImGui.GetColorU32(Colors.Quality));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGuiUtils.ArcProgress(
|
||||||
|
(float)macro.State.Progress / macro.State.Input.Recipe.MaxProgress,
|
||||||
|
progressHeight / 2f,
|
||||||
|
.5f,
|
||||||
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
|
ImGui.GetColorU32(Colors.Progress));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ImGuiUtils.ArcProgress(
|
||||||
|
(float)macro.State.Progress / macro.State.Input.Recipe.MaxProgress,
|
||||||
|
miniRowHeight / 2f,
|
||||||
|
.5f,
|
||||||
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
|
ImGui.GetColorU32(Colors.Progress));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}");
|
||||||
|
|
||||||
|
ImGui.SameLine(0, spacing);
|
||||||
|
ImGuiUtils.ArcProgress(
|
||||||
|
(float)macro.State.Quality / macro.State.Input.Recipe.MaxQuality,
|
||||||
|
miniRowHeight / 2f,
|
||||||
|
.5f,
|
||||||
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
|
ImGui.GetColorU32(Colors.Quality));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}");
|
||||||
|
|
||||||
|
ImGuiUtils.ArcProgress((float)macro.State.Durability / macro.State.Input.Recipe.MaxDurability,
|
||||||
|
miniRowHeight / 2f,
|
||||||
|
.5f,
|
||||||
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
|
ImGui.GetColorU32(Colors.Durability));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Remaining Durability: {macro.State.Durability} / {macro.State.Input.Recipe.MaxDurability}");
|
||||||
|
|
||||||
|
ImGui.SameLine(0, spacing);
|
||||||
|
ImGuiUtils.ArcProgress(
|
||||||
|
(float)macro.State.CP / macro.State.Input.Stats.CP,
|
||||||
|
miniRowHeight / 2f,
|
||||||
|
.5f,
|
||||||
|
ImGui.GetColorU32(ImGuiCol.TableBorderLight),
|
||||||
|
ImGui.GetColorU32(Colors.CP));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"Remaining CP: {macro.State.CP} / {macro.State.Input.Stats.CP}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
{
|
||||||
|
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Edit, new(miniRowHeight)))
|
||||||
|
Service.Plugin.OpenMacroEditor(CharacterStats!, RecipeData!, new(Service.ClientState.LocalPlayer!.StatusList), macro.Actions, setter);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Open in Simulator");
|
||||||
|
if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Paste, new(miniRowHeight)))
|
||||||
|
Service.Plugin.CopyMacro(macro.Actions);
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Copy to Clipboard");
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
{
|
||||||
|
var itemsPerRow = (int)MathF.Floor((ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset + spacing) / (miniRowHeight + spacing));
|
||||||
|
var itemCount = macro.Actions.Count;
|
||||||
|
for (var i = 0; i < itemsPerRow * 2; i++)
|
||||||
|
{
|
||||||
|
if (i % itemsPerRow != 0)
|
||||||
|
ImGui.SameLine(0, spacing);
|
||||||
|
if (i < itemCount)
|
||||||
|
{
|
||||||
|
var shouldShowMore = i + 1 == itemsPerRow * 2 && i + 1 < itemCount;
|
||||||
|
if (!shouldShowMore)
|
||||||
|
{
|
||||||
|
ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip(macro.Actions[i].GetName(RecipeData!.ClassJob));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var amtMore = itemCount - itemsPerRow * 2;
|
||||||
|
var pos = ImGui.GetCursorPos();
|
||||||
|
ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight), default, Vector2.One, new(1, 1, 1, .5f));
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip($"{macro.Actions[i].GetName(RecipeData!.ClassJob)}\nand {amtMore} more");
|
||||||
|
ImGui.SetCursorPos(pos);
|
||||||
|
ImGui.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
|
||||||
|
ImGui.Dummy(new(miniRowHeight));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DrawRequiredStatsTable(int current, int required)
|
||||||
|
{
|
||||||
|
if (current >= required)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(current));
|
||||||
|
|
||||||
|
using var table = ImRaii.Table("requiredStats", 2);
|
||||||
|
if (table)
|
||||||
|
{
|
||||||
|
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 100);
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text("Current");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextColored(new(0, 1, 0, 1), $"{current}");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text("Required");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.TextColored(new(1, 0, 0, 1), $"{required}");
|
||||||
|
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text("You need");
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
ImGui.Text($"{required - current}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CraftableStatus CalculateCraftStatus(Gearsets.GearsetItem[] gearItems)
|
||||||
|
{
|
||||||
|
if (RecipeData!.ClassJob.GetPlayerLevel() == 0)
|
||||||
|
return CraftableStatus.LockedClassJob;
|
||||||
|
|
||||||
|
if (PlayerState.Instance()->CurrentClassJobId != RecipeData.ClassJob.GetClassJobIndex())
|
||||||
|
return CraftableStatus.WrongClassJob;
|
||||||
|
|
||||||
|
if (RecipeData.Recipe.IsSpecializationRequired && !(CharacterStats!.IsSpecialist))
|
||||||
|
return CraftableStatus.SpecialistRequired;
|
||||||
|
|
||||||
|
var itemRequired = RecipeData.Recipe.ItemRequired;
|
||||||
|
if (itemRequired.Row != 0 && itemRequired.Value != null)
|
||||||
|
{
|
||||||
|
if (!gearItems.Any(i => Gearsets.IsItem(i, itemRequired.Row)))
|
||||||
|
return CraftableStatus.RequiredItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
var statusRequired = RecipeData.Recipe.StatusRequired;
|
||||||
|
if (statusRequired.Row != 0 && statusRequired.Value != null)
|
||||||
|
{
|
||||||
|
if (!Service.ClientState.LocalPlayer!.StatusList.Any(s => s.StatusId == statusRequired.Row))
|
||||||
|
return CraftableStatus.RequiredStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RecipeData.Recipe.RequiredCraftsmanship > CharacterStats!.Craftsmanship)
|
||||||
|
return CraftableStatus.CraftsmanshipTooLow;
|
||||||
|
|
||||||
|
if (RecipeData.Recipe.RequiredControl > CharacterStats.Control)
|
||||||
|
return CraftableStatus.ControlTooLow;
|
||||||
|
|
||||||
|
return CraftableStatus.OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string NpcName, string Territory, Vector2 MapLocation, MapLinkPayload Payload) ResolveLevelData(uint levelRowId)
|
||||||
|
{
|
||||||
|
var level = LuminaSheets.LevelSheet.GetRow(levelRowId) ??
|
||||||
|
throw new ArgumentNullException(nameof(levelRowId), $"Invalid level row {levelRowId}");
|
||||||
|
var territory = level.Territory.Value!.PlaceName.Value!.Name.ToDalamudString().ToString();
|
||||||
|
var location = MapUtil.WorldToMap(new(level.X, level.Z), level.Map.Value!);
|
||||||
|
|
||||||
|
return (ResolveNpcResidentName(level.Object), territory, location, new(level.Territory.Row, level.Map.Row, location.X, location.Y));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveNpcResidentName(uint npcRowId)
|
||||||
|
{
|
||||||
|
var resident = LuminaSheets.ENpcResidentSheet.GetRow(npcRowId) ??
|
||||||
|
throw new ArgumentNullException(nameof(npcRowId), $"Invalid npc row {npcRowId}");
|
||||||
|
return resident.Singular.ToDalamudString().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int? GetGearsetForJob(ClassJob job)
|
||||||
|
{
|
||||||
|
var gearsetModule = RaptureGearsetModule.Instance();
|
||||||
|
for (var i = 0; i < 100; i++)
|
||||||
|
{
|
||||||
|
var gearset = gearsetModule->EntriesSpan[i];
|
||||||
|
if (!gearset.Flags.HasFlag(RaptureGearsetModule.GearsetFlag.Exists))
|
||||||
|
continue;
|
||||||
|
if (gearset.ID != i)
|
||||||
|
continue;
|
||||||
|
if (gearset.ClassJob != job.GetClassJobIndex())
|
||||||
|
continue;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CalculateBestMacros()
|
||||||
|
{
|
||||||
|
BestMacroTokenSource?.Cancel();
|
||||||
|
BestMacroTokenSource = new();
|
||||||
|
BestMacroException = null;
|
||||||
|
BestSavedMacro = null;
|
||||||
|
HasSavedMacro = false;
|
||||||
|
BestSuggestedMacro = null;
|
||||||
|
|
||||||
|
var token = BestMacroTokenSource.Token;
|
||||||
|
var task = Task.Run(() => CalculateBestMacrosTask(token), token);
|
||||||
|
_ = task.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (token == BestMacroTokenSource.Token)
|
||||||
|
BestMacroTokenSource = null;
|
||||||
|
});
|
||||||
|
_ = task.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException);
|
||||||
|
}
|
||||||
|
catch (AggregateException e)
|
||||||
|
{
|
||||||
|
BestMacroException = e;
|
||||||
|
Log.Error(e, "Calculating macros failed");
|
||||||
|
}
|
||||||
|
}, TaskContinuationOptions.OnlyOnFaulted);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CalculateBestMacrosTask(CancellationToken token)
|
||||||
|
{
|
||||||
|
var input = new SimulationInput(CharacterStats!, RecipeData!.RecipeInfo);
|
||||||
|
var state = new SimulationState(input);
|
||||||
|
var config = Service.Configuration.SimulatorSolverConfig;
|
||||||
|
var mctsConfig = new MCTSConfig(config);
|
||||||
|
var simulator = new SimulatorNoRandom(state);
|
||||||
|
List<Macro> macros = new(Service.Configuration.Macros);
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
HasSavedMacro = macros.Count > 0;
|
||||||
|
if (HasSavedMacro)
|
||||||
|
{
|
||||||
|
var bestSaved = macros
|
||||||
|
.Select(macro =>
|
||||||
|
{
|
||||||
|
var (resp, outState, failedIdx) = simulator.ExecuteMultiple(state, macro.Actions);
|
||||||
|
outState.ActionCount = macro.Actions.Count;
|
||||||
|
var score = SimulationNode.CalculateScoreForState(outState, simulator.CompletionState, mctsConfig) ?? 0;
|
||||||
|
if (resp != ActionResponse.SimulationComplete)
|
||||||
|
{
|
||||||
|
if (failedIdx != -1)
|
||||||
|
score /= 2;
|
||||||
|
}
|
||||||
|
return (macro, outState, score);
|
||||||
|
})
|
||||||
|
.MaxBy(m => m.score);
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
BestSavedMacro = (bestSaved.macro, bestSaved.outState);
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
var solver = new Solver.Solver(config, state) { Token = token };
|
||||||
|
solver.OnLog += Log.Debug;
|
||||||
|
solver.Start();
|
||||||
|
var solution = solver.GetTask().GetAwaiter().GetResult();
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
BestSuggestedMacro = solution;
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Service.WindowSystem.RemoveWindow(this);
|
||||||
|
AxisFont?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
+356
-109
@@ -1,37 +1,37 @@
|
|||||||
using Craftimizer.Solver.Crafty;
|
using Craftimizer.Solver;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace Craftimizer.Plugin.Windows;
|
namespace Craftimizer.Plugin.Windows;
|
||||||
|
|
||||||
public class Settings : Window
|
public sealed class Settings : Window, IDisposable
|
||||||
{
|
{
|
||||||
|
private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.None;
|
||||||
|
|
||||||
private static Configuration Config => Service.Configuration;
|
private static Configuration Config => Service.Configuration;
|
||||||
|
|
||||||
private const int OptionWidth = 200;
|
private const int OptionWidth = 200;
|
||||||
private static Vector2 OptionButtonSize => new(OptionWidth, ImGuiUtils.ButtonHeight);
|
private static Vector2 OptionButtonSize => new(OptionWidth, ImGui.GetFrameHeight());
|
||||||
|
|
||||||
public const string TabGeneral = "General";
|
|
||||||
public const string TabSimulator = "Simulator";
|
|
||||||
public const string TabSynthHelper = "Synthesis Helper";
|
|
||||||
public const string TabAbout = "About";
|
|
||||||
|
|
||||||
private string? SelectedTab { get; set; }
|
private string? SelectedTab { get; set; }
|
||||||
|
|
||||||
public Settings() : base("Craftimizer Settings")
|
public Settings() : base("Craftimizer Settings", WindowFlags)
|
||||||
{
|
{
|
||||||
Service.WindowSystem.AddWindow(this);
|
Service.WindowSystem.AddWindow(this);
|
||||||
|
|
||||||
SizeConstraints = new WindowSizeConstraints()
|
SizeConstraints = new WindowSizeConstraints()
|
||||||
{
|
{
|
||||||
MinimumSize = new Vector2(400, 400),
|
MinimumSize = new(450, 400),
|
||||||
MaximumSize = new Vector2(float.MaxValue, float.MaxValue)
|
MaximumSize = new(float.PositiveInfinity)
|
||||||
};
|
};
|
||||||
Size = SizeConstraints.Value.MinimumSize;
|
|
||||||
SizeCondition = ImGuiCond.Appearing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SelectTab(string label)
|
public void SelectTab(string label)
|
||||||
@@ -39,16 +39,16 @@ public class Settings : Window
|
|||||||
SelectedTab = label;
|
SelectedTab = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool BeginTabItem(string label)
|
private ImRaii.IEndObject 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;
|
var open = true;
|
||||||
return ImGui.BeginTabItem(label, ref open, ImGuiTabItemFlags.SetSelected);
|
return ImRaii.TabItem(label, ref open, ImGuiTabItemFlags.SetSelected);
|
||||||
}
|
}
|
||||||
return ImGui.BeginTabItem(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)
|
||||||
@@ -62,26 +62,45 @@ public class Settings : Window
|
|||||||
ImGui.SetTooltip(tooltip);
|
ImGui.SetTooltip(tooltip);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DrawOption(string label, string tooltip, int val, Action<int> setter, ref bool isDirty)
|
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);
|
||||||
if (ImGui.InputInt(label, ref val))
|
var text = value.ToString();
|
||||||
|
if (ImGui.InputText(label, ref text, 8, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CharsDecimal))
|
||||||
{
|
{
|
||||||
setter(val);
|
if (T.TryParse(text, null, out var newValue))
|
||||||
|
{
|
||||||
|
newValue = T.Clamp(newValue, min, max);
|
||||||
|
if (value != newValue)
|
||||||
|
{
|
||||||
|
setter(newValue);
|
||||||
isDirty = true;
|
isDirty = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGui.SetTooltip(tooltip);
|
ImGui.SetTooltip(tooltip);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DrawOption(string label, string tooltip, float val, Action<float> setter, ref bool isDirty)
|
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) where T : struct, Enum
|
||||||
{
|
{
|
||||||
ImGui.SetNextItemWidth(OptionWidth);
|
ImGui.SetNextItemWidth(OptionWidth);
|
||||||
if (ImGui.InputFloat(label, ref val))
|
using (var combo = ImRaii.Combo(label, getName(value)))
|
||||||
{
|
{
|
||||||
setter(val);
|
if (combo)
|
||||||
|
{
|
||||||
|
foreach (var type in Enum.GetValues<T>())
|
||||||
|
{
|
||||||
|
if (ImGui.Selectable(getName(type), value.Equals(type)))
|
||||||
|
{
|
||||||
|
setter(type);
|
||||||
isDirty = true;
|
isDirty = true;
|
||||||
}
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip(getTooltip(type));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGui.SetTooltip(tooltip);
|
ImGui.SetTooltip(tooltip);
|
||||||
}
|
}
|
||||||
@@ -111,14 +130,33 @@ public class Settings : Window
|
|||||||
_ => "Unknown"
|
_ => "Unknown"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static string GetCopyTypeName(MacroCopyConfiguration.CopyType type) =>
|
||||||
|
type switch
|
||||||
|
{
|
||||||
|
MacroCopyConfiguration.CopyType.OpenWindow => "Open a Window",
|
||||||
|
MacroCopyConfiguration.CopyType.CopyToMacro => "Copy to Macros",
|
||||||
|
MacroCopyConfiguration.CopyType.CopyToClipboard => "Copy to Clipboard",
|
||||||
|
_ => "Unknown",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string GetCopyTypeTooltip(MacroCopyConfiguration.CopyType type) =>
|
||||||
|
type switch
|
||||||
|
{
|
||||||
|
MacroCopyConfiguration.CopyType.OpenWindow => "Open a dedicated window with all macros being copied.\n" +
|
||||||
|
"Copy, view, and choose at your own leisure.",
|
||||||
|
MacroCopyConfiguration.CopyType.CopyToMacro => "Copy directly to the game's macro system.",
|
||||||
|
MacroCopyConfiguration.CopyType.CopyToClipboard => "Copy to your clipboard. Macros are separated by a blank line.",
|
||||||
|
_ => "Unknown"
|
||||||
|
};
|
||||||
|
|
||||||
public override void Draw()
|
public override void Draw()
|
||||||
{
|
{
|
||||||
if (ImGui.BeginTabBar("settingsTabBar"))
|
if (ImGui.BeginTabBar("settingsTabBar"))
|
||||||
{
|
{
|
||||||
DrawTabGeneral();
|
DrawTabGeneral();
|
||||||
DrawTabSimulator();
|
DrawTabSimulator();
|
||||||
if (Config.EnableSynthHelper)
|
//if (Config.EnableSynthHelper)
|
||||||
DrawTabSynthHelper();
|
// DrawTabSynthHelper();
|
||||||
DrawTabAbout();
|
DrawTabAbout();
|
||||||
|
|
||||||
ImGui.EndTabBar();
|
ImGui.EndTabBar();
|
||||||
@@ -127,22 +165,18 @@ public class Settings : Window
|
|||||||
|
|
||||||
private void DrawTabGeneral()
|
private void DrawTabGeneral()
|
||||||
{
|
{
|
||||||
if (!BeginTabItem("General"))
|
using var tab = TabItem("General");
|
||||||
|
if (!tab)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
|
||||||
var isDirty = false;
|
var isDirty = false;
|
||||||
|
|
||||||
DrawOption(
|
|
||||||
"Override Uncraftability Warning",
|
|
||||||
"Allow simulation for crafts that otherwise wouldn't\n" +
|
|
||||||
"be able to be crafted with your current gear",
|
|
||||||
Config.OverrideUncraftability,
|
|
||||||
v => Config.OverrideUncraftability = v,
|
|
||||||
ref isDirty
|
|
||||||
);
|
|
||||||
|
|
||||||
|
using (var g = ImRaii.Group())
|
||||||
|
{
|
||||||
|
using var d = ImRaii.Disabled();
|
||||||
DrawOption(
|
DrawOption(
|
||||||
"Enable Synthesis Helper",
|
"Enable Synthesis Helper",
|
||||||
"Adds a helper next to your synthesis window to help solve for the best craft.\n" +
|
"Adds a helper next to your synthesis window to help solve for the best craft.\n" +
|
||||||
@@ -152,11 +186,192 @@ public class Settings : Window
|
|||||||
v => Config.EnableSynthHelper = v,
|
v => Config.EnableSynthHelper = v,
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Disabled temporarily for testing");
|
||||||
|
|
||||||
|
DrawOption(
|
||||||
|
"Show Only One Macro Stat",
|
||||||
|
"Only one stat will be shown for a macro. If a craft will be finished, quality\n" +
|
||||||
|
"is shown. Otherwise, progress is shown. Durability and remaining CP will be\n" +
|
||||||
|
"hidden.",
|
||||||
|
Config.ShowOptimalMacroStat,
|
||||||
|
v => Config.ShowOptimalMacroStat = v,
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
|
||||||
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
|
||||||
|
using (var panel = ImGuiUtils.GroupPanel("Copying Settings", -1, out _))
|
||||||
|
{
|
||||||
|
DrawOption(
|
||||||
|
"Macro Copy Method",
|
||||||
|
"The method to copy a macro with.",
|
||||||
|
GetCopyTypeName,
|
||||||
|
GetCopyTypeTooltip,
|
||||||
|
Config.MacroCopy.Type,
|
||||||
|
v => Config.MacroCopy.Type = v,
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Config.MacroCopy.Type == MacroCopyConfiguration.CopyType.CopyToMacro)
|
||||||
|
{
|
||||||
|
DrawOption(
|
||||||
|
"Copy Downwards",
|
||||||
|
"Copy subsequent macros downward (#1 -> #11) instead of to the right.",
|
||||||
|
Config.MacroCopy.CopyDown,
|
||||||
|
v => Config.MacroCopy.CopyDown = v,
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
|
||||||
|
DrawOption(
|
||||||
|
"Copy to Shared Macros",
|
||||||
|
"Copy to the shared macros tab. Leaving this unchecked copies to the\n" +
|
||||||
|
"individual tab.",
|
||||||
|
Config.MacroCopy.SharedMacro,
|
||||||
|
v => Config.MacroCopy.SharedMacro = v,
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
|
||||||
|
DrawOption(
|
||||||
|
"Macro Number",
|
||||||
|
"The # of the macro to being copying to. Subsequent macros will be\n" +
|
||||||
|
"copied relative to this macro.",
|
||||||
|
Config.MacroCopy.StartMacroIdx,
|
||||||
|
0, 99,
|
||||||
|
v => Config.MacroCopy.StartMacroIdx = v,
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
|
||||||
|
DrawOption(
|
||||||
|
"Max Macro Copy Count",
|
||||||
|
"The maximum number of macros to be copied. Any more and a window is\n" +
|
||||||
|
"displayed with the rest of them.",
|
||||||
|
Config.MacroCopy.MaxMacroCount,
|
||||||
|
1, 99,
|
||||||
|
v => Config.MacroCopy.MaxMacroCount = v,
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawOption(
|
||||||
|
"Use Macro Chain's /nextmacro",
|
||||||
|
"Replaces the last step with /nextmacro to run the next macro\n" +
|
||||||
|
"automatically. Overrides Add End Notification except for the\n" +
|
||||||
|
"last macro.",
|
||||||
|
Config.MacroCopy.UseNextMacro,
|
||||||
|
v => Config.MacroCopy.UseNextMacro = v,
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Config.MacroCopy.UseNextMacro && !Service.PluginInterface.InstalledPlugins.Any(p => p.IsLoaded && string.Equals(p.InternalName, "MacroChain", StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
ImGui.SameLine();
|
||||||
|
using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange))
|
||||||
|
{
|
||||||
|
using var font = ImRaii.PushFont(UiBuilder.IconFont);
|
||||||
|
ImGui.Text(FontAwesomeIcon.ExclamationCircle.ToIconString());
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Macro Chain is not installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawOption(
|
||||||
|
"Add Macro Lock",
|
||||||
|
"Adds /mlock to the beginning of every macro. Prevents other\n" +
|
||||||
|
"macros from being run.",
|
||||||
|
Config.MacroCopy.UseMacroLock,
|
||||||
|
v => Config.MacroCopy.UseMacroLock = v,
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
|
||||||
|
DrawOption(
|
||||||
|
"Add Notification",
|
||||||
|
"Replaces the last step of every macro with a /echo notification.",
|
||||||
|
Config.MacroCopy.AddNotification,
|
||||||
|
v => Config.MacroCopy.AddNotification = v,
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Config.MacroCopy.AddNotification)
|
||||||
|
{
|
||||||
|
var isForceUseful = Config.MacroCopy.Type == MacroCopyConfiguration.CopyType.CopyToMacro || !Config.MacroCopy.CombineMacro;
|
||||||
|
using (var d = ImRaii.Disabled(!isForceUseful))
|
||||||
|
{
|
||||||
|
DrawOption(
|
||||||
|
"Force Notification",
|
||||||
|
"Prioritize always having a notification sound at the end of\n" +
|
||||||
|
"every macro. Keeping this off prevents macros with only 1 action.",
|
||||||
|
Config.MacroCopy.ForceNotification,
|
||||||
|
v => Config.MacroCopy.ForceNotification = v,
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!isForceUseful && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||||
|
ImGui.SetTooltip("Only useful when Combine Macro is off");
|
||||||
|
|
||||||
|
DrawOption(
|
||||||
|
"Add Notification Sound",
|
||||||
|
"Adds a sound to the end of every macro.",
|
||||||
|
Config.MacroCopy.AddNotificationSound,
|
||||||
|
v => Config.MacroCopy.AddNotificationSound = v,
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Config.MacroCopy.AddNotificationSound)
|
||||||
|
{
|
||||||
|
DrawOption(
|
||||||
|
"Intermediate Notification Sound",
|
||||||
|
"Ending notification sound for an intermediary macro.\n" +
|
||||||
|
"Uses <se.#>",
|
||||||
|
Config.MacroCopy.IntermediateNotificationSound,
|
||||||
|
1, 16,
|
||||||
|
v =>
|
||||||
|
{
|
||||||
|
Config.MacroCopy.IntermediateNotificationSound = v;
|
||||||
|
UIModule.PlayChatSoundEffect((uint)v);
|
||||||
|
},
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
|
||||||
|
DrawOption(
|
||||||
|
"Final Notification Sound",
|
||||||
|
"Ending notification sound for the final macro.\n" +
|
||||||
|
"Uses <se.#>",
|
||||||
|
Config.MacroCopy.EndNotificationSound,
|
||||||
|
1, 16,
|
||||||
|
v =>
|
||||||
|
{
|
||||||
|
Config.MacroCopy.EndNotificationSound = v;
|
||||||
|
UIModule.PlayChatSoundEffect((uint)v);
|
||||||
|
},
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Config.MacroCopy.Type != MacroCopyConfiguration.CopyType.CopyToMacro)
|
||||||
|
{
|
||||||
|
DrawOption(
|
||||||
|
"Remove Wait Times",
|
||||||
|
"Remove <wait.#> at the end of every action. Useful for SomethingNeedDoing.",
|
||||||
|
Config.MacroCopy.RemoveWaitTimes,
|
||||||
|
v => Config.MacroCopy.RemoveWaitTimes = v,
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
|
||||||
|
DrawOption(
|
||||||
|
"Combine Macro",
|
||||||
|
"Doesn't split the macro into smaller macros. Useful for SomethingNeedDoing.",
|
||||||
|
Config.MacroCopy.CombineMacro,
|
||||||
|
v => Config.MacroCopy.CombineMacro = v,
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isDirty)
|
if (isDirty)
|
||||||
Config.Save();
|
Config.Save();
|
||||||
|
|
||||||
ImGui.EndTabItem();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DrawSolverConfig(ref SolverConfig configRef, SolverConfig defaultConfig, out bool isDirty)
|
private static void DrawSolverConfig(ref SolverConfig configRef, SolverConfig defaultConfig, out bool isDirty)
|
||||||
@@ -165,35 +380,25 @@ public class Settings : Window
|
|||||||
|
|
||||||
var config = configRef;
|
var config = configRef;
|
||||||
|
|
||||||
ImGuiUtils.BeginGroupPanel("General");
|
using (var panel = ImGuiUtils.GroupPanel("General", -1, out _))
|
||||||
|
{
|
||||||
if (ImGui.Button("Reset to Default", OptionButtonSize))
|
if (ImGui.Button("Reset to Default", OptionButtonSize))
|
||||||
{
|
{
|
||||||
config = defaultConfig;
|
config = defaultConfig;
|
||||||
isDirty = true;
|
isDirty = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui.SetNextItemWidth(OptionWidth);
|
DrawOption(
|
||||||
if (ImGui.BeginCombo("Algorithm", GetAlgorithmName(config.Algorithm)))
|
"Algorithm",
|
||||||
{
|
|
||||||
foreach (var alg in Enum.GetValues<SolverAlgorithm>())
|
|
||||||
{
|
|
||||||
if (ImGui.Selectable(GetAlgorithmName(alg), config.Algorithm == alg))
|
|
||||||
{
|
|
||||||
config = config with { Algorithm = alg };
|
|
||||||
isDirty = true;
|
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip(GetAlgorithmTooltip(alg));
|
|
||||||
}
|
|
||||||
ImGui.EndCombo();
|
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip(
|
|
||||||
"The algorithm to use when solving for a macro. Different\n" +
|
"The algorithm to use when solving for a macro. Different\n" +
|
||||||
"algorithms provide different pros and cons for using them.\n" +
|
"algorithms provide different pros and cons for using them.\n" +
|
||||||
"By far, the Stepwise Furcated algorithm provides the best\n" +
|
"By far, the Stepwise Furcated algorithm provides the best\n" +
|
||||||
"results, especially for very difficult crafts."
|
"results, especially for very difficult crafts.",
|
||||||
|
GetAlgorithmName,
|
||||||
|
GetAlgorithmTooltip,
|
||||||
|
config.Algorithm,
|
||||||
|
v => config = config with { Algorithm = v },
|
||||||
|
ref isDirty
|
||||||
);
|
);
|
||||||
|
|
||||||
DrawOption(
|
DrawOption(
|
||||||
@@ -203,6 +408,8 @@ public class Settings : Window
|
|||||||
"also may decrease variance, so other values should be tweaked\n" +
|
"also may decrease variance, so other values should be tweaked\n" +
|
||||||
"as necessary to get a more favorable outcome.",
|
"as necessary to get a more favorable outcome.",
|
||||||
config.Iterations,
|
config.Iterations,
|
||||||
|
1000,
|
||||||
|
500000,
|
||||||
v => config = config with { Iterations = v },
|
v => config = config with { Iterations = v },
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
@@ -215,6 +422,8 @@ public class Settings : Window
|
|||||||
"won't learn much per iteration; too high and it will waste time\n" +
|
"won't learn much per iteration; too high and it will waste time\n" +
|
||||||
"on useless extra steps.",
|
"on useless extra steps.",
|
||||||
config.MaxStepCount,
|
config.MaxStepCount,
|
||||||
|
1,
|
||||||
|
100,
|
||||||
v => config = config with { MaxStepCount = v },
|
v => config = config with { MaxStepCount = v },
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
@@ -225,6 +434,8 @@ public class Settings : Window
|
|||||||
"possibly good paths. If this value is too high,\n" +
|
"possibly good paths. If this value is too high,\n" +
|
||||||
"moves will mostly be decided at random.",
|
"moves will mostly be decided at random.",
|
||||||
config.ExplorationConstant,
|
config.ExplorationConstant,
|
||||||
|
0,
|
||||||
|
10,
|
||||||
v => config = config with { ExplorationConstant = v },
|
v => config = config with { ExplorationConstant = v },
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
@@ -236,11 +447,29 @@ public class Settings : Window
|
|||||||
"actions will be chosen based on their average outcome, whereas\n" +
|
"actions will be chosen based on their average outcome, whereas\n" +
|
||||||
"1 uses their best outcome achieved so far.",
|
"1 uses their best outcome achieved so far.",
|
||||||
config.MaxScoreWeightingConstant,
|
config.MaxScoreWeightingConstant,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
v => config = config with { MaxScoreWeightingConstant = v },
|
v => config = config with { MaxScoreWeightingConstant = v },
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
|
|
||||||
ImGui.BeginDisabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated));
|
using (var d = ImRaii.Disabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated)))
|
||||||
|
DrawOption(
|
||||||
|
"Max Core Count",
|
||||||
|
"The number of cores to use when solving. You should use as many\n" +
|
||||||
|
"as you can. If it's too high, it will have an effect on your gameplay\n" +
|
||||||
|
$"experience. A good estimate would be 1 or 2 cores less than your\n" +
|
||||||
|
$"system (FYI, you have {Environment.ProcessorCount} cores), but make sure to accomodate\n" +
|
||||||
|
$"for any other tasks you have in the background, if you have any.\n" +
|
||||||
|
"(Only used in the Forked and Furcated algorithms)",
|
||||||
|
config.MaxThreadCount,
|
||||||
|
1,
|
||||||
|
Environment.ProcessorCount,
|
||||||
|
v => config = config with { MaxThreadCount = v },
|
||||||
|
ref isDirty
|
||||||
|
);
|
||||||
|
|
||||||
|
using (var d = ImRaii.Disabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated)))
|
||||||
DrawOption(
|
DrawOption(
|
||||||
"Fork Count",
|
"Fork Count",
|
||||||
"Split the number of iterations across different solvers. In general,\n" +
|
"Split the number of iterations across different solvers. In general,\n" +
|
||||||
@@ -251,12 +480,13 @@ public class Settings : Window
|
|||||||
"to the exploration constant.\n" +
|
"to the exploration constant.\n" +
|
||||||
"(Only used in the Forked and Furcated algorithms)",
|
"(Only used in the Forked and Furcated algorithms)",
|
||||||
config.ForkCount,
|
config.ForkCount,
|
||||||
|
1,
|
||||||
|
500,
|
||||||
v => config = config with { ForkCount = v },
|
v => config = config with { ForkCount = v },
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
ImGui.EndDisabled();
|
|
||||||
|
|
||||||
ImGui.BeginDisabled(config.Algorithm is not SolverAlgorithm.StepwiseFurcated);
|
using (var d = ImRaii.Disabled(config.Algorithm is not SolverAlgorithm.StepwiseFurcated))
|
||||||
DrawOption(
|
DrawOption(
|
||||||
"Furcated Action Count",
|
"Furcated Action Count",
|
||||||
"On every craft step, pick this many top solutions and use them as\n" +
|
"On every craft step, pick this many top solutions and use them as\n" +
|
||||||
@@ -264,21 +494,23 @@ public class Settings : Window
|
|||||||
"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 Furcated algorithm)",
|
"(Only used in the Stepwise Furcated algorithm)",
|
||||||
config.FurcatedActionCount,
|
config.FurcatedActionCount,
|
||||||
|
1,
|
||||||
|
500,
|
||||||
v => config = config with { FurcatedActionCount = v },
|
v => config = config with { FurcatedActionCount = v },
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
ImGui.EndDisabled();
|
}
|
||||||
|
|
||||||
ImGuiUtils.EndGroupPanel();
|
|
||||||
|
|
||||||
ImGuiUtils.BeginGroupPanel("Advanced");
|
|
||||||
|
|
||||||
|
using (var panel = ImGuiUtils.GroupPanel("Advanced", -1, out _))
|
||||||
|
{
|
||||||
DrawOption(
|
DrawOption(
|
||||||
"Score Storage Threshold",
|
"Score Storage Threshold",
|
||||||
"If a craft achieves this certain arbitrary score, the solver will\n" +
|
"If a craft achieves this certain arbitrary score, the solver will\n" +
|
||||||
"throw away all other possible combinations in favor of that one.\n" +
|
"throw away all other possible combinations in favor of that one.\n" +
|
||||||
"Only change this value if you absolutely know what you're doing.",
|
"Only change this value if you absolutely know what you're doing.",
|
||||||
config.ScoreStorageThreshold,
|
config.ScoreStorageThreshold,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
v => config = config with { ScoreStorageThreshold = v },
|
v => config = config with { ScoreStorageThreshold = v },
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
@@ -289,6 +521,8 @@ public class Settings : Window
|
|||||||
"Decreasing this value can have unintended side effects. Only change\n" +
|
"Decreasing this value can have unintended side effects. Only change\n" +
|
||||||
"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,
|
||||||
|
50,
|
||||||
v => config = config with { MaxRolloutStepCount = v },
|
v => config = config with { MaxRolloutStepCount = v },
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
@@ -302,42 +536,50 @@ public class Settings : Window
|
|||||||
v => config = config with { StrictActions = v },
|
v => config = config with { StrictActions = v },
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ImGuiUtils.EndGroupPanel();
|
using (var panel = ImGuiUtils.GroupPanel("Score Weights (Advanced)", -1, out _))
|
||||||
|
{
|
||||||
ImGuiUtils.BeginGroupPanel("Score Weights (Advanced)");
|
|
||||||
ImGui.TextWrapped("All values should add up to 1. Otherwise, the Score Storage Threshold should be changed.");
|
ImGui.TextWrapped("All values should add up to 1. Otherwise, the Score Storage Threshold should be changed.");
|
||||||
ImGuiHelpers.ScaledDummy(10);
|
ImGuiHelpers.ScaledDummy(10);
|
||||||
|
|
||||||
DrawOption(
|
DrawOption(
|
||||||
"Progress",
|
"Progress",
|
||||||
"Amount of weight to give to the craft's progress.",
|
"Amount of weight to give to the craft's progress.",
|
||||||
config.ScoreProgressBonus,
|
config.ScoreProgress,
|
||||||
v => config = config with { ScoreProgressBonus = v },
|
0,
|
||||||
|
1,
|
||||||
|
v => config = config with { ScoreProgress = v },
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
|
|
||||||
DrawOption(
|
DrawOption(
|
||||||
"Quality",
|
"Quality",
|
||||||
"Amount of weight to give to the craft's quality.",
|
"Amount of weight to give to the craft's quality.",
|
||||||
config.ScoreQualityBonus,
|
config.ScoreQuality,
|
||||||
v => config = config with { ScoreQualityBonus = v },
|
0,
|
||||||
|
1,
|
||||||
|
v => config = config with { ScoreQuality = v },
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
|
|
||||||
DrawOption(
|
DrawOption(
|
||||||
"Durability",
|
"Durability",
|
||||||
"Amount of weight to give to the craft's remaining durability.",
|
"Amount of weight to give to the craft's remaining durability.",
|
||||||
config.ScoreDurabilityBonus,
|
config.ScoreDurability,
|
||||||
v => config = config with { ScoreDurabilityBonus = v },
|
0,
|
||||||
|
1,
|
||||||
|
v => config = config with { ScoreDurability = v },
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
|
|
||||||
DrawOption(
|
DrawOption(
|
||||||
"CP",
|
"CP",
|
||||||
"Amount of weight to give to the craft's remaining CP.",
|
"Amount of weight to give to the craft's remaining CP.",
|
||||||
config.ScoreCPBonus,
|
config.ScoreCP,
|
||||||
v => config = config with { ScoreCPBonus = v },
|
0,
|
||||||
|
1,
|
||||||
|
v => config = config with { ScoreCP = v },
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -345,32 +587,33 @@ public class Settings : Window
|
|||||||
"Steps",
|
"Steps",
|
||||||
"Amount of weight to give to the craft's number of steps. The lower\n" +
|
"Amount of weight to give to the craft's number of steps. The lower\n" +
|
||||||
"the step count, the higher the score.",
|
"the step count, the higher the score.",
|
||||||
config.ScoreFewerStepsBonus,
|
config.ScoreSteps,
|
||||||
v => config = config with { ScoreFewerStepsBonus = v },
|
0,
|
||||||
|
1,
|
||||||
|
v => config = config with { ScoreSteps = v },
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
|
|
||||||
if (ImGui.Button("Normalize Weights", OptionButtonSize))
|
if (ImGui.Button("Normalize Weights", OptionButtonSize))
|
||||||
{
|
{
|
||||||
var total = config.ScoreProgressBonus +
|
var total = config.ScoreProgress +
|
||||||
config.ScoreQualityBonus +
|
config.ScoreQuality +
|
||||||
config.ScoreDurabilityBonus +
|
config.ScoreDurability +
|
||||||
config.ScoreCPBonus +
|
config.ScoreCP +
|
||||||
config.ScoreFewerStepsBonus;
|
config.ScoreSteps;
|
||||||
config = config with
|
config = config with
|
||||||
{
|
{
|
||||||
ScoreProgressBonus = config.ScoreProgressBonus / total,
|
ScoreProgress = config.ScoreProgress / total,
|
||||||
ScoreQualityBonus = config.ScoreQualityBonus / total,
|
ScoreQuality = config.ScoreQuality / total,
|
||||||
ScoreDurabilityBonus = config.ScoreDurabilityBonus / total,
|
ScoreDurability = config.ScoreDurability / total,
|
||||||
ScoreCPBonus = config.ScoreCPBonus / total,
|
ScoreCP = config.ScoreCP / total,
|
||||||
ScoreFewerStepsBonus = config.ScoreFewerStepsBonus / total
|
ScoreSteps = config.ScoreSteps / total
|
||||||
};
|
};
|
||||||
isDirty = true;
|
isDirty = true;
|
||||||
}
|
}
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGui.SetTooltip("Normalize all weights to sum up to 1");
|
ImGui.SetTooltip("Normalize all weights to sum up to 1");
|
||||||
|
}
|
||||||
ImGuiUtils.EndGroupPanel();
|
|
||||||
|
|
||||||
if (isDirty)
|
if (isDirty)
|
||||||
configRef = config;
|
configRef = config;
|
||||||
@@ -378,22 +621,17 @@ public class Settings : Window
|
|||||||
|
|
||||||
private void DrawTabSimulator()
|
private void DrawTabSimulator()
|
||||||
{
|
{
|
||||||
if (!BeginTabItem("Simulator"))
|
using var tab = TabItem("Simulator");
|
||||||
|
if (!tab)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
|
|
||||||
var isDirty = false;
|
var isDirty = false;
|
||||||
|
|
||||||
DrawOption(
|
using (var g = ImRaii.Group())
|
||||||
"Show Only Learned Actions",
|
{
|
||||||
"Don't show crafting actions that haven't been\n" +
|
using var d = ImRaii.Disabled();
|
||||||
"learned yet with your current job on the simulator sidebar",
|
|
||||||
Config.HideUnlearnedActions,
|
|
||||||
v => Config.HideUnlearnedActions = v,
|
|
||||||
ref isDirty
|
|
||||||
);
|
|
||||||
|
|
||||||
DrawOption(
|
DrawOption(
|
||||||
"Condition Randomness",
|
"Condition Randomness",
|
||||||
"Allows the simulator condition to fluctuate randomly like a real craft.\n" +
|
"Allows the simulator condition to fluctuate randomly like a real craft.\n" +
|
||||||
@@ -402,6 +640,9 @@ public class Settings : Window
|
|||||||
v => Config.ConditionRandomness = v,
|
v => Config.ConditionRandomness = v,
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
if (ImGui.IsItemHovered())
|
||||||
|
ImGui.SetTooltip("Disabled temporarily for testing");
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
ImGui.Separator();
|
ImGui.Separator();
|
||||||
@@ -417,13 +658,12 @@ public class Settings : Window
|
|||||||
|
|
||||||
if (isDirty)
|
if (isDirty)
|
||||||
Config.Save();
|
Config.Save();
|
||||||
|
|
||||||
ImGui.EndTabItem();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawTabSynthHelper()
|
private void DrawTabSynthHelper()
|
||||||
{
|
{
|
||||||
if (!BeginTabItem("Synthesis Helper"))
|
using var tab = TabItem("Synthesis Helper");
|
||||||
|
if (!tab)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
@@ -434,6 +674,8 @@ public class Settings : Window
|
|||||||
"Step Count",
|
"Step Count",
|
||||||
"The number of future actions to solve for during an in-game craft.",
|
"The number of future actions to solve for during an in-game craft.",
|
||||||
Config.SynthHelperStepCount,
|
Config.SynthHelperStepCount,
|
||||||
|
1,
|
||||||
|
100,
|
||||||
v => Config.SynthHelperStepCount = v,
|
v => Config.SynthHelperStepCount = v,
|
||||||
ref isDirty
|
ref isDirty
|
||||||
);
|
);
|
||||||
@@ -452,13 +694,12 @@ public class Settings : Window
|
|||||||
|
|
||||||
if (isDirty)
|
if (isDirty)
|
||||||
Config.Save();
|
Config.Save();
|
||||||
|
|
||||||
ImGui.EndTabItem();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DrawTabAbout()
|
private void DrawTabAbout()
|
||||||
{
|
{
|
||||||
if (!BeginTabItem("About"))
|
using var tab = TabItem("About");
|
||||||
|
if (!tab)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
ImGuiHelpers.ScaledDummy(5);
|
||||||
@@ -466,28 +707,34 @@ public class Settings : Window
|
|||||||
var plugin = Service.Plugin;
|
var plugin = Service.Plugin;
|
||||||
var icon = plugin.Icon;
|
var icon = plugin.Icon;
|
||||||
|
|
||||||
ImGui.BeginTable("settingsAboutTable", 2);
|
using (var table = ImRaii.Table("settingsAboutTable", 2))
|
||||||
|
{
|
||||||
|
if (table)
|
||||||
|
{
|
||||||
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, icon.Width);
|
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, icon.Width);
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.Image(icon.ImGuiHandle, new(icon.Width, icon.Height));
|
ImGui.Image(icon.ImGuiHandle, new(icon.Width, icon.Height));
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.Text($"{plugin.Name} v{plugin.Version} {plugin.Configuration}");
|
ImGui.Text($"Craftimizer v{plugin.Version} {plugin.BuildConfiguration}");
|
||||||
ImGui.Text($"By {plugin.Author} (");
|
ImGui.Text($"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");
|
||||||
ImGui.SameLine(0, 0);
|
ImGui.SameLine(0, 0);
|
||||||
ImGui.Text(")");
|
ImGui.Text(")");
|
||||||
|
}
|
||||||
ImGui.EndTable();
|
}
|
||||||
|
|
||||||
ImGui.Text("Credit to altosock's ");
|
ImGui.Text("Credit to altosock's ");
|
||||||
ImGui.SameLine(0, 0);
|
ImGui.SameLine(0, 0);
|
||||||
ImGuiUtils.Hyperlink("Craftingway", "https://craftingway.app");
|
ImGuiUtils.Hyperlink("Craftingway", "https://craftingway.app");
|
||||||
ImGui.SameLine(0, 0);
|
ImGui.SameLine(0, 0);
|
||||||
ImGui.Text(" for the original solver algorithm");
|
ImGui.Text(" for the original solver algorithm");
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.EndTabItem();
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Service.WindowSystem.RemoveWindow(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
using Craftimizer.Simulator;
|
|
||||||
using Craftimizer.Simulator.Actions;
|
|
||||||
using Dalamud.Interface.Windowing;
|
|
||||||
using ImGuiNET;
|
|
||||||
using Lumina.Excel.GeneratedSheets;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using ClassJob = Craftimizer.Simulator.ClassJob;
|
|
||||||
|
|
||||||
namespace Craftimizer.Plugin.Windows;
|
|
||||||
|
|
||||||
public sealed partial class Simulator : Window, IDisposable
|
|
||||||
{
|
|
||||||
private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.AlwaysAutoResize;
|
|
||||||
|
|
||||||
private static Configuration Config => Service.Configuration;
|
|
||||||
|
|
||||||
private Item Item { get; }
|
|
||||||
private bool IsExpert { get; }
|
|
||||||
private SimulationInput Input { get; }
|
|
||||||
private ClassJob ClassJob { get; }
|
|
||||||
private Macro? Macro { get; set; }
|
|
||||||
private string MacroName { get; set; }
|
|
||||||
// State is the state of the simulation *after* its corresponding action is executed.
|
|
||||||
private List<(ActionType Action, string Tooltip, ActionResponse Response, SimulationState State)> Actions { get; }
|
|
||||||
private Craftimizer.Simulator.Simulator Sim { get; set; }
|
|
||||||
|
|
||||||
private SimulationState LatestState => Actions.Count == 0 ? new(Input) : Actions[^1].State;
|
|
||||||
|
|
||||||
// Simulator is set by ResetSimulator()
|
|
||||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
|
||||||
public Simulator(Item item, bool isExpert, SimulationInput input, ClassJob classJob, Macro? macro) : base("Craftimizer Simulator", WindowFlags)
|
|
||||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
|
||||||
{
|
|
||||||
Service.WindowSystem.AddWindow(this);
|
|
||||||
|
|
||||||
Item = item;
|
|
||||||
IsExpert = isExpert;
|
|
||||||
Input = input;
|
|
||||||
ClassJob = classJob;
|
|
||||||
Macro = macro;
|
|
||||||
MacroName = Macro?.Name ?? $"Macro {Config.Macros.Count + 1}";
|
|
||||||
Actions = new();
|
|
||||||
ResetSimulator();
|
|
||||||
|
|
||||||
IsOpen = true;
|
|
||||||
|
|
||||||
CollapsedCondition = ImGuiCond.Appearing;
|
|
||||||
Collapsed = false;
|
|
||||||
|
|
||||||
SizeCondition = ImGuiCond.Appearing;
|
|
||||||
Size = SizeConstraints?.MinimumSize ?? new(10);
|
|
||||||
|
|
||||||
if (Macro != null)
|
|
||||||
foreach (var action in Macro.Actions)
|
|
||||||
AppendAction(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ResetSimulator()
|
|
||||||
{
|
|
||||||
Sim = Config.CreateSimulator(LatestState);
|
|
||||||
ReexecuteAllActions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
using Craftimizer.Simulator.Actions;
|
|
||||||
using Dalamud.Interface.Windowing;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Craftimizer.Plugin.Windows;
|
|
||||||
|
|
||||||
public sealed partial class Simulator : Window, IDisposable
|
|
||||||
{
|
|
||||||
private void AppendAction(ActionType action)
|
|
||||||
{
|
|
||||||
OnActionsChanged();
|
|
||||||
|
|
||||||
AppendGeneratedAction(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AppendGeneratedAction(ActionType action)
|
|
||||||
{
|
|
||||||
var actionBase = action.Base();
|
|
||||||
if (actionBase is BaseComboAction comboActionBase)
|
|
||||||
{
|
|
||||||
AppendGeneratedAction(comboActionBase.ActionTypeA);
|
|
||||||
AppendGeneratedAction(comboActionBase.ActionTypeB);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var tooltip = actionBase.GetTooltip(Sim, false);
|
|
||||||
var (response, state) = Sim.Execute(LatestState, action);
|
|
||||||
Actions.Add((action, tooltip, response, state));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RemoveAction(int actionIndex)
|
|
||||||
{
|
|
||||||
OnActionsChanged();
|
|
||||||
|
|
||||||
// Remove action
|
|
||||||
Actions.RemoveAt(actionIndex);
|
|
||||||
|
|
||||||
// Take note of all actions afterwards
|
|
||||||
Span<ActionType> succeedingActions = stackalloc ActionType[Actions.Count - actionIndex];
|
|
||||||
for (var i = 0; i < succeedingActions.Length; i++)
|
|
||||||
succeedingActions[i] = Actions[i + actionIndex].Action;
|
|
||||||
|
|
||||||
// Remove all future actions
|
|
||||||
Actions.RemoveRange(actionIndex, succeedingActions.Length);
|
|
||||||
|
|
||||||
// Re-execute all future actions
|
|
||||||
foreach (var action in succeedingActions)
|
|
||||||
AppendAction(action);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InsertAction(int actionIndex, ActionType action)
|
|
||||||
{
|
|
||||||
OnActionsChanged();
|
|
||||||
|
|
||||||
// Take note of all actions afterwards
|
|
||||||
Span<ActionType> succeedingActions = stackalloc ActionType[Actions.Count - actionIndex];
|
|
||||||
for (var i = 0; i < succeedingActions.Length; i++)
|
|
||||||
succeedingActions[i] = Actions[i + actionIndex].Action;
|
|
||||||
|
|
||||||
// Remove all future actions
|
|
||||||
Actions.RemoveRange(actionIndex, succeedingActions.Length);
|
|
||||||
|
|
||||||
// Execute new action
|
|
||||||
AppendAction(action);
|
|
||||||
|
|
||||||
// Re-execute all future actions
|
|
||||||
foreach (var succeededAction in succeedingActions)
|
|
||||||
AppendAction(succeededAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearAllActions()
|
|
||||||
{
|
|
||||||
OnActionsChanged();
|
|
||||||
|
|
||||||
Actions.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReexecuteAllActions()
|
|
||||||
{
|
|
||||||
Span<ActionType> actions = stackalloc ActionType[Actions.Count];
|
|
||||||
for (var i = 0; i < actions.Length; i++)
|
|
||||||
actions[i] = Actions[i].Action;
|
|
||||||
|
|
||||||
Actions.Clear();
|
|
||||||
foreach (var action in actions)
|
|
||||||
AppendAction(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,439 +0,0 @@
|
|||||||
using Craftimizer.Simulator;
|
|
||||||
using Craftimizer.Simulator.Actions;
|
|
||||||
using Dalamud.Game.Text;
|
|
||||||
using Dalamud.Interface;
|
|
||||||
using Dalamud.Interface.Components;
|
|
||||||
using Dalamud.Interface.Windowing;
|
|
||||||
using Dalamud.Utility;
|
|
||||||
using ImGuiNET;
|
|
||||||
using ImGuiScene;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace Craftimizer.Plugin.Windows;
|
|
||||||
|
|
||||||
public sealed partial class Simulator : Window, IDisposable
|
|
||||||
{
|
|
||||||
private const int ActionColumnSize = 260;
|
|
||||||
|
|
||||||
private static readonly Vector2 ProgressBarSize = new(200, 20);
|
|
||||||
private static readonly Vector2 DurabilityBarSize = new(100, 20);
|
|
||||||
private static readonly Vector2 ConditionBarSize = new(20, 20);
|
|
||||||
private static readonly Vector2 ProgressBarSizeOld = new(200, 20);
|
|
||||||
public static readonly Vector2 TooltipProgressBarSize = new(100, 5);
|
|
||||||
|
|
||||||
private static readonly Vector4 ProgressColor = new(0.44f, 0.65f, 0.18f, 1f);
|
|
||||||
private static readonly Vector4 QualityColor = new(0.26f, 0.71f, 0.69f, 1f);
|
|
||||||
private static readonly Vector4 DurabilityColor = new(0.13f, 0.52f, 0.93f, 1f);
|
|
||||||
private static readonly Vector4 HQColor = new(0.592f, 0.863f, 0.376f, 1f);
|
|
||||||
private static readonly Vector4 CPColor = new(0.63f, 0.37f, 0.75f, 1f);
|
|
||||||
|
|
||||||
private static readonly Vector4 BadActionImageTint = new(1f, .5f, .5f, 1f);
|
|
||||||
private static readonly Vector4 BadActionImageColor = new(1f, .3f, .3f, 1f);
|
|
||||||
|
|
||||||
private static readonly Vector4 BadActionTextColor = new(1f, .2f, .2f, 1f);
|
|
||||||
|
|
||||||
private static readonly (ActionCategory Category, ActionType[] Actions)[] SortedActions;
|
|
||||||
|
|
||||||
static Simulator()
|
|
||||||
{
|
|
||||||
SortedActions = Enum.GetValues<ActionType>()
|
|
||||||
.Where(a => a.Category() != ActionCategory.Combo)
|
|
||||||
.GroupBy(a => a.Category())
|
|
||||||
.Select(g => (g.Key, g.OrderBy(a => a.Level()).ToArray()))
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Draw()
|
|
||||||
{
|
|
||||||
while (SolverActionQueue.TryDequeue(out var poppedAction))
|
|
||||||
AppendGeneratedAction(poppedAction);
|
|
||||||
|
|
||||||
ImGui.BeginTable("simulatorWindow", 2, ImGuiTableFlags.BordersInnerV);
|
|
||||||
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, ActionColumnSize);
|
|
||||||
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthStretch);
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
DrawActions();
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
DrawSimulation();
|
|
||||||
ImGui.EndTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawActions()
|
|
||||||
{
|
|
||||||
var hideUnlearnedActions = Config.HideUnlearnedActions;
|
|
||||||
if (ImGui.Checkbox("Show only learned actions", ref hideUnlearnedActions))
|
|
||||||
{
|
|
||||||
Config.HideUnlearnedActions = hideUnlearnedActions;
|
|
||||||
Config.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
Sim.SetState(LatestState);
|
|
||||||
|
|
||||||
var actionSize = new Vector2((ActionColumnSize / 5) - ImGui.GetStyle().ItemSpacing.X * (6f / 5));
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero);
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero);
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero);
|
|
||||||
ImGui.BeginDisabled(!CanModifyActions);
|
|
||||||
|
|
||||||
foreach (var (category, actions) in SortedActions)
|
|
||||||
{
|
|
||||||
var i = 0;
|
|
||||||
ImGuiUtils.BeginGroupPanel(category.GetDisplayName(), ActionColumnSize);
|
|
||||||
foreach (var action in actions)
|
|
||||||
{
|
|
||||||
var baseAction = action.Base();
|
|
||||||
|
|
||||||
var cannotUse = action.Level() > Input.Stats.Level || (action == ActionType.Manipulation && !Input.Stats.CanUseManipulation);
|
|
||||||
if (cannotUse && Config.HideUnlearnedActions)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var shouldNotUse = !baseAction.CanUse(Sim) || Sim.IsComplete;
|
|
||||||
|
|
||||||
ImGui.BeginDisabled(cannotUse);
|
|
||||||
|
|
||||||
if (ImGui.ImageButton(action.GetIcon(ClassJob).ImGuiHandle, actionSize, Vector2.Zero, Vector2.One, 0, default, shouldNotUse ? BadActionImageTint : Vector4.One))
|
|
||||||
AppendAction(action);
|
|
||||||
|
|
||||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
|
||||||
ImGui.SetTooltip($"{action.GetName(ClassJob)}\n{baseAction.GetTooltip(Sim, true)}");
|
|
||||||
|
|
||||||
ImGui.EndDisabled();
|
|
||||||
|
|
||||||
if (++i % 5 != 0)
|
|
||||||
ImGui.SameLine();
|
|
||||||
}
|
|
||||||
if (i == 0)
|
|
||||||
ImGui.Dummy(actionSize);
|
|
||||||
ImGuiUtils.EndGroupPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.EndDisabled();
|
|
||||||
ImGui.PopStyleColor(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSimulation()
|
|
||||||
{
|
|
||||||
var drawParams = CalculateSynthDrawParams();
|
|
||||||
|
|
||||||
DrawSimulationHeader();
|
|
||||||
DrawSimulationBars(drawParams);
|
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
|
||||||
DrawSimulationEffects(drawParams);
|
|
||||||
ImGuiHelpers.ScaledDummy(5);
|
|
||||||
DrawSimulationActions(drawParams);
|
|
||||||
var bottom = ImGui.GetContentRegionAvail().Y - ImGui.GetStyle().FramePadding.Y * 2;
|
|
||||||
var buttonHeight = ImGui.GetFrameHeightWithSpacing() * 2 + ImGui.GetFrameHeight();
|
|
||||||
ImGuiHelpers.ScaledDummy(bottom - buttonHeight);
|
|
||||||
DrawSimulationButtons(drawParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSimulationHeader()
|
|
||||||
{
|
|
||||||
var imageSize = new Vector2(ImGui.GetFontSize() * 2.25f);
|
|
||||||
|
|
||||||
ImGui.Image(Icons.GetIconFromId(Item.Icon).ImGuiHandle, imageSize);
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize.Y - ImGui.GetFontSize()) / 2f);
|
|
||||||
ImGui.TextUnformatted(Item.Name.ToDalamudString().ToString());
|
|
||||||
if (Item.IsCollectable)
|
|
||||||
{
|
|
||||||
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
|
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize.Y - ImGui.GetFontSize()) / 2f);
|
|
||||||
ImGui.TextColored(new(0.98f, 0.98f, 0.61f, 1), SeIconChar.Collectible.ToIconString());
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip("Collectable");
|
|
||||||
}
|
|
||||||
if (IsExpert)
|
|
||||||
{
|
|
||||||
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
|
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize.Y - ImGui.GetFontSize()) / 2f);
|
|
||||||
// Using ItemLevel icon instead of '◈' because the game fonts hate
|
|
||||||
// me and I can't bother to include a font just for this one icon.
|
|
||||||
ImGui.TextColored(new(0.93f, 0.59f, 0.45f, 1), SeIconChar.ItemLevel.ToIconString());
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip("Expert Recipe");
|
|
||||||
}
|
|
||||||
var availWidth = ImGui.GetContentRegionAvail().X;
|
|
||||||
var text = $"Step {LatestState.StepCount + 1}";
|
|
||||||
var textWidth = ImGui.CalcTextSize(text).X;
|
|
||||||
ImGui.SameLine(availWidth - textWidth);
|
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize.Y - ImGui.GetFontSize()) / 2f);
|
|
||||||
ImGui.TextUnformatted(text);
|
|
||||||
ImGui.Separator();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSimulationBars(SynthDrawParams drawParams)
|
|
||||||
{
|
|
||||||
var state = LatestState;
|
|
||||||
|
|
||||||
var (leftColumn, rightColumn, leftText, rightText) = (drawParams.LeftColumn, drawParams.RightColumn, drawParams.LeftText, drawParams.RightText);
|
|
||||||
|
|
||||||
ImGui.BeginTable("simSynth", 2);
|
|
||||||
|
|
||||||
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, leftColumn);
|
|
||||||
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, rightColumn);
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
|
|
||||||
DrawSynthProgress("Durability", state.Durability, Input.Recipe.MaxDurability, DurabilityBarSize, DurabilityColor, leftText);
|
|
||||||
|
|
||||||
DrawSynthCircle("Condition", state.Condition.Name(), ConditionBarSize, new Vector4(.35f, .35f, .35f, 0) + state.Condition.GetColor(DateTime.UtcNow.TimeOfDay), DurabilityBarSize, leftText);
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip(state.Condition.Description(state.Input.Stats.HasSplendorousBuff));
|
|
||||||
|
|
||||||
if (Item.IsCollectable)
|
|
||||||
{
|
|
||||||
var collectibility = Math.Max(state.Quality / 10, 1);
|
|
||||||
DrawSynthBar("Collectability", collectibility, Input.Recipe.MaxQuality / 10, $"{collectibility}", DurabilityBarSize, HQColor, leftText);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
DrawSynthBar("HQ %", state.HQPercent, 100, $"{state.HQPercent}%", DurabilityBarSize, HQColor, leftText);
|
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
|
|
||||||
DrawSynthProgress("Progress", state.Progress, Input.Recipe.MaxProgress, ProgressBarSize, ProgressColor, rightText);
|
|
||||||
DrawSynthProgress("Quality", state.Quality, Input.Recipe.MaxQuality, ProgressBarSize, QualityColor, rightText);
|
|
||||||
DrawSynthProgress("CP", state.CP, Input.Stats.CP, ProgressBarSize, CPColor, rightText);
|
|
||||||
|
|
||||||
ImGui.EndTable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSimulationEffects(SynthDrawParams drawParams)
|
|
||||||
{
|
|
||||||
ImGuiUtils.BeginGroupPanel("Effects", drawParams.Total);
|
|
||||||
|
|
||||||
var effectHeight = ImGui.GetFontSize() * 2f;
|
|
||||||
Vector2 GetEffectSize(TextureWrap icon) => new(icon.Width * effectHeight / icon.Height, effectHeight);
|
|
||||||
|
|
||||||
ImGui.Dummy(new(0, effectHeight));
|
|
||||||
ImGui.SameLine(0, 0);
|
|
||||||
foreach (var effect in Enum.GetValues<EffectType>())
|
|
||||||
{
|
|
||||||
var duration = Sim.GetEffectDuration(effect);
|
|
||||||
if (duration == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var strength = Sim.GetEffectStrength(effect);
|
|
||||||
var icon = effect.GetIcon(strength);
|
|
||||||
var iconSize = GetEffectSize(icon);
|
|
||||||
|
|
||||||
ImGui.Image(icon.ImGuiHandle, iconSize);
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip(effect.GetTooltip(strength, duration));
|
|
||||||
if (duration != 0)
|
|
||||||
{
|
|
||||||
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
|
|
||||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (effectHeight - ImGui.GetFontSize()) / 2f);
|
|
||||||
ImGui.Text($"{duration}");
|
|
||||||
}
|
|
||||||
ImGui.SameLine();
|
|
||||||
}
|
|
||||||
ImGui.Dummy(Vector2.Zero);
|
|
||||||
|
|
||||||
ImGuiUtils.EndGroupPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSimulationActions(SynthDrawParams drawParams)
|
|
||||||
{
|
|
||||||
ImGuiUtils.BeginGroupPanel("Actions", drawParams.Total);
|
|
||||||
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero);
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero);
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero);
|
|
||||||
var actionSize = new Vector2((drawParams.Total / 10) - ImGui.GetStyle().ItemSpacing.X * (11f / 10));
|
|
||||||
ImGui.Dummy(new(0, actionSize.Y));
|
|
||||||
ImGui.SameLine(0, 0);
|
|
||||||
for (var i = 0; i < Actions.Count; ++i)
|
|
||||||
{
|
|
||||||
var (action, tooltip, response, state) = Actions[i];
|
|
||||||
ImGui.PushID(i);
|
|
||||||
if (ImGui.ImageButton(action.GetIcon(ClassJob).ImGuiHandle, actionSize, Vector2.Zero, Vector2.One, 0, default, response != ActionResponse.UsedAction ? BadActionImageTint : Vector4.One))
|
|
||||||
if (CanModifyActions)
|
|
||||||
RemoveAction(i);
|
|
||||||
if (CanModifyActions)
|
|
||||||
{
|
|
||||||
if (ImGui.BeginDragDropSource())
|
|
||||||
{
|
|
||||||
unsafe { ImGui.SetDragDropPayload("simulationAction", (nint)(&i), sizeof(int)); }
|
|
||||||
ImGui.ImageButton(Actions[i].Action.GetIcon(ClassJob).ImGuiHandle, actionSize);
|
|
||||||
ImGui.EndDragDropSource();
|
|
||||||
}
|
|
||||||
if (ImGui.BeginDragDropTarget())
|
|
||||||
{
|
|
||||||
var payload = ImGui.AcceptDragDropPayload("simulationAction");
|
|
||||||
bool isValidPayload;
|
|
||||||
unsafe { isValidPayload = payload.NativePtr != null; }
|
|
||||||
if (isValidPayload)
|
|
||||||
{
|
|
||||||
int draggedIdx;
|
|
||||||
unsafe { draggedIdx = *(int*)payload.Data; }
|
|
||||||
var draggedAction = Actions[draggedIdx].Action;
|
|
||||||
RemoveAction(draggedIdx);
|
|
||||||
InsertAction(i, draggedAction);
|
|
||||||
}
|
|
||||||
ImGui.EndDragDropTarget();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
{
|
|
||||||
ImGui.BeginTooltip();
|
|
||||||
var responseText = response switch
|
|
||||||
{
|
|
||||||
ActionResponse.SimulationComplete => "Recipe Complete",
|
|
||||||
ActionResponse.ActionNotUnlocked => "Action Not Unlocked",
|
|
||||||
ActionResponse.NotEnoughCP => "Not Enough CP",
|
|
||||||
ActionResponse.NoDurability => "No More Durability",
|
|
||||||
ActionResponse.CannotUseAction => "Cannot Use",
|
|
||||||
_ => string.Empty,
|
|
||||||
};
|
|
||||||
if (response != ActionResponse.UsedAction)
|
|
||||||
ImGui.TextColored(BadActionTextColor, responseText);
|
|
||||||
ImGui.Text($"{action.GetName(ClassJob)}\n{tooltip}");
|
|
||||||
DrawAllProgressTooltips(state);
|
|
||||||
if (CanModifyActions)
|
|
||||||
ImGui.Text("Click to Remove\nDrag to Move");
|
|
||||||
ImGui.EndTooltip();
|
|
||||||
}
|
|
||||||
ImGui.PopID();
|
|
||||||
if (i % 10 != 9)
|
|
||||||
ImGui.SameLine();
|
|
||||||
}
|
|
||||||
ImGui.PopStyleColor(3);
|
|
||||||
|
|
||||||
ImGuiUtils.EndGroupPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSimulationButtons(SynthDrawParams drawParams)
|
|
||||||
{
|
|
||||||
var totalWidth = drawParams.Total;
|
|
||||||
var halfWidth = (totalWidth - ImGui.GetStyle().ItemSpacing.X) / 2f;
|
|
||||||
var quarterWidth = (halfWidth - ImGui.GetStyle().ItemSpacing.X) / 2f;
|
|
||||||
var halfButtonSize = new Vector2(halfWidth, ImGuiUtils.ButtonHeight);
|
|
||||||
var quarterButtonSize = new Vector2(quarterWidth, ImGuiUtils.ButtonHeight);
|
|
||||||
|
|
||||||
var conditionRandomnessText = "Condition Randomness";
|
|
||||||
var conditionRandomness = Config.ConditionRandomness;
|
|
||||||
ImGui.BeginDisabled(!CanModifyActions);
|
|
||||||
if (ImGui.Checkbox(conditionRandomnessText, ref conditionRandomness))
|
|
||||||
{
|
|
||||||
Config.ConditionRandomness = conditionRandomness;
|
|
||||||
Config.Save();
|
|
||||||
ResetSimulator();
|
|
||||||
}
|
|
||||||
ImGui.EndDisabled();
|
|
||||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
|
||||||
ImGui.SetTooltip("Allows the condition to fluctuate randomly like a real craft.\nTurns off when generating a macro.");
|
|
||||||
|
|
||||||
var labelSize = ImGui.CalcTextSize(conditionRandomnessText);
|
|
||||||
var checkboxWidth = ImGui.GetFrameHeight() + (labelSize.X > 0 ? ImGui.GetStyle().ItemInnerSpacing.X + labelSize.X : 0);
|
|
||||||
ImGui.PushFont(UiBuilder.IconFont);
|
|
||||||
var cogWidth = ImGui.CalcTextSize(FontAwesomeIcon.Cog.ToIconString()).X;
|
|
||||||
ImGui.PopFont();
|
|
||||||
ImGui.SameLine(0, totalWidth - ImGui.GetStyle().ItemSpacing.X - checkboxWidth - cogWidth);
|
|
||||||
if (ImGuiComponents.IconButton("simSettingsButton", FontAwesomeIcon.Cog))
|
|
||||||
Service.Plugin.OpenSettingsTab(Settings.TabSimulator);
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
var macroName = MacroName;
|
|
||||||
ImGui.SetNextItemWidth(halfWidth);
|
|
||||||
if (ImGui.InputTextWithHint("", "Macro Name", ref macroName, 64))
|
|
||||||
MacroName = macroName;
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
|
|
||||||
DrawSimulationGenerateButton(halfButtonSize);
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
ImGui.BeginDisabled(!CanModifyActions);
|
|
||||||
if (Macro != null)
|
|
||||||
{
|
|
||||||
if (ImGui.Button("Save", quarterButtonSize))
|
|
||||||
{
|
|
||||||
Macro.Name = MacroName;
|
|
||||||
Macro.Actions = Actions.Select(a => a.Action).ToList();
|
|
||||||
Config.Save();
|
|
||||||
}
|
|
||||||
ImGui.SameLine();
|
|
||||||
}
|
|
||||||
if (ImGui.Button("Save New", Macro == null ? halfButtonSize : quarterButtonSize))
|
|
||||||
{
|
|
||||||
Macro = new() { Name = MacroName, Actions = Actions.Select(a => a.Action).ToList() };
|
|
||||||
Config.Macros.Add(Macro);
|
|
||||||
Config.Save();
|
|
||||||
}
|
|
||||||
ImGui.SameLine();
|
|
||||||
if (ImGui.Button("Reset", halfButtonSize))
|
|
||||||
ClearAllActions();
|
|
||||||
ImGui.EndDisabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawSimulationGenerateButton(Vector2 buttonSize)
|
|
||||||
{
|
|
||||||
var state = GenerateSolverState();
|
|
||||||
string buttonText;
|
|
||||||
string tooltipText;
|
|
||||||
bool isEnabled;
|
|
||||||
var taskCompleted = SolverTask?.IsCompleted ?? true;
|
|
||||||
var taskCancelled = SolverTaskToken?.IsCancellationRequested ?? false;
|
|
||||||
if (!taskCompleted)
|
|
||||||
{
|
|
||||||
if (taskCancelled)
|
|
||||||
{
|
|
||||||
buttonText = "Cancelling...";
|
|
||||||
tooltipText = "Cancelling macro generation. This shouldn't take long.";
|
|
||||||
isEnabled = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
buttonText = "Cancel";
|
|
||||||
tooltipText = "Cancel macro generation";
|
|
||||||
isEnabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (SolverActionsChanged)
|
|
||||||
{
|
|
||||||
buttonText = "Generate";
|
|
||||||
tooltipText = "Generate a set of actions to finish the macro.";
|
|
||||||
isEnabled = state.HasValue;
|
|
||||||
if (!isEnabled)
|
|
||||||
tooltipText += "\nMake sure your craft so far is valid (without random condition changes)";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
buttonText = "Regenerate";
|
|
||||||
tooltipText = "Retry and regenerate a new set of actions to finish the macro.";
|
|
||||||
isEnabled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui.BeginDisabled(!isEnabled);
|
|
||||||
if (ImGui.Button(buttonText, buttonSize))
|
|
||||||
{
|
|
||||||
if (!taskCompleted)
|
|
||||||
{
|
|
||||||
if (!taskCancelled)
|
|
||||||
SolverTaskToken?.Cancel();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (SolverActionsChanged)
|
|
||||||
{
|
|
||||||
if (state.HasValue)
|
|
||||||
SolveMacro(state.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Actions.RemoveRange(SolverInitialActionCount, Actions.Count - SolverInitialActionCount);
|
|
||||||
SolveMacro(GenerateSolverState()!.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui.EndDisabled();
|
|
||||||
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
|
||||||
ImGui.SetTooltip(tooltipText);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
using Craftimizer.Simulator;
|
|
||||||
using Dalamud.Interface.Windowing;
|
|
||||||
using ImGuiNET;
|
|
||||||
using System;
|
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace Craftimizer.Plugin.Windows;
|
|
||||||
|
|
||||||
public sealed partial class Simulator : Window, IDisposable
|
|
||||||
{
|
|
||||||
private readonly record struct SynthDrawParams
|
|
||||||
{
|
|
||||||
public float LeftColumn { get; init; }
|
|
||||||
public float RightColumn { get; init; }
|
|
||||||
public float LeftText { get; init; }
|
|
||||||
public float RightText { get; init; }
|
|
||||||
public float Total { get; init; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private SynthDrawParams CalculateSynthDrawParams()
|
|
||||||
{
|
|
||||||
var sidePadding = ImGui.GetFrameHeight() / 2;
|
|
||||||
var separatorTextWidth = ImGui.CalcTextSize(" / ").X;
|
|
||||||
var itemSpacing = ImGui.GetStyle().ItemSpacing.X;
|
|
||||||
|
|
||||||
var leftDigits = (int)MathF.Floor(MathF.Log10(Input.Recipe.MaxDurability) + 1);
|
|
||||||
var leftTextWidth = ImGui.CalcTextSize(new string('0', leftDigits)).X;
|
|
||||||
var leftWidth = DurabilityBarSize.X + sidePadding + itemSpacing * 2 + separatorTextWidth + leftTextWidth * 2;
|
|
||||||
|
|
||||||
|
|
||||||
var rightDigits = (int)MathF.Floor(MathF.Log10(Math.Max(Math.Max(Input.Recipe.MaxProgress, Input.Recipe.MaxQuality), Input.Stats.CP)) + 1);
|
|
||||||
var rightTextWidth = ImGui.CalcTextSize(new string('0', rightDigits)).X;
|
|
||||||
var rightWidth = ProgressBarSize.X + sidePadding + itemSpacing * 2 + separatorTextWidth + rightTextWidth * 2;
|
|
||||||
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
LeftColumn = leftWidth,
|
|
||||||
LeftText = leftTextWidth,
|
|
||||||
RightColumn = rightWidth,
|
|
||||||
RightText = rightTextWidth,
|
|
||||||
Total = leftWidth + rightWidth + itemSpacing
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic Progress Bar
|
|
||||||
private static void DrawSynthProgress(string name, int current, int max, Vector2 size, Vector4 color, float textWidth)
|
|
||||||
{
|
|
||||||
ImGuiUtils.BeginGroupPanel(name);
|
|
||||||
|
|
||||||
DrawProgressBar(current, max, size, color);
|
|
||||||
|
|
||||||
var w = ImGui.GetStyle().ItemSpacing.X;
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, ImGui.GetStyle().ItemSpacing.Y));
|
|
||||||
|
|
||||||
ImGui.SameLine(0, textWidth - ImGui.CalcTextSize($"{current}").X + w);
|
|
||||||
var adjustedHeight = ImGui.GetCursorPosY() - ((ImGui.GetFrameHeight() - ImGui.GetFontSize()) / 2f);
|
|
||||||
ImGui.SetCursorPosY(adjustedHeight);
|
|
||||||
ImGui.TextUnformatted($"{current}");
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.SetCursorPosY(adjustedHeight);
|
|
||||||
ImGui.TextUnformatted(" / ");
|
|
||||||
|
|
||||||
ImGui.SameLine(0, textWidth - ImGui.CalcTextSize($"{max}").X);
|
|
||||||
ImGui.SetCursorPosY(adjustedHeight);
|
|
||||||
ImGui.TextUnformatted($"{max}");
|
|
||||||
|
|
||||||
ImGui.PopStyleVar();
|
|
||||||
|
|
||||||
ImGuiUtils.EndGroupPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// HQ% / Collectability Bar (has no fractional bar to indicate max)
|
|
||||||
private static void DrawSynthBar(string name, int current, int max, string text, Vector2 size, Vector4 color, float textWidth)
|
|
||||||
{
|
|
||||||
ImGuiUtils.BeginGroupPanel(name);
|
|
||||||
|
|
||||||
DrawProgressBar(current, max, size, color);
|
|
||||||
|
|
||||||
var w = ImGui.GetStyle().ItemSpacing.X;
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, ImGui.GetStyle().ItemSpacing.Y));
|
|
||||||
|
|
||||||
var totalWidth = textWidth * 2 + ImGui.CalcTextSize(" / ").X;
|
|
||||||
|
|
||||||
ImGui.SameLine(0, totalWidth - ImGui.CalcTextSize(text).X + w);
|
|
||||||
var adjustedHeight = ImGui.GetCursorPosY() - ((ImGui.GetFrameHeight() - ImGui.GetFontSize()) / 2f);
|
|
||||||
ImGui.SetCursorPosY(adjustedHeight);
|
|
||||||
ImGui.TextUnformatted(text);
|
|
||||||
|
|
||||||
ImGui.PopStyleVar();
|
|
||||||
|
|
||||||
ImGuiUtils.EndGroupPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Condition "Bar" Circle (always 100%, is a circle)
|
|
||||||
private static void DrawSynthCircle(string name, string text, Vector2 size, Vector4 color, Vector2 otherProgressSize, float textWidth)
|
|
||||||
{
|
|
||||||
ImGuiUtils.BeginGroupPanel(name);
|
|
||||||
|
|
||||||
var w = ImGui.GetStyle().ItemSpacing.X;
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, ImGui.GetStyle().ItemSpacing.Y));
|
|
||||||
|
|
||||||
var contentWidth = size.X + w + ImGui.CalcTextSize(text).X;
|
|
||||||
var totalWidth = otherProgressSize.X + w + textWidth * 2 + ImGui.CalcTextSize(" / ").X;
|
|
||||||
|
|
||||||
ImGui.Dummy(default);
|
|
||||||
ImGui.SameLine(0, (totalWidth - contentWidth) / 2);
|
|
||||||
ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, Math.Max(size.X, size.Y));
|
|
||||||
DrawProgressBar(1, 1, size, color);
|
|
||||||
ImGui.PopStyleVar();
|
|
||||||
ImGui.SameLine(0, w);
|
|
||||||
var adjustedHeight = ImGui.GetCursorPosY() - ((ImGui.GetFrameHeight() - ImGui.GetFontSize()) / 2f);
|
|
||||||
ImGui.SetCursorPosY(adjustedHeight);
|
|
||||||
ImGui.TextUnformatted(text);
|
|
||||||
|
|
||||||
ImGui.PopStyleVar();
|
|
||||||
|
|
||||||
ImGuiUtils.EndGroupPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void DrawAllProgressBars(SimulationState state, Vector2 progressBarSize)
|
|
||||||
{
|
|
||||||
DrawProgressBar(state.Progress, state.Input.Recipe.MaxProgress, progressBarSize, ProgressColor);
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip($"Progress: {state.Progress} / {state.Input.Recipe.MaxProgress}");
|
|
||||||
DrawProgressBar(state.Quality, state.Input.Recipe.MaxQuality, progressBarSize, QualityColor);
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip($"Quality: {state.Quality} / {state.Input.Recipe.MaxQuality}");
|
|
||||||
DrawProgressBar(state.Durability, state.Input.Recipe.MaxDurability, progressBarSize, DurabilityColor);
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip($"Durability: {state.Durability} / {state.Input.Recipe.MaxDurability}");
|
|
||||||
DrawProgressBar(state.CP, state.Input.Stats.CP, progressBarSize, CPColor);
|
|
||||||
if (ImGui.IsItemHovered())
|
|
||||||
ImGui.SetTooltip($"CP: {state.CP} / {state.Input.Stats.CP}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void DrawAllProgressTooltips(SimulationState state) =>
|
|
||||||
DrawAllProgressBars(state, TooltipProgressBarSize);
|
|
||||||
|
|
||||||
private static void DrawProgressBar(int progress, int maxProgress, Vector2 size, Vector4 color, string overlay = "")
|
|
||||||
{
|
|
||||||
ImGui.PushStyleColor(ImGuiCol.PlotHistogram, color);
|
|
||||||
ImGui.ProgressBar(Math.Clamp((float)progress / maxProgress, 0f, 1f), size, overlay);
|
|
||||||
ImGui.PopStyleColor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
using Craftimizer.Simulator;
|
|
||||||
using Craftimizer.Simulator.Actions;
|
|
||||||
using Dalamud.Interface.Windowing;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Craftimizer.Plugin.Windows;
|
|
||||||
|
|
||||||
public sealed partial class Simulator : Window, IDisposable
|
|
||||||
{
|
|
||||||
private Task? SolverTask { get; set; }
|
|
||||||
private CancellationTokenSource? SolverTaskToken { get; set; }
|
|
||||||
private ConcurrentQueue<ActionType> SolverActionQueue { get; } = new();
|
|
||||||
private int SolverInitialActionCount { get; set; }
|
|
||||||
private bool SolverActionsChanged { get; set; } = true;
|
|
||||||
|
|
||||||
private bool CanModifyActions => SolverTask?.IsCompleted ?? true;
|
|
||||||
|
|
||||||
private void OnActionsChanged()
|
|
||||||
{
|
|
||||||
SolverActionsChanged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SimulationState? GenerateSolverState()
|
|
||||||
{
|
|
||||||
if (Sim is SimulatorNoRandom)
|
|
||||||
{
|
|
||||||
if (!Actions.Exists(a => a.Response != ActionResponse.UsedAction))
|
|
||||||
return LatestState;
|
|
||||||
else
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret = new SimulationState(Input);
|
|
||||||
if (Actions.Count != 0)
|
|
||||||
{
|
|
||||||
var tmpSim = new SimulatorNoRandom(ret);
|
|
||||||
foreach (var action in Actions)
|
|
||||||
{
|
|
||||||
(var resp, ret) = tmpSim.Execute(ret, action.Action);
|
|
||||||
if (resp != ActionResponse.UsedAction)
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StopSolveMacro()
|
|
||||||
{
|
|
||||||
if (SolverTask == null || SolverTaskToken == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!SolverTask.IsCompleted)
|
|
||||||
SolverTaskToken.Cancel();
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SolverTaskToken.Dispose();
|
|
||||||
SolverTask.Dispose();
|
|
||||||
|
|
||||||
SolverTask = null;
|
|
||||||
SolverTaskToken = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SolveMacro(SimulationState solverState)
|
|
||||||
{
|
|
||||||
StopSolveMacro();
|
|
||||||
|
|
||||||
// Prevents the quality bar from being unfair between solves
|
|
||||||
if (Config.ConditionRandomness)
|
|
||||||
{
|
|
||||||
Config.ConditionRandomness = false;
|
|
||||||
Config.Save();
|
|
||||||
|
|
||||||
ResetSimulator();
|
|
||||||
}
|
|
||||||
|
|
||||||
SolverActionsChanged = false;
|
|
||||||
|
|
||||||
SolverActionQueue.Clear();
|
|
||||||
|
|
||||||
SolverInitialActionCount = Actions.Count;
|
|
||||||
SolverTaskToken = new();
|
|
||||||
SolverTask = Task.Run(() => Config.SimulatorSolverConfig.Invoke(solverState, SolverActionQueue.Enqueue, SolverTaskToken.Token));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
StopSolveMacro();
|
|
||||||
SolverTask?.Wait();
|
|
||||||
SolverTask?.Dispose();
|
|
||||||
SolverTaskToken?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,15 +4,15 @@
|
|||||||
"net7.0-windows7.0": {
|
"net7.0-windows7.0": {
|
||||||
"DalamudPackager": {
|
"DalamudPackager": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[2.1.11, )",
|
"requested": "[2.1.12, )",
|
||||||
"resolved": "2.1.11",
|
"resolved": "2.1.12",
|
||||||
"contentHash": "9qlAWoRRTiL/geAvuwR/g6Bcbrd/bJJgVnB/RurBiyKs6srsP0bvpoo8IK+Eg8EA6jWeM6/YJWs66w4FIAzqPw=="
|
"contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg=="
|
||||||
},
|
},
|
||||||
"Meziantou.Analyzer": {
|
"Meziantou.Analyzer": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[2.0.62, )",
|
"requested": "[2.0.92, )",
|
||||||
"resolved": "2.0.62",
|
"resolved": "2.0.92",
|
||||||
"contentHash": "uG2CiDIm97q8KrUt8B34WdElpEDDLOe4YzrLWpwlQmesXrSX2WuJZ+HwIGWrJgDBBMi2a3tVjeF8oKjV+AhUdA=="
|
"contentHash": "gVyPM2gDPfxvZ2rGUKzTNZsNhdgsetfYd+OKowKwMqZ9K00j1amUU+SnlRI26629EKK4cbJWJwHs00UPXRr0BA=="
|
||||||
},
|
},
|
||||||
"craftimizer.simulator": {
|
"craftimizer.simulator": {
|
||||||
"type": "Project"
|
"type": "Project"
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
using Craftimizer.Simulator.Actions;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace Craftimizer.Simulator;
|
namespace Craftimizer.Simulator;
|
||||||
|
|
||||||
public enum ActionCategory
|
public enum ActionCategory
|
||||||
@@ -13,6 +16,25 @@ public enum ActionCategory
|
|||||||
|
|
||||||
public static class ActionCategoryUtils
|
public static class ActionCategoryUtils
|
||||||
{
|
{
|
||||||
|
private static readonly ReadOnlyDictionary<ActionCategory, ActionType[]> SortedActions;
|
||||||
|
|
||||||
|
static ActionCategoryUtils()
|
||||||
|
{
|
||||||
|
SortedActions = new(
|
||||||
|
Enum.GetValues<ActionType>()
|
||||||
|
.Where(a => a.Category() != ActionCategory.Combo)
|
||||||
|
.GroupBy(a => a.Category())
|
||||||
|
.ToDictionary(g => g.Key, g => g.OrderBy(a => a.Level()).ToArray()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<ActionType> GetActions(this ActionCategory me)
|
||||||
|
{
|
||||||
|
if (SortedActions.TryGetValue(me, out var actions))
|
||||||
|
return actions;
|
||||||
|
|
||||||
|
throw new ArgumentException($"Unknown action category {me}", nameof(me));
|
||||||
|
}
|
||||||
|
|
||||||
public static string GetDisplayName(this ActionCategory category) =>
|
public static string GetDisplayName(this ActionCategory category) =>
|
||||||
category switch
|
category switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ internal sealed class AdvancedTouch : BaseAction
|
|||||||
public override bool IncreasesQuality => true;
|
public override bool IncreasesQuality => true;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => s.ActionStates.TouchComboIdx == 2 ? 18 : 46;
|
public override int CPCost(Simulator s) => s.ActionStates.TouchComboIdx == 2 ? 18 : 46;
|
||||||
public override float Efficiency(Simulator s) => 1.50f;
|
public override int Efficiency(Simulator s) => 150;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public abstract class BaseAction
|
|||||||
|
|
||||||
// Instanced properties
|
// Instanced properties
|
||||||
public abstract int CPCost(Simulator s);
|
public abstract int CPCost(Simulator s);
|
||||||
public virtual float Efficiency(Simulator s) => 0f;
|
public virtual int Efficiency(Simulator s) => 0;
|
||||||
public virtual float SuccessRate(Simulator s) => 1f;
|
public virtual float SuccessRate(Simulator s) => 1f;
|
||||||
|
|
||||||
public virtual bool CanUse(Simulator s) =>
|
public virtual bool CanUse(Simulator s) =>
|
||||||
@@ -48,6 +48,7 @@ public abstract class BaseAction
|
|||||||
s.ActionStates.MutateState(this);
|
s.ActionStates.MutateState(this);
|
||||||
s.ActionCount++;
|
s.ActionCount++;
|
||||||
|
|
||||||
|
if (IncreasesStepCount)
|
||||||
s.ActiveEffects.DecrementDuration();
|
s.ActiveEffects.DecrementDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,13 @@ internal abstract class BaseBuffAction : BaseAction
|
|||||||
public override void UseSuccess(Simulator s) =>
|
public override void UseSuccess(Simulator s) =>
|
||||||
s.AddEffect(Effect, Duration);
|
s.AddEffect(Effect, Duration);
|
||||||
|
|
||||||
public sealed override string GetTooltip(Simulator s, bool addUsability)
|
public override string GetTooltip(Simulator s, bool addUsability)
|
||||||
{
|
{
|
||||||
var builder = new StringBuilder(base.GetTooltip(s, addUsability));
|
var builder = new StringBuilder(base.GetTooltip(s, addUsability));
|
||||||
builder.AppendLine($"{Duration} Steps");
|
builder.AppendLine($"{Duration} Steps");
|
||||||
return builder.ToString();
|
return builder.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected string GetBaseTooltip(Simulator s, bool addUsability) =>
|
||||||
|
base.GetTooltip(s, addUsability);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ internal sealed class BasicSynthesis : BaseAction
|
|||||||
|
|
||||||
public override int CPCost(Simulator s) => 0;
|
public override int CPCost(Simulator s) => 0;
|
||||||
// Basic Synthesis Mastery Trait
|
// Basic Synthesis Mastery Trait
|
||||||
public override float Efficiency(Simulator s) => s.Input.Stats.Level >= 31 ? 1.20f : 1.00f;
|
public override int Efficiency(Simulator s) => s.Input.Stats.Level >= 31 ? 120 : 100;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ internal sealed class BasicTouch : BaseAction
|
|||||||
public override bool IncreasesQuality => true;
|
public override bool IncreasesQuality => true;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => 18;
|
public override int CPCost(Simulator s) => 18;
|
||||||
public override float Efficiency(Simulator s) => 1.00f;
|
public override int Efficiency(Simulator s) => 100;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ internal sealed class ByregotsBlessing : BaseAction
|
|||||||
public override bool IncreasesQuality => true;
|
public override bool IncreasesQuality => true;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => 24;
|
public override int CPCost(Simulator s) => 24;
|
||||||
public override float Efficiency(Simulator s) => 1.00f + (0.20f * s.GetEffectStrength(EffectType.InnerQuiet));
|
public override int Efficiency(Simulator s) => 100 + (20 * s.GetEffectStrength(EffectType.InnerQuiet));
|
||||||
|
|
||||||
public override bool CanUse(Simulator s) => s.HasEffect(EffectType.InnerQuiet) && base.CanUse(s);
|
public override bool CanUse(Simulator s) => s.HasEffect(EffectType.InnerQuiet) && base.CanUse(s);
|
||||||
|
|
||||||
|
|||||||
@@ -15,4 +15,7 @@ internal sealed class CarefulObservation : BaseAction
|
|||||||
public override bool CanUse(Simulator s) => s.Input.Stats.IsSpecialist && s.ActionStates.CarefulObservationCount < 3;
|
public override bool CanUse(Simulator s) => s.Input.Stats.IsSpecialist && s.ActionStates.CarefulObservationCount < 3;
|
||||||
|
|
||||||
public override void UseSuccess(Simulator s) => s.StepCondition();
|
public override void UseSuccess(Simulator s) => s.StepCondition();
|
||||||
|
|
||||||
|
public override string GetTooltip(Simulator s, bool addUsability) =>
|
||||||
|
$"{base.GetTooltip(s, addUsability)}Specialist Only";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ internal sealed class CarefulSynthesis : BaseAction
|
|||||||
|
|
||||||
public override int CPCost(Simulator s) => 7;
|
public override int CPCost(Simulator s) => 7;
|
||||||
// Careful Synthesis Mastery Trait
|
// Careful Synthesis Mastery Trait
|
||||||
public override float Efficiency(Simulator s) => s.Input.Stats.Level >= 82 ? 1.80f : 1.50f;
|
public override int Efficiency(Simulator s) => s.Input.Stats.Level >= 82 ? 180 : 150;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,5 +10,5 @@ internal sealed class DelicateSynthesis : BaseAction
|
|||||||
public override bool IncreasesQuality => true;
|
public override bool IncreasesQuality => true;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => 32;
|
public override int CPCost(Simulator s) => 32;
|
||||||
public override float Efficiency(Simulator s) => 1.00f;
|
public override int Efficiency(Simulator s) => 100;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ internal sealed class FocusedSynthesis : BaseAction
|
|||||||
public override bool IncreasesProgress => true;
|
public override bool IncreasesProgress => true;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => 5;
|
public override int CPCost(Simulator s) => 5;
|
||||||
public override float Efficiency(Simulator s) => 2.00f;
|
public override int Efficiency(Simulator s) => 200;
|
||||||
public override float SuccessRate(Simulator s) => s.ActionStates.Observed ? 1.00f : 0.50f;
|
public override float SuccessRate(Simulator s) => s.ActionStates.Observed ? 1.00f : 0.50f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ internal sealed class FocusedTouch : BaseAction
|
|||||||
public override bool IncreasesQuality => true;
|
public override bool IncreasesQuality => true;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => 18;
|
public override int CPCost(Simulator s) => 18;
|
||||||
public override float Efficiency(Simulator s) => 1.50f;
|
public override int Efficiency(Simulator s) => 150;
|
||||||
public override float SuccessRate(Simulator s) => s.ActionStates.Observed ? 1.00f : 0.50f;
|
public override float SuccessRate(Simulator s) => s.ActionStates.Observed ? 1.00f : 0.50f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ internal sealed class Groundwork : BaseAction
|
|||||||
public override int DurabilityCost => 20;
|
public override int DurabilityCost => 20;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => 18;
|
public override int CPCost(Simulator s) => 18;
|
||||||
public override float Efficiency(Simulator s)
|
public override int Efficiency(Simulator s)
|
||||||
{
|
{
|
||||||
// Groundwork Mastery Trait
|
// Groundwork Mastery Trait
|
||||||
var ret = s.Input.Stats.Level >= 86 ? 3.60f : 3.00f;
|
var ret = s.Input.Stats.Level >= 86 ? 360 : 300;
|
||||||
return s.Durability < s.CalculateDurabilityCost(DurabilityCost) ? ret / 2 : ret;
|
return s.Durability < s.CalculateDurabilityCost(DurabilityCost) ? ret / 2 : ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ internal sealed class HastyTouch : BaseAction
|
|||||||
public override bool IncreasesQuality => true;
|
public override bool IncreasesQuality => true;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => 0;
|
public override int CPCost(Simulator s) => 0;
|
||||||
public override float Efficiency(Simulator s) => 1.00f;
|
public override int Efficiency(Simulator s) => 100;
|
||||||
public override float SuccessRate(Simulator s) => 0.60f;
|
public override float SuccessRate(Simulator s) => 0.60f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,7 @@ internal sealed class HeartAndSoul : BaseBuffAction
|
|||||||
public override int CPCost(Simulator s) => 0;
|
public override int CPCost(Simulator s) => 0;
|
||||||
|
|
||||||
public override bool CanUse(Simulator s) => s.Input.Stats.IsSpecialist && !s.ActionStates.UsedHeartAndSoul;
|
public override bool CanUse(Simulator s) => s.Input.Stats.IsSpecialist && !s.ActionStates.UsedHeartAndSoul;
|
||||||
|
|
||||||
|
public override string GetTooltip(Simulator s, bool addUsability) =>
|
||||||
|
$"{GetBaseTooltip(s, addUsability)}Specialist Only";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ internal sealed class IntensiveSynthesis : BaseAction
|
|||||||
public override bool IncreasesProgress => true;
|
public override bool IncreasesProgress => true;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => 6;
|
public override int CPCost(Simulator s) => 6;
|
||||||
public override float Efficiency(Simulator s) => 4.00f;
|
public override int Efficiency(Simulator s) => 400;
|
||||||
|
|
||||||
public override bool CanUse(Simulator s) =>
|
public override bool CanUse(Simulator s) =>
|
||||||
(s.Condition == Condition.Good || s.Condition == Condition.Excellent || s.HasEffect(EffectType.HeartAndSoul))
|
(s.Condition == Condition.Good || s.Condition == Condition.Excellent || s.HasEffect(EffectType.HeartAndSoul))
|
||||||
|
|||||||
@@ -14,14 +14,16 @@ internal sealed class Manipulation : BaseBuffAction
|
|||||||
|
|
||||||
public override void Use(Simulator s)
|
public override void Use(Simulator s)
|
||||||
{
|
{
|
||||||
if (s.HasEffect(EffectType.Manipulation))
|
|
||||||
s.RestoreDurability(5);
|
|
||||||
|
|
||||||
s.ReduceCP(CPCost(s));
|
|
||||||
s.ReduceDurability(DurabilityCost);
|
|
||||||
|
|
||||||
UseSuccess(s);
|
UseSuccess(s);
|
||||||
|
|
||||||
|
s.ReduceCP(CPCost(s));
|
||||||
|
|
||||||
s.IncreaseStepCount();
|
s.IncreaseStepCount();
|
||||||
|
|
||||||
|
s.ActionStates.MutateState(this);
|
||||||
|
s.ActionCount++;
|
||||||
|
|
||||||
|
if (IncreasesStepCount)
|
||||||
|
s.ActiveEffects.DecrementDuration();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ internal sealed class MuscleMemory : BaseAction
|
|||||||
public override bool IncreasesProgress => true;
|
public override bool IncreasesProgress => true;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => 6;
|
public override int CPCost(Simulator s) => 6;
|
||||||
public override float Efficiency(Simulator s) => 3.00f;
|
public override int Efficiency(Simulator s) => 300;
|
||||||
|
|
||||||
public override bool CanUse(Simulator s) => s.IsFirstStep && base.CanUse(s);
|
public override bool CanUse(Simulator s) => s.IsFirstStep && base.CanUse(s);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ internal sealed class PreciseTouch : BaseAction
|
|||||||
public override bool IncreasesQuality => true;
|
public override bool IncreasesQuality => true;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => 18;
|
public override int CPCost(Simulator s) => 18;
|
||||||
public override float Efficiency(Simulator s) => 1.50f;
|
public override int Efficiency(Simulator s) => 150;
|
||||||
|
|
||||||
public override bool CanUse(Simulator s) =>
|
public override bool CanUse(Simulator s) =>
|
||||||
(s.Condition == Condition.Good || s.Condition == Condition.Excellent || s.HasEffect(EffectType.HeartAndSoul))
|
(s.Condition == Condition.Good || s.Condition == Condition.Excellent || s.HasEffect(EffectType.HeartAndSoul))
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ internal sealed class PreparatoryTouch : BaseAction
|
|||||||
public override int DurabilityCost => 20;
|
public override int DurabilityCost => 20;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => 40;
|
public override int CPCost(Simulator s) => 40;
|
||||||
public override float Efficiency(Simulator s) => 2.00f;
|
public override int Efficiency(Simulator s) => 200;
|
||||||
|
|
||||||
public override void UseSuccess(Simulator s)
|
public override void UseSuccess(Simulator s)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ internal sealed class PrudentSynthesis : BaseAction
|
|||||||
public override int DurabilityCost => base.DurabilityCost / 2;
|
public override int DurabilityCost => base.DurabilityCost / 2;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => 18;
|
public override int CPCost(Simulator s) => 18;
|
||||||
public override float Efficiency(Simulator s) => 1.80f;
|
public override int Efficiency(Simulator s) => 180;
|
||||||
|
|
||||||
public override bool CanUse(Simulator s) =>
|
public override bool CanUse(Simulator s) =>
|
||||||
!(s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2))
|
!(s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2))
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ internal sealed class PrudentTouch : BaseAction
|
|||||||
public override int DurabilityCost => base.DurabilityCost / 2;
|
public override int DurabilityCost => base.DurabilityCost / 2;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => 25;
|
public override int CPCost(Simulator s) => 25;
|
||||||
public override float Efficiency(Simulator s) => 1.00f;
|
public override int Efficiency(Simulator s) => 100;
|
||||||
|
|
||||||
public override bool CanUse(Simulator s) =>
|
public override bool CanUse(Simulator s) =>
|
||||||
!(s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2))
|
!(s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2))
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ internal sealed class RapidSynthesis : BaseAction
|
|||||||
|
|
||||||
public override int CPCost(Simulator s) => 0;
|
public override int CPCost(Simulator s) => 0;
|
||||||
// Rapid Synthesis Mastery Trait
|
// Rapid Synthesis Mastery Trait
|
||||||
public override float Efficiency(Simulator s) => s.Input.Stats.Level >= 63 ? 5.00f : 2.50f;
|
public override int Efficiency(Simulator s) => s.Input.Stats.Level >= 63 ? 500 : 250;
|
||||||
public override float SuccessRate(Simulator s) => 0.50f;
|
public override float SuccessRate(Simulator s) => 0.50f;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ internal sealed class Reflect : BaseAction
|
|||||||
public override bool IncreasesQuality => true;
|
public override bool IncreasesQuality => true;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => 6;
|
public override int CPCost(Simulator s) => 6;
|
||||||
public override float Efficiency(Simulator s) => 1.00f;
|
public override int Efficiency(Simulator s) => 100;
|
||||||
|
|
||||||
public override bool CanUse(Simulator s) => s.IsFirstStep && base.CanUse(s);
|
public override bool CanUse(Simulator s) => s.IsFirstStep && base.CanUse(s);
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ internal sealed class StandardTouch : BaseAction
|
|||||||
public override bool IncreasesQuality => true;
|
public override bool IncreasesQuality => true;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => s.ActionStates.TouchComboIdx == 1 ? 18 : 32;
|
public override int CPCost(Simulator s) => s.ActionStates.TouchComboIdx == 1 ? 18 : 32;
|
||||||
public override float Efficiency(Simulator s) => 1.25f;
|
public override int Efficiency(Simulator s) => 125;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,4 +18,7 @@ internal sealed class TrainedEye : BaseAction
|
|||||||
|
|
||||||
public override void UseSuccess(Simulator s) =>
|
public override void UseSuccess(Simulator s) =>
|
||||||
s.IncreaseQualityRaw(s.Input.Recipe.MaxQuality - s.Quality);
|
s.IncreaseQualityRaw(s.Input.Recipe.MaxQuality - s.Quality);
|
||||||
|
|
||||||
|
public override string GetTooltip(Simulator s, bool addUsability) =>
|
||||||
|
$"{base.GetTooltip(s, addUsability)}+{s.Input.Recipe.MaxQuality - s.Quality} Quality";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ internal sealed class TrainedFinesse : BaseAction
|
|||||||
public override int DurabilityCost => 0;
|
public override int DurabilityCost => 0;
|
||||||
|
|
||||||
public override int CPCost(Simulator s) => 32;
|
public override int CPCost(Simulator s) => 32;
|
||||||
public override float Efficiency(Simulator s) => 1.00f;
|
public override int Efficiency(Simulator s) => 100;
|
||||||
|
|
||||||
public override bool CanUse(Simulator s) =>
|
public override bool CanUse(Simulator s) =>
|
||||||
s.GetEffectStrength(EffectType.InnerQuiet) == 10
|
s.GetEffectStrength(EffectType.InnerQuiet) == 10
|
||||||
|
|||||||
@@ -6,5 +6,7 @@ public enum CompletionState : byte
|
|||||||
ProgressComplete,
|
ProgressComplete,
|
||||||
NoMoreDurability,
|
NoMoreDurability,
|
||||||
|
|
||||||
Other
|
InvalidAction,
|
||||||
|
MaxActionCountReached,
|
||||||
|
NoMoreActions
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,19 @@
|
|||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<Platforms>x64</Platforms>
|
||||||
|
<Configurations>Debug;Release</Configurations>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
|
<PackageReference Include="Meziantou.Analyzer" Version="2.0.92">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(IS_BENCH)'=='1'">
|
||||||
|
<DefineConstants>$(DefineConstants);IS_DETERMINISTIC</DefineConstants>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -82,6 +82,11 @@ public record struct Effects
|
|||||||
_ => 0
|
_ => 0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static bool IsIndefinite(EffectType effect) =>
|
||||||
|
effect is EffectType.InnerQuiet or EffectType.HeartAndSoul;
|
||||||
|
|
||||||
[Pure]
|
[Pure]
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public readonly byte GetStrength(EffectType effect) =>
|
public readonly byte GetStrength(EffectType effect) =>
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ public record struct SimulationState
|
|||||||
74, 76, 78, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 94, 96, 98, 100
|
74, 76, 78, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 94, 96, 98, 100
|
||||||
};
|
};
|
||||||
public readonly int HQPercent => HQPercentTable[(int)Math.Clamp((float)Quality / Input.Recipe.MaxQuality * 100, 0, 100)];
|
public readonly int HQPercent => HQPercentTable[(int)Math.Clamp((float)Quality / Input.Recipe.MaxQuality * 100, 0, 100)];
|
||||||
|
public readonly int Collectability => Math.Max(Quality / 10, 1);
|
||||||
|
public readonly int MaxCollectability => Math.Max(Input.Recipe.MaxQuality / 10, 1);
|
||||||
|
|
||||||
public readonly bool IsFirstStep => StepCount == 0;
|
public readonly bool IsFirstStep => StepCount == 0;
|
||||||
|
|
||||||
|
|||||||
+46
-30
@@ -21,8 +21,17 @@ public class Simulator
|
|||||||
|
|
||||||
public bool IsFirstStep => State.StepCount == 0;
|
public bool IsFirstStep => State.StepCount == 0;
|
||||||
|
|
||||||
public CompletionState CompletionState => CalculateCompletionState(State);
|
public virtual CompletionState CompletionState {
|
||||||
public virtual bool IsComplete => CompletionState != CompletionState.Incomplete;
|
get
|
||||||
|
{
|
||||||
|
if (Progress >= Input.Recipe.MaxProgress)
|
||||||
|
return CompletionState.ProgressComplete;
|
||||||
|
if (Durability <= 0)
|
||||||
|
return CompletionState.NoMoreDurability;
|
||||||
|
return CompletionState.Incomplete;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool IsComplete => CompletionState != CompletionState.Incomplete;
|
||||||
|
|
||||||
public IEnumerable<ActionType> AvailableActions => ActionUtils.AvailableActions(this);
|
public IEnumerable<ActionType> AvailableActions => ActionUtils.AvailableActions(this);
|
||||||
|
|
||||||
@@ -54,6 +63,8 @@ public class Simulator
|
|||||||
return ActionResponse.ActionNotUnlocked;
|
return ActionResponse.ActionNotUnlocked;
|
||||||
if (action == ActionType.Manipulation && !Input.Stats.CanUseManipulation)
|
if (action == ActionType.Manipulation && !Input.Stats.CanUseManipulation)
|
||||||
return ActionResponse.ActionNotUnlocked;
|
return ActionResponse.ActionNotUnlocked;
|
||||||
|
if (action is ActionType.CarefulObservation or ActionType.HeartAndSoul && !Input.Stats.IsSpecialist)
|
||||||
|
return ActionResponse.ActionNotUnlocked;
|
||||||
if (baseAction.CPCost(this) > CP)
|
if (baseAction.CPCost(this) > CP)
|
||||||
return ActionResponse.NotEnoughCP;
|
return ActionResponse.NotEnoughCP;
|
||||||
return ActionResponse.CannotUseAction;
|
return ActionResponse.CannotUseAction;
|
||||||
@@ -64,6 +75,20 @@ public class Simulator
|
|||||||
return ActionResponse.UsedAction;
|
return ActionResponse.UsedAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public (ActionResponse Response, SimulationState NewState, int FailedActionIdx) ExecuteMultiple(SimulationState state, IEnumerable<ActionType> actions)
|
||||||
|
{
|
||||||
|
State = state;
|
||||||
|
var i = 0;
|
||||||
|
foreach(var action in actions)
|
||||||
|
{
|
||||||
|
var resp = Execute(action);
|
||||||
|
if (resp != ActionResponse.UsedAction)
|
||||||
|
return (resp, State, i);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return (ActionResponse.UsedAction, State, -1);
|
||||||
|
}
|
||||||
|
|
||||||
[Pure]
|
[Pure]
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public int GetEffectStrength(EffectType effect) =>
|
public int GetEffectStrength(EffectType effect) =>
|
||||||
@@ -188,51 +213,51 @@ public class Simulator
|
|||||||
return (int)Math.Ceiling(amt);
|
return (int)Math.Ceiling(amt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int CalculateProgressGain(float efficiency, bool dryRun = true)
|
public int CalculateProgressGain(int efficiency, bool dryRun = true)
|
||||||
{
|
{
|
||||||
var buffModifier = 1.00f;
|
var buffModifier = 100;
|
||||||
if (HasEffect(EffectType.MuscleMemory))
|
if (HasEffect(EffectType.MuscleMemory))
|
||||||
{
|
{
|
||||||
buffModifier += 1.00f;
|
buffModifier += 100;
|
||||||
if (!dryRun)
|
if (!dryRun)
|
||||||
RemoveEffect(EffectType.MuscleMemory);
|
RemoveEffect(EffectType.MuscleMemory);
|
||||||
}
|
}
|
||||||
if (HasEffect(EffectType.Veneration))
|
if (HasEffect(EffectType.Veneration))
|
||||||
buffModifier += 0.50f;
|
buffModifier += 50;
|
||||||
|
|
||||||
var conditionModifier = Condition switch
|
var conditionModifier = Condition switch
|
||||||
{
|
{
|
||||||
Condition.Malleable => 1.50f,
|
Condition.Malleable => 150,
|
||||||
_ => 1.00f
|
_ => 100
|
||||||
};
|
};
|
||||||
|
|
||||||
var progressGain = (int)(Input.BaseProgressGain * efficiency * conditionModifier * buffModifier);
|
var progressGain = (int)((long)Input.BaseProgressGain * efficiency * conditionModifier * buffModifier / 1e6);
|
||||||
return progressGain;
|
return progressGain;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int CalculateQualityGain(float efficiency, bool dryRun = true)
|
public int CalculateQualityGain(int efficiency, bool dryRun = true)
|
||||||
{
|
{
|
||||||
var buffModifier = 1.00f;
|
var buffModifier = 100;
|
||||||
if (HasEffect(EffectType.GreatStrides))
|
if (HasEffect(EffectType.GreatStrides))
|
||||||
{
|
{
|
||||||
buffModifier += 1.00f;
|
buffModifier += 100;
|
||||||
if (!dryRun)
|
if (!dryRun)
|
||||||
RemoveEffect(EffectType.GreatStrides);
|
RemoveEffect(EffectType.GreatStrides);
|
||||||
}
|
}
|
||||||
if (HasEffect(EffectType.Innovation))
|
if (HasEffect(EffectType.Innovation))
|
||||||
buffModifier += 0.50f;
|
buffModifier += 50;
|
||||||
|
|
||||||
buffModifier *= 1 + (GetEffectStrength(EffectType.InnerQuiet) * 0.10f);
|
var iqModifier = 100 + (GetEffectStrength(EffectType.InnerQuiet) * 10);
|
||||||
|
|
||||||
var conditionModifier = Condition switch
|
var conditionModifier = Condition switch
|
||||||
{
|
{
|
||||||
Condition.Poor => 0.50f,
|
Condition.Poor => 50,
|
||||||
Condition.Good => Input.Stats.HasSplendorousBuff ? 1.75f : 1.50f,
|
Condition.Good => Input.Stats.HasSplendorousBuff ? 175 : 150,
|
||||||
Condition.Excellent => 4.00f,
|
Condition.Excellent => 400,
|
||||||
_ => 1.00f,
|
_ => 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
var qualityGain = (int)(Input.BaseQualityGain * efficiency * conditionModifier * buffModifier);
|
var qualityGain = (int)((long)Input.BaseQualityGain * efficiency * conditionModifier * iqModifier * buffModifier / 1e8);
|
||||||
return qualityGain;
|
return qualityGain;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,19 +297,10 @@ public class Simulator
|
|||||||
ReduceCPRaw(CalculateCPCost(amount));
|
ReduceCPRaw(CalculateCPCost(amount));
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void IncreaseProgress(float efficiency) =>
|
public void IncreaseProgress(int efficiency) =>
|
||||||
IncreaseProgressRaw(CalculateProgressGain(efficiency, false));
|
IncreaseProgressRaw(CalculateProgressGain(efficiency, false));
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void IncreaseQuality(float efficiency) =>
|
public void IncreaseQuality(int efficiency) =>
|
||||||
IncreaseQualityRaw(CalculateQualityGain(efficiency, false));
|
IncreaseQualityRaw(CalculateQualityGain(efficiency, false));
|
||||||
|
|
||||||
public static CompletionState CalculateCompletionState(SimulationState state)
|
|
||||||
{
|
|
||||||
if (state.Progress >= state.Input.Recipe.MaxProgress)
|
|
||||||
return CompletionState.ProgressComplete;
|
|
||||||
if (state.Durability <= 0)
|
|
||||||
return CompletionState.NoMoreDurability;
|
|
||||||
return CompletionState.Incomplete;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
using Craftimizer.Simulator.Actions;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Craftimizer.Solver;
|
||||||
|
|
||||||
|
public struct ActionSet
|
||||||
|
{
|
||||||
|
private uint bits;
|
||||||
|
|
||||||
|
public static readonly ActionType[] AcceptedActions = new[]
|
||||||
|
{
|
||||||
|
ActionType.StandardTouchCombo,
|
||||||
|
ActionType.AdvancedTouchCombo,
|
||||||
|
ActionType.FocusedTouchCombo,
|
||||||
|
ActionType.FocusedSynthesisCombo,
|
||||||
|
ActionType.TrainedFinesse,
|
||||||
|
ActionType.PrudentSynthesis,
|
||||||
|
ActionType.Groundwork,
|
||||||
|
ActionType.AdvancedTouch,
|
||||||
|
ActionType.CarefulSynthesis,
|
||||||
|
ActionType.TrainedEye,
|
||||||
|
ActionType.DelicateSynthesis,
|
||||||
|
ActionType.PreparatoryTouch,
|
||||||
|
ActionType.Reflect,
|
||||||
|
ActionType.FocusedTouch,
|
||||||
|
ActionType.FocusedSynthesis,
|
||||||
|
ActionType.PrudentTouch,
|
||||||
|
ActionType.Manipulation,
|
||||||
|
ActionType.MuscleMemory,
|
||||||
|
ActionType.ByregotsBlessing,
|
||||||
|
ActionType.WasteNot2,
|
||||||
|
ActionType.BasicSynthesis,
|
||||||
|
ActionType.Innovation,
|
||||||
|
ActionType.GreatStrides,
|
||||||
|
ActionType.StandardTouch,
|
||||||
|
ActionType.Veneration,
|
||||||
|
ActionType.WasteNot,
|
||||||
|
ActionType.Observe,
|
||||||
|
ActionType.MastersMend,
|
||||||
|
ActionType.BasicTouch,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly int[] AcceptedActionsLUT;
|
||||||
|
|
||||||
|
static ActionSet()
|
||||||
|
{
|
||||||
|
AcceptedActionsLUT = new int[Enum.GetValues<ActionType>().Length];
|
||||||
|
for (var i = 0; i < AcceptedActionsLUT.Length; i++)
|
||||||
|
AcceptedActionsLUT[i] = -1;
|
||||||
|
for (var i = 0; i < AcceptedActions.Length; i++)
|
||||||
|
AcceptedActionsLUT[(byte)AcceptedActions[i]] = i;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static int FromAction(ActionType action)
|
||||||
|
{
|
||||||
|
var ret = AcceptedActionsLUT[(byte)action];
|
||||||
|
if (ret == -1)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(action), action, $"Action {action} is unsupported in {nameof(ActionSet)}.");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
[Pure]
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static ActionType ToAction(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= AcceptedActions.Length)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(index), index, $"Index {index} is out of range for {nameof(ActionSet)}.");
|
||||||
|
return AcceptedActions[index];
|
||||||
|
}
|
||||||
|
[Pure]
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static uint ToMask(ActionType action) => 1u << (FromAction(action) + 1);
|
||||||
|
|
||||||
|
// Return true if action was newly added and not there before.
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool AddAction(ActionType action)
|
||||||
|
{
|
||||||
|
var mask = ToMask(action);
|
||||||
|
var old = bits;
|
||||||
|
bits |= mask;
|
||||||
|
return (old & mask) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return true if action was newly removed and not already gone.
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool RemoveAction(ActionType action)
|
||||||
|
{
|
||||||
|
var mask = ToMask(action);
|
||||||
|
var old = bits;
|
||||||
|
bits &= ~mask;
|
||||||
|
return (old & mask) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public readonly bool HasAction(ActionType action) => (bits & ToMask(action)) != 0;
|
||||||
|
[Pure]
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public readonly ActionType ElementAt(int index) => ToAction(Intrinsics.NthBitSet(bits, index) - 1);
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
public readonly int Count => BitOperations.PopCount(bits);
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
public readonly bool IsEmpty => bits == 0;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public readonly ActionType SelectRandom(Random random)
|
||||||
|
{
|
||||||
|
#if IS_DETERMINISTIC
|
||||||
|
return First();
|
||||||
|
#else
|
||||||
|
return ElementAt(random.Next(Count));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public ActionType PopRandom(Random random)
|
||||||
|
{
|
||||||
|
#if IS_DETERMINISTIC
|
||||||
|
return PopFirst();
|
||||||
|
#else
|
||||||
|
var action = ElementAt(random.Next(Count));
|
||||||
|
RemoveAction(action);
|
||||||
|
return action;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if IS_DETERMINISTIC
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private ActionType PopFirst()
|
||||||
|
{
|
||||||
|
var action = First();
|
||||||
|
RemoveAction(action);
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private readonly ActionType First() => ElementAt(0);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ using System.Diagnostics.Contracts;
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Craftimizer.Solver.Crafty;
|
namespace Craftimizer.Solver;
|
||||||
|
|
||||||
// Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs
|
// Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs
|
||||||
public struct ArenaBuffer<T> where T : struct
|
public struct ArenaBuffer<T> where T : struct
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Craftimizer.Solver.Crafty;
|
namespace Craftimizer.Solver;
|
||||||
|
|
||||||
public sealed class ArenaNode<T> where T : struct
|
public sealed class ArenaNode<T> where T : struct
|
||||||
{
|
{
|
||||||
@@ -5,10 +5,12 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||||
|
<Platforms>x64</Platforms>
|
||||||
|
<Configurations>Debug;Release</Configurations>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
|
<PackageReference Include="Meziantou.Analyzer" Version="2.0.92">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
@@ -18,4 +20,8 @@
|
|||||||
<ProjectReference Include="..\Simulator\Craftimizer.Simulator.csproj" />
|
<ProjectReference Include="..\Simulator\Craftimizer.Simulator.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="'$(IS_BENCH)'=='1'">
|
||||||
|
<DefineConstants>$(DefineConstants);IS_DETERMINISTIC</DefineConstants>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
using Craftimizer.Simulator.Actions;
|
|
||||||
using System.Diagnostics.Contracts;
|
|
||||||
using System.Numerics;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace Craftimizer.Solver.Crafty;
|
|
||||||
|
|
||||||
public struct ActionSet
|
|
||||||
{
|
|
||||||
private const bool IsDeterministic = false;
|
|
||||||
|
|
||||||
private uint bits;
|
|
||||||
|
|
||||||
[Pure]
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private static int FromAction(ActionType action) => Simulator.AcceptedActionsLUT[(byte)action];
|
|
||||||
[Pure]
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private static ActionType ToAction(int index) => Simulator.AcceptedActions[index];
|
|
||||||
[Pure]
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private static uint ToMask(ActionType action) => 1u << FromAction(action) + 1;
|
|
||||||
|
|
||||||
// Return true if action was newly added and not there before.
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public bool AddAction(ActionType action)
|
|
||||||
{
|
|
||||||
var mask = ToMask(action);
|
|
||||||
var old = bits;
|
|
||||||
bits |= mask;
|
|
||||||
return (old & mask) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return true if action was newly removed and not already gone.
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public bool RemoveAction(ActionType action)
|
|
||||||
{
|
|
||||||
var mask = ToMask(action);
|
|
||||||
var old = bits;
|
|
||||||
bits &= ~mask;
|
|
||||||
return (old & mask) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Pure]
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public readonly bool HasAction(ActionType action) => (bits & ToMask(action)) != 0;
|
|
||||||
[Pure]
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public readonly ActionType ElementAt(int index) => ToAction(Intrinsics.NthBitSet(bits, index) - 1);
|
|
||||||
|
|
||||||
[Pure]
|
|
||||||
public readonly int Count => BitOperations.PopCount(bits);
|
|
||||||
|
|
||||||
[Pure]
|
|
||||||
public readonly bool IsEmpty => bits == 0;
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public readonly ActionType SelectRandom(Random random)
|
|
||||||
{
|
|
||||||
if (IsDeterministic)
|
|
||||||
return First();
|
|
||||||
|
|
||||||
return ElementAt(random.Next(Count));
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public ActionType PopRandom(Random random)
|
|
||||||
{
|
|
||||||
if (IsDeterministic)
|
|
||||||
return PopFirst();
|
|
||||||
|
|
||||||
var action = ElementAt(random.Next(Count));
|
|
||||||
RemoveAction(action);
|
|
||||||
return action;
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public ActionType PopFirst()
|
|
||||||
{
|
|
||||||
var action = First();
|
|
||||||
RemoveAction(action);
|
|
||||||
return action;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Pure]
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
public readonly ActionType First() => ElementAt(0);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using CompState = Craftimizer.Simulator.CompletionState;
|
|
||||||
|
|
||||||
namespace Craftimizer.Solver.Crafty;
|
|
||||||
|
|
||||||
public enum CompletionState : byte
|
|
||||||
{
|
|
||||||
Incomplete,
|
|
||||||
ProgressComplete,
|
|
||||||
NoMoreDurability,
|
|
||||||
|
|
||||||
InvalidAction,
|
|
||||||
MaxActionCountReached,
|
|
||||||
NoMoreActions
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static class CompletionStateUtils
|
|
||||||
{
|
|
||||||
public static CompState IntoBase(this CompletionState me) =>
|
|
||||||
(CompState)me >= CompState.Other ? CompState.Other : (CompState)me;
|
|
||||||
}
|
|
||||||
@@ -1,590 +0,0 @@
|
|||||||
using Craftimizer.Simulator;
|
|
||||||
using Craftimizer.Simulator.Actions;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.Contracts;
|
|
||||||
using System.Numerics;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Text;
|
|
||||||
using Node = Craftimizer.Solver.Crafty.ArenaNode<Craftimizer.Solver.Crafty.SimulationNode>;
|
|
||||||
|
|
||||||
namespace Craftimizer.Solver.Crafty;
|
|
||||||
|
|
||||||
// https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/simulator.rs
|
|
||||||
public sealed class Solver
|
|
||||||
{
|
|
||||||
private SolverConfig config;
|
|
||||||
private Node rootNode;
|
|
||||||
private RootScores rootScores;
|
|
||||||
|
|
||||||
public float MaxScore => rootScores.MaxScore;
|
|
||||||
|
|
||||||
public Solver(SolverConfig config, SimulationState state)
|
|
||||||
{
|
|
||||||
this.config = config;
|
|
||||||
var sim = new Simulator(state, config.MaxStepCount);
|
|
||||||
rootNode = new(new(
|
|
||||||
state,
|
|
||||||
null,
|
|
||||||
sim.CompletionState,
|
|
||||||
sim.AvailableActionsHeuristic(config.StrictActions)
|
|
||||||
));
|
|
||||||
rootScores = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SimulationNode Execute(Simulator simulator, SimulationState state, ActionType action, bool strict)
|
|
||||||
{
|
|
||||||
(_, var newState) = simulator.Execute(state, action);
|
|
||||||
return new(
|
|
||||||
newState,
|
|
||||||
action,
|
|
||||||
simulator.CompletionState,
|
|
||||||
simulator.AvailableActionsHeuristic(strict)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Node ExecuteActions(Simulator simulator, Node startNode, ReadOnlySpan<ActionType> actions, bool strict)
|
|
||||||
{
|
|
||||||
foreach (var action in actions)
|
|
||||||
{
|
|
||||||
var state = startNode.State;
|
|
||||||
if (state.IsComplete)
|
|
||||||
return startNode;
|
|
||||||
|
|
||||||
if (!state.AvailableActions.HasAction(action))
|
|
||||||
return startNode;
|
|
||||||
state.AvailableActions.RemoveAction(action);
|
|
||||||
|
|
||||||
startNode = startNode.Add(Execute(simulator, state.State, action, strict));
|
|
||||||
}
|
|
||||||
|
|
||||||
return startNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Pure]
|
|
||||||
private SolverSolution Solution()
|
|
||||||
{
|
|
||||||
var actions = new List<ActionType>();
|
|
||||||
var node = rootNode;
|
|
||||||
|
|
||||||
while (node.Children.Count != 0)
|
|
||||||
{
|
|
||||||
node = node.ChildAt(ChildMaxScore(ref node.ChildScores))!;
|
|
||||||
|
|
||||||
if (node.State.Action != null)
|
|
||||||
actions.Add(node.State.Action.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
//var at = node.ChildIdx;
|
|
||||||
//ref var sum = ref node.ParentScores!.Value.Data[at.arrayIdx].ScoreSum.Span[at.subIdx];
|
|
||||||
//ref var max = ref node.ParentScores!.Value.Data[at.arrayIdx].MaxScore.Span[at.subIdx];
|
|
||||||
//ref var visits = ref node.ParentScores!.Value.Data[at.arrayIdx].Visits.Span[at.subIdx];
|
|
||||||
//Console.WriteLine($"{sum} {max} {visits}");
|
|
||||||
|
|
||||||
return new(actions, node.State.State);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Pure]
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private static (int arrayIdx, int subIdx) ChildMaxScore(ref NodeScoresBuffer scores)
|
|
||||||
{
|
|
||||||
var length = scores.Count;
|
|
||||||
var vecLength = Vector<float>.Count;
|
|
||||||
|
|
||||||
var max = (0, 0);
|
|
||||||
var maxScore = 0f;
|
|
||||||
for (var i = 0; length > 0; ++i)
|
|
||||||
{
|
|
||||||
var iterCount = Math.Min(vecLength, length);
|
|
||||||
|
|
||||||
ref var chunk = ref scores.Data[i];
|
|
||||||
var m = new Vector<float>(chunk.MaxScore.Span);
|
|
||||||
|
|
||||||
var idx = Intrinsics.HMaxIndex(m, iterCount);
|
|
||||||
|
|
||||||
if (m[idx] >= maxScore)
|
|
||||||
{
|
|
||||||
max = (i, idx);
|
|
||||||
maxScore = m[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
length -= iterCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculates the best child node to explore next
|
|
||||||
// Exploitation: ((1 - w) * (s / v)) + (w * m)
|
|
||||||
// Exploration: sqrt(c * ln(V) / v)
|
|
||||||
// w = maxScoreWeightingConstant
|
|
||||||
// s = score sum
|
|
||||||
// m = max score
|
|
||||||
// v = visits
|
|
||||||
// V = parentVisits
|
|
||||||
// c = explorationConstant
|
|
||||||
|
|
||||||
// Somewhat based off of https://en.wikipedia.org/wiki/Monte_Carlo_tree_search#Exploration_and_exploitation
|
|
||||||
// Here, w_i = (1-w)*score sum
|
|
||||||
// n_i = visits
|
|
||||||
// max score is tacked onto it
|
|
||||||
// N_i = parent visits
|
|
||||||
// c = exploration constant (but crafty places it inside the sqrt..?)
|
|
||||||
[Pure]
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
|
||||||
private (int arrayIdx, int subIdx) EvalBestChild(int parentVisits, ref NodeScoresBuffer scores)
|
|
||||||
{
|
|
||||||
var length = scores.Count;
|
|
||||||
var vecLength = Vector<float>.Count;
|
|
||||||
|
|
||||||
var C = MathF.Sqrt(config.ExplorationConstant * MathF.Log(parentVisits));
|
|
||||||
var w = config.MaxScoreWeightingConstant;
|
|
||||||
var W = 1f - w;
|
|
||||||
var CVector = new Vector<float>(C);
|
|
||||||
|
|
||||||
var max = (0, 0);
|
|
||||||
var maxScore = 0f;
|
|
||||||
for (var i = 0; length > 0; ++i)
|
|
||||||
{
|
|
||||||
var iterCount = Math.Min(vecLength, length);
|
|
||||||
|
|
||||||
ref var chunk = ref scores.Data[i];
|
|
||||||
var s = new Vector<float>(chunk.ScoreSum.Span);
|
|
||||||
var vInt = new Vector<int>(chunk.Visits.Span);
|
|
||||||
var m = new Vector<float>(chunk.MaxScore.Span);
|
|
||||||
|
|
||||||
vInt = Vector.Max(vInt, Vector<int>.One);
|
|
||||||
var v = Vector.ConvertToSingle(vInt);
|
|
||||||
|
|
||||||
var exploitation = (W * (s / v)) + (w * m);
|
|
||||||
var exploration = CVector * Intrinsics.ReciprocalSqrt(v);
|
|
||||||
var evalScores = exploitation + exploration;
|
|
||||||
|
|
||||||
var idx = Intrinsics.HMaxIndex(evalScores, iterCount);
|
|
||||||
|
|
||||||
if (evalScores[idx] >= maxScore)
|
|
||||||
{
|
|
||||||
max = (i, idx);
|
|
||||||
maxScore = evalScores[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
length -= iterCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
return max;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Pure]
|
|
||||||
public Node Select()
|
|
||||||
{
|
|
||||||
var node = rootNode;
|
|
||||||
var nodeVisits = rootScores.Visits;
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var expandable = !node.State.AvailableActions.IsEmpty;
|
|
||||||
var likelyTerminal = node.Children.Count == 0;
|
|
||||||
if (expandable || likelyTerminal)
|
|
||||||
return node;
|
|
||||||
|
|
||||||
// select the node with the highest score
|
|
||||||
var at = EvalBestChild(nodeVisits, ref node.ChildScores);
|
|
||||||
nodeVisits = node.ChildScores.GetVisits(at);
|
|
||||||
node = node.ChildAt(at)!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public (Node ExpandedNode, float Score) ExpandAndRollout(Random random, Simulator simulator, Node initialNode)
|
|
||||||
{
|
|
||||||
ref var initialState = ref initialNode.State;
|
|
||||||
// expand once
|
|
||||||
if (initialState.IsComplete)
|
|
||||||
return (initialNode, initialState.CalculateScore(config) ?? 0);
|
|
||||||
|
|
||||||
var poppedAction = initialState.AvailableActions.PopRandom(random);
|
|
||||||
var expandedNode = initialNode.Add(Execute(simulator, initialState.State, poppedAction, true));
|
|
||||||
|
|
||||||
// playout to a terminal state
|
|
||||||
var currentState = expandedNode.State.State;
|
|
||||||
var currentCompletionState = expandedNode.State.SimulationCompletionState;
|
|
||||||
var currentActions = expandedNode.State.AvailableActions;
|
|
||||||
|
|
||||||
|
|
||||||
byte actionCount = 0;
|
|
||||||
Span<ActionType> actions = stackalloc ActionType[Math.Min(config.MaxStepCount - currentState.ActionCount, config.MaxRolloutStepCount)];
|
|
||||||
while (SimulationNode.GetCompletionState(currentCompletionState, currentActions) == CompletionState.Incomplete &&
|
|
||||||
actionCount < actions.Length)
|
|
||||||
{
|
|
||||||
var nextAction = currentActions.SelectRandom(random);
|
|
||||||
actions[actionCount++] = nextAction;
|
|
||||||
(_, currentState) = simulator.Execute(currentState, nextAction);
|
|
||||||
currentCompletionState = simulator.CompletionState;
|
|
||||||
if (currentCompletionState != CompletionState.Incomplete)
|
|
||||||
break;
|
|
||||||
currentActions = simulator.AvailableActionsHeuristic(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// store the result if a max score was reached
|
|
||||||
var score = SimulationNode.CalculateScoreForState(currentState, currentCompletionState, config) ?? 0;
|
|
||||||
if (currentCompletionState == CompletionState.ProgressComplete)
|
|
||||||
{
|
|
||||||
if (score >= config.ScoreStorageThreshold && score >= MaxScore)
|
|
||||||
{
|
|
||||||
var terminalNode = ExecuteActions(simulator, expandedNode, actions[..actionCount], true);
|
|
||||||
return (terminalNode, score);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (expandedNode, score);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Backpropagate(Node startNode, float score)
|
|
||||||
{
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
if (startNode == rootNode)
|
|
||||||
{
|
|
||||||
rootScores.Visit(score);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
startNode.ParentScores!.Value.Visit(startNode.ChildIdx, score);
|
|
||||||
|
|
||||||
startNode = startNode.Parent!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowAllNodes()
|
|
||||||
{
|
|
||||||
static void ShowNodes(StringBuilder b, Node node, Stack<Node> path)
|
|
||||||
{
|
|
||||||
path.Push(node);
|
|
||||||
b.AppendLine($"{new string(' ', path.Count)}{node.State.Action}");
|
|
||||||
{
|
|
||||||
for (var i = 0; i < node.Children.Count; ++i)
|
|
||||||
{
|
|
||||||
var n = node.ChildAt((i >> 3, i & 7))!;
|
|
||||||
ShowNodes(b, n, path);
|
|
||||||
}
|
|
||||||
path.Pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var b = new StringBuilder();
|
|
||||||
ShowNodes(b, rootNode, new());
|
|
||||||
Console.WriteLine(b.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool AllNodesComplete()
|
|
||||||
{
|
|
||||||
static bool NodesIncomplete(Node node, Stack<Node> path)
|
|
||||||
{
|
|
||||||
path.Push(node);
|
|
||||||
if (node.Children.Count == 0)
|
|
||||||
{
|
|
||||||
if (!node.State.AvailableActions.IsEmpty)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
for (var i = 0; i < node.Children.Count; ++i)
|
|
||||||
{
|
|
||||||
var n = node.ChildAt((i >> 3, i & 7))!;
|
|
||||||
if (NodesIncomplete(n, path))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
path.Pop();
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !NodesIncomplete(rootNode, new());
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private void Search(int iterations, CancellationToken token)
|
|
||||||
{
|
|
||||||
Simulator simulator = new(rootNode.State.State, config.MaxStepCount);
|
|
||||||
var random = rootNode.State.State.Input.Random;
|
|
||||||
var n = 0;
|
|
||||||
for (var i = 0; i < iterations || MaxScore == 0; i++)
|
|
||||||
{
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var selectedNode = Select();
|
|
||||||
var (endNode, score) = ExpandAndRollout(random, simulator, selectedNode);
|
|
||||||
if (MaxScore == 0)
|
|
||||||
{
|
|
||||||
if (endNode == selectedNode)
|
|
||||||
{
|
|
||||||
if (n++ > 5000)
|
|
||||||
{
|
|
||||||
n = 0;
|
|
||||||
if (AllNodesComplete())
|
|
||||||
{
|
|
||||||
//Console.WriteLine("All nodes solved for. Can't find a valid solution.");
|
|
||||||
//ShowAllNodes();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
n = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Backpropagate(endNode, score);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SolverSolution SearchStepwiseFurcated(SolverConfig config, SimulationInput input, Action<ActionType>? actionCallback = null, CancellationToken token = default) =>
|
|
||||||
SearchStepwiseFurcated(config, new SimulationState(input), actionCallback, token);
|
|
||||||
|
|
||||||
public static SolverSolution SearchStepwiseFurcated(SolverConfig config, SimulationState state, Action<ActionType>? actionCallback = null, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
var definiteActionCount = 0;
|
|
||||||
var bestSims = new List<(float Score, SolverSolution Result)>();
|
|
||||||
|
|
||||||
var sim = new Simulator(state, config.MaxStepCount);
|
|
||||||
|
|
||||||
var activeStates = new List<SolverSolution>() { new(new(), state) };
|
|
||||||
|
|
||||||
while (activeStates.Count != 0)
|
|
||||||
{
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var s = Stopwatch.StartNew();
|
|
||||||
var tasks = new Task<(float MaxScore, int FurcatedActionIdx, SolverSolution Solution)>[config.ForkCount];
|
|
||||||
for (var i = 0; i < config.ForkCount; i++)
|
|
||||||
{
|
|
||||||
var stateIdx = (int)((float)i / config.ForkCount * activeStates.Count);
|
|
||||||
var st = activeStates[stateIdx];
|
|
||||||
tasks[i] = Task.Run(() =>
|
|
||||||
{
|
|
||||||
var solver = new Solver(config, activeStates[stateIdx].State);
|
|
||||||
solver.Search(config.Iterations / config.ForkCount, token);
|
|
||||||
return (solver.MaxScore, stateIdx, solver.Solution());
|
|
||||||
}, token);
|
|
||||||
}
|
|
||||||
Task.WaitAll(tasks, token);
|
|
||||||
s.Stop();
|
|
||||||
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var bestActions = tasks.Select(t => t.Result).OrderByDescending(r => r.MaxScore).Take(config.FurcatedActionCount).ToArray();
|
|
||||||
|
|
||||||
var bestAction = bestActions[0];
|
|
||||||
if (bestAction.MaxScore >= config.ScoreStorageThreshold)
|
|
||||||
{
|
|
||||||
var (maxScore, furcatedActionIdx, solution) = bestAction;
|
|
||||||
var (activeActions, activeState) = activeStates[furcatedActionIdx];
|
|
||||||
|
|
||||||
activeActions.AddRange(solution.Actions);
|
|
||||||
return solution with { Actions = activeActions };
|
|
||||||
}
|
|
||||||
|
|
||||||
var newStates = new List<SolverSolution>(config.FurcatedActionCount);
|
|
||||||
for (var i = 0; i < bestActions.Length; ++i)
|
|
||||||
{
|
|
||||||
var (maxScore, furcatedActionIdx, (solutionActions, solutionNode)) = bestActions[i];
|
|
||||||
if (solutionActions.Count == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var (activeActions, activeState) = activeStates[furcatedActionIdx];
|
|
||||||
|
|
||||||
var chosenAction = solutionActions[0];
|
|
||||||
|
|
||||||
var newActions = new List<ActionType>(activeActions) { chosenAction };
|
|
||||||
var newState = sim.Execute(activeState, chosenAction).NewState;
|
|
||||||
if (sim.IsComplete)
|
|
||||||
bestSims.Add((maxScore, new(newActions, newState)));
|
|
||||||
else
|
|
||||||
newStates.Add(new(newActions, newState));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bestSims.Count == 0 && newStates.Count != 0)
|
|
||||||
{
|
|
||||||
var definiteCount = definiteActionCount;
|
|
||||||
var equalCount = int.MaxValue;
|
|
||||||
var refActions = newStates[0].Actions;
|
|
||||||
for (var i = 1; i < newStates.Count; ++i)
|
|
||||||
{
|
|
||||||
var cmpActions = newStates[i].Actions;
|
|
||||||
var possibleCount = Math.Min(Math.Min(refActions.Count, cmpActions.Count), equalCount);
|
|
||||||
var completelyEqual = true;
|
|
||||||
for (var j = definiteCount; j < possibleCount; ++j)
|
|
||||||
{
|
|
||||||
if (refActions[j] != cmpActions[j])
|
|
||||||
{
|
|
||||||
equalCount = j;
|
|
||||||
completelyEqual = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (completelyEqual)
|
|
||||||
equalCount = possibleCount;
|
|
||||||
}
|
|
||||||
if (definiteCount != equalCount)
|
|
||||||
{
|
|
||||||
for (var i = definiteCount; i < equalCount; ++i)
|
|
||||||
actionCallback?.Invoke(refActions[i]);
|
|
||||||
|
|
||||||
definiteActionCount = equalCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activeStates = newStates;
|
|
||||||
|
|
||||||
Console.WriteLine($"{s.Elapsed.TotalMilliseconds:0.00}ms {config.Iterations / config.ForkCount / s.Elapsed.TotalSeconds / 1000:0.00} kI/s/t");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bestSims.Count == 0)
|
|
||||||
return new(new(), state);
|
|
||||||
|
|
||||||
var result = bestSims.MaxBy(s => s.Score).Result;
|
|
||||||
for (var i = definiteActionCount; i < result.Actions.Count; ++i)
|
|
||||||
actionCallback?.Invoke(result.Actions[i]);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SolverSolution SearchStepwiseForked(SolverConfig config, SimulationInput input, Action<ActionType>? actionCallback = null, CancellationToken token = default) =>
|
|
||||||
SearchStepwiseForked(config, new SimulationState(input), actionCallback, token);
|
|
||||||
|
|
||||||
public static SolverSolution SearchStepwiseForked(SolverConfig config, SimulationState state, Action<ActionType>? actionCallback = null, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
var actions = new List<ActionType>();
|
|
||||||
var sim = new Simulator(state, config.MaxStepCount);
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (sim.IsComplete)
|
|
||||||
break;
|
|
||||||
|
|
||||||
|
|
||||||
var s = Stopwatch.StartNew();
|
|
||||||
var tasks = new Task<(float MaxScore, SolverSolution Solution)>[config.ForkCount];
|
|
||||||
for (var i = 0; i < config.ForkCount; ++i)
|
|
||||||
tasks[i] = Task.Run(() =>
|
|
||||||
{
|
|
||||||
var solver = new Solver(config, state);
|
|
||||||
solver.Search(config.Iterations / config.ForkCount, token);
|
|
||||||
return (solver.MaxScore, solver.Solution());
|
|
||||||
}, token);
|
|
||||||
Task.WaitAll(tasks, token);
|
|
||||||
s.Stop();
|
|
||||||
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var (maxScore, solution) = tasks.Select(t => t.Result).MaxBy(r => r.MaxScore);
|
|
||||||
|
|
||||||
if (maxScore >= config.ScoreStorageThreshold)
|
|
||||||
{
|
|
||||||
actions.AddRange(solution.Actions);
|
|
||||||
return solution with { Actions = actions };
|
|
||||||
}
|
|
||||||
|
|
||||||
var chosenAction = solution.Actions[0];
|
|
||||||
actionCallback?.Invoke(chosenAction);
|
|
||||||
Console.WriteLine($"{s.Elapsed.TotalMilliseconds:0.00}ms {config.Iterations / config.ForkCount / s.Elapsed.TotalSeconds / 1000:0.00} kI/s/t");
|
|
||||||
|
|
||||||
(_, state) = sim.Execute(state, chosenAction);
|
|
||||||
actions.Add(chosenAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new(actions, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SolverSolution SearchStepwise(SolverConfig config, SimulationInput input, Action<ActionType>? actionCallback = null, CancellationToken token = default) =>
|
|
||||||
SearchStepwise(config, new SimulationState(input), actionCallback, token);
|
|
||||||
|
|
||||||
public static SolverSolution SearchStepwise(SolverConfig config, SimulationState state, Action<ActionType>? actionCallback = null, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
var actions = new List<ActionType>();
|
|
||||||
var sim = new Simulator(state, config.MaxStepCount);
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
if (token.IsCancellationRequested)
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (sim.IsComplete)
|
|
||||||
break;
|
|
||||||
|
|
||||||
var solver = new Solver(config, state);
|
|
||||||
|
|
||||||
var s = Stopwatch.StartNew();
|
|
||||||
solver.Search(config.Iterations, token);
|
|
||||||
s.Stop();
|
|
||||||
|
|
||||||
var solution = solver.Solution();
|
|
||||||
|
|
||||||
if (solver.MaxScore >= config.ScoreStorageThreshold)
|
|
||||||
{
|
|
||||||
actions.AddRange(solution.Actions);
|
|
||||||
return solution with { Actions = actions };
|
|
||||||
}
|
|
||||||
|
|
||||||
var chosenAction = solution.Actions[0];
|
|
||||||
actionCallback?.Invoke(chosenAction);
|
|
||||||
Console.WriteLine($"{s.Elapsed.TotalMilliseconds:0.00}ms {config.Iterations / s.Elapsed.TotalSeconds / 1000:0.00} kI/s");
|
|
||||||
|
|
||||||
(_, state) = sim.Execute(state, chosenAction);
|
|
||||||
actions.Add(chosenAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new(actions, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SolverSolution SearchOneshotForked(SolverConfig config, SimulationInput input, Action<ActionType>? actionCallback = null, CancellationToken token = default) =>
|
|
||||||
SearchOneshotForked(config, new SimulationState(input), actionCallback, token);
|
|
||||||
|
|
||||||
public static SolverSolution SearchOneshotForked(SolverConfig config, SimulationState state, Action<ActionType>? actionCallback = null, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
var tasks = new Task<(float MaxScore, SolverSolution Solution)>[config.ForkCount];
|
|
||||||
for (var i = 0; i < config.ForkCount; ++i)
|
|
||||||
tasks[i] = Task.Run(() =>
|
|
||||||
{
|
|
||||||
var solver = new Solver(config, state);
|
|
||||||
solver.Search(config.Iterations / config.ForkCount, token);
|
|
||||||
return (solver.MaxScore, solver.Solution());
|
|
||||||
}, token);
|
|
||||||
Task.WaitAll(tasks, CancellationToken.None);
|
|
||||||
|
|
||||||
var solution = tasks.Select(t => t.Result).MaxBy(r => r.MaxScore).Solution;
|
|
||||||
foreach (var action in solution.Actions)
|
|
||||||
actionCallback?.Invoke(action);
|
|
||||||
|
|
||||||
return solution;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SolverSolution SearchOneshot(SolverConfig config, SimulationInput input, Action<ActionType>? actionCallback = null, CancellationToken token = default) =>
|
|
||||||
SearchOneshot(config, new SimulationState(input), actionCallback, token);
|
|
||||||
|
|
||||||
public static SolverSolution SearchOneshot(SolverConfig config, SimulationState state, Action<ActionType>? actionCallback = null, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
var solver = new Solver(config, state);
|
|
||||||
solver.Search(config.Iterations, token);
|
|
||||||
var solution = solver.Solution();
|
|
||||||
foreach (var action in solution.Actions)
|
|
||||||
actionCallback?.Invoke(action);
|
|
||||||
|
|
||||||
return solution;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SolverSolution Search(SolverConfig config, SimulationInput input, Action<ActionType>? actionCallback = null, CancellationToken token = default) =>
|
|
||||||
Search(config, new SimulationState(input), actionCallback, token);
|
|
||||||
|
|
||||||
public static SolverSolution Search(SolverConfig config, SimulationState state, Action<ActionType>? actionCallback = null, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
Func<SolverConfig, SimulationState, Action<ActionType>?, CancellationToken, SolverSolution> func = config.Algorithm switch
|
|
||||||
{
|
|
||||||
SolverAlgorithm.Oneshot => SearchOneshot,
|
|
||||||
SolverAlgorithm.OneshotForked => SearchOneshotForked,
|
|
||||||
SolverAlgorithm.Stepwise => SearchStepwise,
|
|
||||||
SolverAlgorithm.StepwiseForked => SearchStepwiseForked,
|
|
||||||
SolverAlgorithm.StepwiseFurcated or _ => SearchStepwiseFurcated,
|
|
||||||
};
|
|
||||||
return func(config, state, actionCallback, token);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
using Craftimizer.Simulator;
|
|
||||||
using Craftimizer.Simulator.Actions;
|
|
||||||
|
|
||||||
namespace Craftimizer.Solver.Crafty;
|
|
||||||
|
|
||||||
public readonly record struct SolverSolution(List<ActionType> Actions, SimulationState State);
|
|
||||||
@@ -4,7 +4,7 @@ using System.Runtime.CompilerServices;
|
|||||||
using System.Runtime.Intrinsics;
|
using System.Runtime.Intrinsics;
|
||||||
using System.Runtime.Intrinsics.X86;
|
using System.Runtime.Intrinsics.X86;
|
||||||
|
|
||||||
namespace Craftimizer.Solver.Crafty;
|
namespace Craftimizer.Solver;
|
||||||
internal static class Intrinsics
|
internal static class Intrinsics
|
||||||
{
|
{
|
||||||
[Pure]
|
[Pure]
|
||||||
@@ -50,7 +50,7 @@ internal static class Intrinsics
|
|||||||
var vcmp = Avx.CompareEqual(vfilt, vmax);
|
var vcmp = Avx.CompareEqual(vfilt, vmax);
|
||||||
var mask = unchecked((uint)Avx2.MoveMask(vcmp.AsByte()));
|
var mask = unchecked((uint)Avx2.MoveMask(vcmp.AsByte()));
|
||||||
|
|
||||||
var inverseIdx = BitOperations.LeadingZeroCount(mask << ((8 - len) << 2)) >> 2;
|
var inverseIdx = BitOperations.LeadingZeroCount(mask << (8 - len << 2)) >> 2;
|
||||||
|
|
||||||
return len - 1 - inverseIdx;
|
return len - 1 - inverseIdx;
|
||||||
}
|
}
|
||||||
+337
@@ -0,0 +1,337 @@
|
|||||||
|
using Craftimizer.Simulator.Actions;
|
||||||
|
using Craftimizer.Simulator;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using Node = Craftimizer.Solver.ArenaNode<Craftimizer.Solver.SimulationNode>;
|
||||||
|
|
||||||
|
namespace Craftimizer.Solver;
|
||||||
|
|
||||||
|
// https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/simulator.rs
|
||||||
|
public sealed class MCTS
|
||||||
|
{
|
||||||
|
private readonly MCTSConfig config;
|
||||||
|
private readonly Node rootNode;
|
||||||
|
private readonly RootScores rootScores;
|
||||||
|
|
||||||
|
public float MaxScore => rootScores.MaxScore;
|
||||||
|
|
||||||
|
public MCTS(MCTSConfig config, SimulationState state)
|
||||||
|
{
|
||||||
|
this.config = config;
|
||||||
|
var sim = new Simulator(state, config.MaxStepCount);
|
||||||
|
rootNode = new(new(
|
||||||
|
state,
|
||||||
|
null,
|
||||||
|
sim.CompletionState,
|
||||||
|
sim.AvailableActionsHeuristic(config.StrictActions)
|
||||||
|
));
|
||||||
|
rootScores = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SimulationNode Execute(Simulator simulator, SimulationState state, ActionType action, bool strict)
|
||||||
|
{
|
||||||
|
(_, var newState) = simulator.Execute(state, action);
|
||||||
|
return new(
|
||||||
|
newState,
|
||||||
|
action,
|
||||||
|
simulator.CompletionState,
|
||||||
|
simulator.AvailableActionsHeuristic(strict)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Node ExecuteActions(Simulator simulator, Node startNode, ReadOnlySpan<ActionType> actions, bool strict)
|
||||||
|
{
|
||||||
|
foreach (var action in actions)
|
||||||
|
{
|
||||||
|
var state = startNode.State;
|
||||||
|
if (state.IsComplete)
|
||||||
|
return startNode;
|
||||||
|
|
||||||
|
if (!state.AvailableActions.HasAction(action))
|
||||||
|
return startNode;
|
||||||
|
state.AvailableActions.RemoveAction(action);
|
||||||
|
|
||||||
|
startNode = startNode.Add(Execute(simulator, state.State, action, strict));
|
||||||
|
}
|
||||||
|
|
||||||
|
return startNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static (int arrayIdx, int subIdx) ChildMaxScore(ref NodeScoresBuffer scores)
|
||||||
|
{
|
||||||
|
var length = scores.Count;
|
||||||
|
var vecLength = Vector<float>.Count;
|
||||||
|
|
||||||
|
var max = (0, 0);
|
||||||
|
var maxScore = 0f;
|
||||||
|
for (var i = 0; length > 0; ++i)
|
||||||
|
{
|
||||||
|
var iterCount = Math.Min(vecLength, length);
|
||||||
|
|
||||||
|
ref var chunk = ref scores.Data[i];
|
||||||
|
var m = new Vector<float>(chunk.MaxScore.Span);
|
||||||
|
|
||||||
|
var idx = Intrinsics.HMaxIndex(m, iterCount);
|
||||||
|
|
||||||
|
if (m[idx] >= maxScore)
|
||||||
|
{
|
||||||
|
max = (i, idx);
|
||||||
|
maxScore = m[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
length -= iterCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates the best child node to explore next
|
||||||
|
// Exploitation: ((1 - w) * (s / v)) + (w * m)
|
||||||
|
// Exploration: sqrt(c * ln(V) / v)
|
||||||
|
// w = maxScoreWeightingConstant
|
||||||
|
// s = score sum
|
||||||
|
// m = max score
|
||||||
|
// v = visits
|
||||||
|
// V = parentVisits
|
||||||
|
// c = explorationConstant
|
||||||
|
|
||||||
|
// Somewhat based off of https://en.wikipedia.org/wiki/Monte_Carlo_tree_search#Exploration_and_exploitation
|
||||||
|
// Here, w_i = (1-w)*score sum
|
||||||
|
// n_i = visits
|
||||||
|
// max score is tacked onto it
|
||||||
|
// N_i = parent visits
|
||||||
|
// c = exploration constant (but crafty places it inside the sqrt..?)
|
||||||
|
[Pure]
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private static (int arrayIdx, int subIdx) EvalBestChild(
|
||||||
|
float explorationConstant,
|
||||||
|
float maxScoreWeightingConstant,
|
||||||
|
int parentVisits,
|
||||||
|
ref NodeScoresBuffer scores)
|
||||||
|
{
|
||||||
|
var length = scores.Count;
|
||||||
|
var vecLength = Vector<float>.Count;
|
||||||
|
|
||||||
|
var C = MathF.Sqrt(explorationConstant * MathF.Log(parentVisits));
|
||||||
|
var w = maxScoreWeightingConstant;
|
||||||
|
var W = 1f - w;
|
||||||
|
var CVector = new Vector<float>(C);
|
||||||
|
|
||||||
|
var max = (0, 0);
|
||||||
|
var maxScore = 0f;
|
||||||
|
for (var i = 0; length > 0; ++i)
|
||||||
|
{
|
||||||
|
var iterCount = Math.Min(vecLength, length);
|
||||||
|
|
||||||
|
ref var chunk = ref scores.Data[i];
|
||||||
|
var s = new Vector<float>(chunk.ScoreSum.Span);
|
||||||
|
var vInt = new Vector<int>(chunk.Visits.Span);
|
||||||
|
var m = new Vector<float>(chunk.MaxScore.Span);
|
||||||
|
|
||||||
|
vInt = Vector.Max(vInt, Vector<int>.One);
|
||||||
|
var v = Vector.ConvertToSingle(vInt);
|
||||||
|
|
||||||
|
var exploitation = W * (s / v) + w * m;
|
||||||
|
var exploration = CVector * Intrinsics.ReciprocalSqrt(v);
|
||||||
|
var evalScores = exploitation + exploration;
|
||||||
|
|
||||||
|
var idx = Intrinsics.HMaxIndex(evalScores, iterCount);
|
||||||
|
|
||||||
|
if (evalScores[idx] >= maxScore)
|
||||||
|
{
|
||||||
|
max = (i, idx);
|
||||||
|
maxScore = evalScores[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
length -= iterCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return max;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
private Node Select()
|
||||||
|
{
|
||||||
|
var node = rootNode;
|
||||||
|
var nodeVisits = rootScores.Visits;
|
||||||
|
|
||||||
|
float explorationConstant = config.ExplorationConstant, maxScoreWeightingConstant = config.MaxScoreWeightingConstant;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var expandable = !node.State.AvailableActions.IsEmpty;
|
||||||
|
var likelyTerminal = node.Children.Count == 0;
|
||||||
|
if (expandable || likelyTerminal)
|
||||||
|
return node;
|
||||||
|
|
||||||
|
// select the node with the highest score
|
||||||
|
var at = EvalBestChild(explorationConstant, maxScoreWeightingConstant, nodeVisits, ref node.ChildScores);
|
||||||
|
nodeVisits = node.ChildScores.GetVisits(at);
|
||||||
|
node = node.ChildAt(at)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private (Node ExpandedNode, float Score) ExpandAndRollout(Random random, Simulator simulator, Node initialNode)
|
||||||
|
{
|
||||||
|
ref var initialState = ref initialNode.State;
|
||||||
|
// expand once
|
||||||
|
if (initialState.IsComplete)
|
||||||
|
return (initialNode, initialState.CalculateScore(config) ?? 0);
|
||||||
|
|
||||||
|
var poppedAction = initialState.AvailableActions.PopRandom(random);
|
||||||
|
var expandedNode = initialNode.Add(Execute(simulator, initialState.State, poppedAction, true));
|
||||||
|
|
||||||
|
// playout to a terminal state
|
||||||
|
var currentState = expandedNode.State.State;
|
||||||
|
var currentCompletionState = expandedNode.State.SimulationCompletionState;
|
||||||
|
var currentActions = expandedNode.State.AvailableActions;
|
||||||
|
|
||||||
|
|
||||||
|
byte actionCount = 0;
|
||||||
|
Span<ActionType> actions = stackalloc ActionType[Math.Min(config.MaxStepCount - currentState.ActionCount, config.MaxRolloutStepCount)];
|
||||||
|
while (SimulationNode.GetCompletionState(currentCompletionState, currentActions) == CompletionState.Incomplete &&
|
||||||
|
actionCount < actions.Length)
|
||||||
|
{
|
||||||
|
var nextAction = currentActions.SelectRandom(random);
|
||||||
|
actions[actionCount++] = nextAction;
|
||||||
|
(_, currentState) = simulator.Execute(currentState, nextAction);
|
||||||
|
currentCompletionState = simulator.CompletionState;
|
||||||
|
if (currentCompletionState != CompletionState.Incomplete)
|
||||||
|
break;
|
||||||
|
currentActions = simulator.AvailableActionsHeuristic(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// store the result if a max score was reached
|
||||||
|
var score = SimulationNode.CalculateScoreForState(currentState, currentCompletionState, config) ?? 0;
|
||||||
|
if (currentCompletionState == CompletionState.ProgressComplete)
|
||||||
|
{
|
||||||
|
if (score >= config.ScoreStorageThreshold && score >= MaxScore)
|
||||||
|
{
|
||||||
|
var terminalNode = ExecuteActions(simulator, expandedNode, actions[..actionCount], true);
|
||||||
|
return (terminalNode, score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (expandedNode, score);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Backpropagate(Node startNode, float score)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (startNode == rootNode)
|
||||||
|
{
|
||||||
|
rootScores.Visit(score);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
startNode.ParentScores!.Value.Visit(startNode.ChildIdx, score);
|
||||||
|
|
||||||
|
startNode = startNode.Parent!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowAllNodes()
|
||||||
|
{
|
||||||
|
static void ShowNodes(StringBuilder b, Node node, Stack<Node> path)
|
||||||
|
{
|
||||||
|
path.Push(node);
|
||||||
|
b.AppendLine($"{new string(' ', path.Count)}{node.State.Action}");
|
||||||
|
{
|
||||||
|
for (var i = 0; i < node.Children.Count; ++i)
|
||||||
|
{
|
||||||
|
var n = node.ChildAt((i >> 3, i & 7))!;
|
||||||
|
ShowNodes(b, n, path);
|
||||||
|
}
|
||||||
|
path.Pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var b = new StringBuilder();
|
||||||
|
ShowNodes(b, rootNode, new());
|
||||||
|
Console.WriteLine(b.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool AllNodesComplete()
|
||||||
|
{
|
||||||
|
static bool NodesIncomplete(Node node, Stack<Node> path)
|
||||||
|
{
|
||||||
|
path.Push(node);
|
||||||
|
if (node.Children.Count == 0)
|
||||||
|
{
|
||||||
|
if (!node.State.AvailableActions.IsEmpty)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (var i = 0; i < node.Children.Count; ++i)
|
||||||
|
{
|
||||||
|
var n = node.ChildAt((i >> 3, i & 7))!;
|
||||||
|
if (NodesIncomplete(n, path))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
path.Pop();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !NodesIncomplete(rootNode, new());
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public void Search(int iterations, CancellationToken token)
|
||||||
|
{
|
||||||
|
Simulator simulator = new(rootNode.State.State, config.MaxStepCount);
|
||||||
|
var random = rootNode.State.State.Input.Random;
|
||||||
|
var n = 0;
|
||||||
|
for (var i = 0; i < iterations || MaxScore == 0; i++)
|
||||||
|
{
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var selectedNode = Select();
|
||||||
|
var (endNode, score) = ExpandAndRollout(random, simulator, selectedNode);
|
||||||
|
if (MaxScore == 0)
|
||||||
|
{
|
||||||
|
if (endNode == selectedNode)
|
||||||
|
{
|
||||||
|
if (n++ > 5000)
|
||||||
|
{
|
||||||
|
n = 0;
|
||||||
|
if (AllNodesComplete())
|
||||||
|
{
|
||||||
|
//Console.WriteLine("All nodes solved for. Can't find a valid solution.");
|
||||||
|
//ShowAllNodes();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
n = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Backpropagate(endNode, score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
public SolverSolution Solution()
|
||||||
|
{
|
||||||
|
var actions = new List<ActionType>();
|
||||||
|
var node = rootNode;
|
||||||
|
|
||||||
|
while (node.Children.Count != 0)
|
||||||
|
{
|
||||||
|
node = node.ChildAt(ChildMaxScore(ref node.ChildScores))!;
|
||||||
|
|
||||||
|
if (node.State.Action != null)
|
||||||
|
actions.Add(node.State.Action.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
//var at = node.ChildIdx;
|
||||||
|
//ref var sum = ref node.ParentScores!.Value.Data[at.arrayIdx].ScoreSum.Span[at.subIdx];
|
||||||
|
//ref var max = ref node.ParentScores!.Value.Data[at.arrayIdx].MaxScore.Span[at.subIdx];
|
||||||
|
//ref var visits = ref node.ParentScores!.Value.Data[at.arrayIdx].Visits.Span[at.subIdx];
|
||||||
|
//Console.WriteLine($"{sum} {max} {visits}");
|
||||||
|
|
||||||
|
return new(actions, node.State.State);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Craftimizer.Solver;
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Auto)]
|
||||||
|
public readonly record struct MCTSConfig
|
||||||
|
{
|
||||||
|
public int MaxThreadCount { get; init; }
|
||||||
|
|
||||||
|
public int MaxStepCount { get; init; }
|
||||||
|
public int MaxRolloutStepCount { get; init; }
|
||||||
|
public bool StrictActions { get; init; }
|
||||||
|
|
||||||
|
public float MaxScoreWeightingConstant { get; init; }
|
||||||
|
public float ExplorationConstant { get; init; }
|
||||||
|
public float ScoreStorageThreshold { get; init; }
|
||||||
|
|
||||||
|
public float ScoreProgress { get; init; }
|
||||||
|
public float ScoreQuality { get; init; }
|
||||||
|
public float ScoreDurability { get; init; }
|
||||||
|
public float ScoreCP { get; init; }
|
||||||
|
public float ScoreSteps { get; init; }
|
||||||
|
|
||||||
|
public MCTSConfig(SolverConfig config)
|
||||||
|
{
|
||||||
|
MaxStepCount = config.MaxStepCount;
|
||||||
|
MaxRolloutStepCount = config.MaxRolloutStepCount;
|
||||||
|
StrictActions = config.StrictActions;
|
||||||
|
|
||||||
|
MaxScoreWeightingConstant = config.MaxScoreWeightingConstant;
|
||||||
|
ExplorationConstant = config.ExplorationConstant;
|
||||||
|
ScoreStorageThreshold = config.ScoreStorageThreshold;
|
||||||
|
|
||||||
|
ScoreProgress = config.ScoreProgress;
|
||||||
|
ScoreQuality = config.ScoreQuality;
|
||||||
|
ScoreDurability = config.ScoreDurability;
|
||||||
|
ScoreCP = config.ScoreCP;
|
||||||
|
ScoreSteps = config.ScoreSteps;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ using System.Diagnostics.Contracts;
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Craftimizer.Solver.Crafty;
|
namespace Craftimizer.Solver;
|
||||||
|
|
||||||
// Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs
|
// Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs
|
||||||
public struct NodeScoresBuffer
|
public struct NodeScoresBuffer
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Craftimizer.Solver.Crafty;
|
namespace Craftimizer.Solver;
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Auto)]
|
[StructLayout(LayoutKind.Auto)]
|
||||||
public sealed class RootScores
|
public sealed class RootScores
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
using Craftimizer.Simulator;
|
using Craftimizer.Simulator;
|
||||||
using Craftimizer.Simulator.Actions;
|
using Craftimizer.Simulator.Actions;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Craftimizer.Solver.Crafty;
|
namespace Craftimizer.Solver;
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Auto)]
|
[StructLayout(LayoutKind.Auto)]
|
||||||
public struct SimulationNode
|
public struct SimulationNode
|
||||||
@@ -30,52 +32,48 @@ public struct SimulationNode
|
|||||||
CompletionState.NoMoreActions :
|
CompletionState.NoMoreActions :
|
||||||
simCompletionState;
|
simCompletionState;
|
||||||
|
|
||||||
public readonly float? CalculateScore(SolverConfig config) =>
|
public readonly float? CalculateScore(MCTSConfig config) =>
|
||||||
CalculateScoreForState(State, SimulationCompletionState, config);
|
CalculateScoreForState(State, SimulationCompletionState, config);
|
||||||
|
|
||||||
private static bool CanByregot(SimulationState state)
|
public static float? CalculateScoreForState(SimulationState state, CompletionState completionState, MCTSConfig config)
|
||||||
{
|
|
||||||
if (state.ActiveEffects.InnerQuiet == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return BaseComboAction.VerifyDurability2(state, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static float? CalculateScoreForState(SimulationState state, CompletionState completionState, SolverConfig config)
|
|
||||||
{
|
{
|
||||||
if (completionState != CompletionState.ProgressComplete)
|
if (completionState != CompletionState.ProgressComplete)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
if (state.Input.Recipe.MaxQuality == 0)
|
||||||
|
return 1f - ((float)(state.ActionCount + 1) / config.MaxStepCount);
|
||||||
|
|
||||||
|
[Pure]
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
static float Apply(float bonus, float value, float target) =>
|
static float Apply(float bonus, float value, float target) =>
|
||||||
bonus * Math.Min(1f, value / target);
|
bonus * (target > 0 ? Math.Clamp(value / target, 0, 1) : 1);
|
||||||
|
|
||||||
var progressScore = Apply(
|
var progressScore = Apply(
|
||||||
config.ScoreProgressBonus,
|
config.ScoreProgress,
|
||||||
state.Progress,
|
state.Progress,
|
||||||
state.Input.Recipe.MaxProgress
|
state.Input.Recipe.MaxProgress
|
||||||
);
|
);
|
||||||
|
|
||||||
var byregotBonus = CanByregot(state) ? (state.ActiveEffects.InnerQuiet * .2f + 1) * state.Input.BaseQualityGain : 0;
|
|
||||||
var qualityScore = Apply(
|
var qualityScore = Apply(
|
||||||
config.ScoreQualityBonus,
|
config.ScoreQuality,
|
||||||
state.Quality + byregotBonus,
|
state.Quality,
|
||||||
state.Input.Recipe.MaxQuality
|
state.Input.Recipe.MaxQuality
|
||||||
);
|
);
|
||||||
|
|
||||||
var durabilityScore = Apply(
|
var durabilityScore = Apply(
|
||||||
config.ScoreDurabilityBonus,
|
config.ScoreDurability,
|
||||||
state.Durability,
|
state.Durability,
|
||||||
state.Input.Recipe.MaxDurability
|
state.Input.Recipe.MaxDurability
|
||||||
);
|
);
|
||||||
|
|
||||||
var cpScore = Apply(
|
var cpScore = Apply(
|
||||||
config.ScoreCPBonus,
|
config.ScoreCP,
|
||||||
state.CP,
|
state.CP,
|
||||||
state.Input.Stats.CP
|
state.Input.Stats.CP
|
||||||
);
|
);
|
||||||
|
|
||||||
var fewerStepsScore =
|
var fewerStepsScore =
|
||||||
config.ScoreFewerStepsBonus * (1f - ((float)(state.ActionCount + 1) / config.MaxStepCount));
|
config.ScoreSteps * (1f - ((float)(state.ActionCount + 1) / config.MaxStepCount));
|
||||||
|
|
||||||
return progressScore + qualityScore + durabilityScore + cpScore + fewerStepsScore;
|
return progressScore + qualityScore + durabilityScore + cpScore + fewerStepsScore;
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user