Merge branch 'alt' into main

This commit is contained in:
Asriel Camora
2023-11-08 04:40:11 -08:00
108 changed files with 7215 additions and 3588 deletions
+1
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
# Auto detect text files and perform LF normalization
* text eol=crlf
*.png binary
+27
View File
@@ -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
View File
@@ -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}}
+1
View File
@@ -2,3 +2,4 @@
obj/ obj/
bin/ bin/
*.user *.user
BenchmarkDotNet.Artifacts/
+97
View File
@@ -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;
}
}
+15 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+78 -24
View File
@@ -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);
+10 -3
View File
@@ -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>
+2 -3
View File
@@ -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

-19
View File
@@ -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");
}
+139
View File
@@ -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
View File
@@ -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);
}
} }
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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>();
}
} }
+42 -11
View File
@@ -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
View File
@@ -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);
} }
} }
} }
+12
View File
@@ -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);
}
+121
View File
@@ -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;
}
}
+224
View File
@@ -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,
}
+28 -7
View File
@@ -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)
+2 -2
View File
@@ -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)
+81
View File
@@ -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();
}
}
+12
View File
@@ -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);
}
+158
View File
@@ -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);
}
}
+358
View File
@@ -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);
}
}
+82
View File
@@ -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);
}
}
-156
View File
@@ -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;
}
}
+41
View File
@@ -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);
}
}
-224
View File
@@ -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;
}
}
-157
View File
@@ -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
};
}
}
-96
View File
@@ -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();
}
}
}
-81
View File
@@ -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);
}
}
-553
View File
@@ -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();
}
}
+79
View File
@@ -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
+344
View File
@@ -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);
}
}
+874
View File
@@ -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
View File
@@ -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);
} }
} }
-64
View File
@@ -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();
}
}
-89
View File
@@ -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);
}
}
-439
View File
@@ -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);
}
}
-146
View File
@@ -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();
}
}
-96
View File
@@ -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();
}
}
+6 -6
View File
@@ -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"
+22
View File
@@ -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
{ {
+1 -1
View File
@@ -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;
} }
+2 -1
View File
@@ -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();
} }
+4 -1
View File
@@ -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);
} }
+1 -1
View File
@@ -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;
} }
+1 -1
View File
@@ -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;
} }
+1 -1
View File
@@ -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);
+3
View File
@@ -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";
} }
+1 -1
View File
@@ -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;
} }
+1 -1
View File
@@ -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;
} }
+1 -1
View File
@@ -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;
} }
+1 -1
View File
@@ -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;
} }
+2 -2
View File
@@ -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;
} }
} }
+1 -1
View File
@@ -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;
} }
+3
View File
@@ -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";
} }
+1 -1
View File
@@ -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))
+8 -6
View File
@@ -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();
} }
} }
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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))
+1 -1
View File
@@ -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)
{ {
+1 -1
View File
@@ -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))
+1 -1
View File
@@ -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))
+1 -1
View File
@@ -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;
} }
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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;
} }
+3
View File
@@ -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";
} }
+1 -1
View File
@@ -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
+3 -1
View File
@@ -6,5 +6,7 @@ public enum CompletionState : byte
ProgressComplete, ProgressComplete,
NoMoreDurability, NoMoreDurability,
Other InvalidAction,
MaxActionCountReached,
NoMoreActions
} }
+7 -1
View File
@@ -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>
+5
View File
@@ -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) =>
+2
View File
@@ -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
View File
@@ -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;
}
} }
+145
View File
@@ -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
{ {
+7 -1
View File
@@ -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>
-88
View File
@@ -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);
}
-20
View File
@@ -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;
}
-590
View File
@@ -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);
}
}
-6
View File
@@ -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
View File
@@ -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);
}
}
+40
View File
@@ -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