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.MA0011.severity = silent
dotnet_diagnostic.MA0076.severity = silent
dotnet_diagnostic.MA0046.severity = silent
dotnet_diagnostic.MA0002.severity = silent
csharp_style_prefer_switch_expression = true:suggestion
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
on:
push:
tags:
- '*'
push
env:
PLUGIN_REPO: WorkingRobot/MyDalamudPlugins
PROJECT_NAME: Craftimizer
IS_OFFICIAL: ${{true}}
jobs:
build:
runs-on: windows-latest
runs-on: ubuntu-latest
env:
DOTNET_CLI_TELEMETRY_OPTOUT: true
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v1.0.3
- name: Setup .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '7.0'
- name: Download Dalamud
run: |
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev\"
wget https://goatcorp.github.io/dalamud-distrib/stg/latest.zip
unzip latest.zip -d dalamud/
echo "DALAMUD_HOME=$PWD/dalamud" >> $GITHUB_ENV
- name: Restore
run: |
dotnet restore -r win
- name: Build
run: |
dotnet restore -r win ${{env.PROJECT_NAME}}.sln
dotnet build --configuration Release
env:
DOTNET_CLI_TELEMETRY_OUTPUT: true
dotnet build --configuration Release --no-restore
- name: Upload Artifact
uses: actions/upload-artifact@v2.2.1
- name: Test
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:
name: ${{env.PROJECT_NAME}}
path: ${{env.PROJECT_NAME}}/bin/x64/Release/${{env.PROJECT_NAME}}
if-no-files-found: error
- name: Upload Test Results
uses: actions/upload-artifact@v3
if: ${{ !cancelled() }}
with:
name: TestResults
path: TestResults
- name: Create Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
id: release
with:
files: ${{env.PROJECT_NAME}}/bin/x64/Release/${{env.PROJECT_NAME}}/*
- name: Trigger Plugin Repo Update
uses: peter-evans/repository-dispatch@v1
uses: peter-evans/repository-dispatch@v2
if: ${{ steps.release.conclusion == 'success' }}
with:
token: ${{secrets.PAT}}
repository: ${{env.PLUGIN_REPO}}
+2 -1
View File
@@ -1,4 +1,5 @@
.vs/
obj/
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>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.5" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<Compile Remove="BenchmarkDotNet.Artifacts\**" />
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -20,4 +28,9 @@
<ProjectReference Include="..\Simulator\Craftimizer.Simulator.csproj" />
<ProjectReference Include="..\Solver\Craftimizer.Solver.csproj" />
</ItemGroup>
<PropertyGroup Condition="'$(IS_BENCH)'=='1'">
<DefineConstants>$(DefineConstants);IS_DETERMINISTIC</DefineConstants>
</PropertyGroup>
</Project>
+61 -67
View File
@@ -1,17 +1,68 @@
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Craftimizer.Solver.Crafty;
using System.Diagnostics;
using Craftimizer.Solver;
namespace Craftimizer.Benchmark;
internal static class Program
{
private static void Main()
private static Task Main(string[] args)
{
//var summary = BenchmarkRunner.Run<Bench>();
//return;
RunBench(args);
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);
//return;
@@ -77,68 +128,11 @@ internal static class Program
Console.WriteLine($"{state.Quality} {state.CP} {state.Progress} {state.Durability}");
//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}");
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}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Test", "Test\Craftimizer.Test.csproj", "{C3AEA981-9DA8-405C-995B-86528493891B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
EndGlobalSection
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.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.Build.0 = Release|x64
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.ActiveCfg = Debug|Any CPU
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.Build.0 = Debug|Any CPU
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|Any CPU.Build.0 = Release|Any CPU
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.ActiveCfg = Release|Any CPU
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.Build.0 = Release|Any CPU
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.ActiveCfg = Debug|Any CPU
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.Build.0 = Debug|Any CPU
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|Any CPU.Build.0 = Release|Any CPU
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.ActiveCfg = Release|Any CPU
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.Build.0 = Release|Any CPU
{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
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.ActiveCfg = Debug|x64
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.Build.0 = Debug|x64
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.ActiveCfg = Release|x64
{057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.Build.0 = Release|x64
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.ActiveCfg = Debug|x64
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.Build.0 = Debug|x64
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.ActiveCfg = Release|x64
{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.Build.0 = Release|x64
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.ActiveCfg = Debug|x64
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.Build.0 = Debug|x64
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.ActiveCfg = Release|x64
{2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.Build.0 = Release|x64
{C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.ActiveCfg = Debug|x64
{C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.Build.0 = Debug|x64
{C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.ActiveCfg = Release|x64
{C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
+80 -26
View File
@@ -1,57 +1,111 @@
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Craftimizer.Solver.Crafty;
using Craftimizer.Solver;
using Dalamud.Configuration;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Threading;
namespace Craftimizer.Plugin;
[Serializable]
public class Macro
{
public static event Action<Macro>? OnMacroChanged;
public string Name { get; set; } = string.Empty;
public List<ActionType> Actions { get; set; } = new();
}
public static class AlgorithmUtils
{
public static void Invoke(this SolverConfig me, SimulationState state, Action<ActionType>? actionCallback = null, CancellationToken token = default)
[JsonProperty(PropertyName = "Actions")]
private List<ActionType> actions { get; set; } = new();
[JsonIgnore]
public IReadOnlyList<ActionType> Actions
{
try
get => actions;
set => ActionEnumerable = value;
}
[JsonIgnore]
public IEnumerable<ActionType> ActionEnumerable
{
set
{
Solver.Crafty.Solver.Search(me, state, actionCallback, token);
}
catch (AggregateException e)
{
e.Handle(ex => ex is OperationCanceledException);
}
catch (OperationCanceledException)
{
actions = new(value);
OnMacroChanged?.Invoke(this);
}
}
}
[Serializable]
public class MacroCopyConfiguration
{
public enum CopyType
{
OpenWindow, // useful for big macros
CopyToMacro, // (add option for down or right) (max macro count; open copy-paste window if too much)
CopyToClipboard,
}
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]
public class Configuration : IPluginConfiguration
{
public int Version { get; set; } = 1;
public bool OverrideUncraftability { get; set; } = true;
public bool HideUnlearnedActions { get; set; } = true;
public List<Macro> Macros { get; set; } = new();
public static event Action? OnMacroListChanged;
[JsonProperty(PropertyName = "Macros")]
private List<Macro> macros { get; set; } = new();
[JsonIgnore]
public IReadOnlyList<Macro> Macros => macros;
public bool ConditionRandomness { get; set; } = true;
public SolverConfig SimulatorSolverConfig { get; set; } = SolverConfig.SimulatorDefault;
public SolverConfig SynthHelperSolverConfig { get; set; } = SolverConfig.SynthHelperDefault;
public bool EnableSynthHelper { get; set; } = true;
public bool ShowOptimalMacroStat { get; set; } = true;
public int SynthHelperStepCount { get; set; } = 5;
public Simulator.Simulator CreateSimulator(SimulationState state) =>
ConditionRandomness ?
new Simulator.Simulator(state) :
new SimulatorNoRandom(state);
public MacroCopyConfiguration MacroCopy { get; set; } = new();
public void AddMacro(Macro macro)
{
macros.Add(macro);
Save();
OnMacroListChanged?.Invoke();
}
public void RemoveMacro(Macro macro)
{
if (macros.Remove(macro))
{
Save();
OnMacroListChanged?.Invoke();
}
}
public void Save() =>
Service.PluginInterface.SavePluginConfig(this);
+10 -3
View File
@@ -2,8 +2,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>Asriel Camora</Authors>
<Version>1.2.0.0</Version>
<Version>1.9.2.0</Version>
<PackageProjectUrl>https://github.com/WorkingRobot/craftimizer.git</PackageProjectUrl>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>
<PropertyGroup>
@@ -28,11 +29,17 @@
<ItemGroup>
<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>
<PackageReference Include="DalamudPackager" Version="2.1.11" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PackageReference Include="DalamudPackager" Version="2.1.12" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.92">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
+2 -3
View File
@@ -1,12 +1,11 @@
{
"Author": "Asriel Camora",
"Author": "Asriel",
"Name": "Craftimizer",
"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.",
"RepoUrl": "https://github.com/WorkingRobot/craftimizer",
"InternalName": "craftimizer",
"InternalName": "Craftimizer",
"ApplicableVersion": "any",
"DalamudApiLevel": 8,
"Tags": [
"crafting",
"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;
}
}
+494 -98
View File
@@ -1,138 +1,476 @@
using Craftimizer.Utils;
using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
namespace Craftimizer.Plugin;
internal static class ImGuiUtils
{
public static float ButtonHeight =>
ImGui.CalcTextSize("A").Y + (ImGui.GetStyle().FramePadding.Y * 2);
private static readonly Stack<(Vector2 Min, Vector2 Max)> GroupPanelLabelStack = new();
private static readonly Stack<(Vector2 Min, Vector2 Max, float TopPadding)> GroupPanelLabelStack = new();
// Adapted from https://github.com/ocornut/imgui/issues/1496#issuecomment-655048353
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();
var itemSpacing = ImGui.GetStyle().ItemSpacing;
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
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);
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);
// inner group
ImGui.BeginGroup();
ImGui.Dummy(new Vector2(fullWidth, 0));
ImGui.Dummy(new Vector2(itemSpacing.X, 0)); // shifts next group by is.x
ImGui.SameLine(0, 0);
ImGui.BeginGroup();
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax()));
ImGui.SameLine(0, 0);
ImGui.Dummy(new Vector2(0f, frameHeight * (addPadding ? 1 : .5f) + itemSpacing.Y));
// label group
ImGui.BeginGroup();
if (ImGui.CalcTextSize(name, true).X == 0)
{
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);
var textFrameHeight = ImGui.GetFrameHeight();
ImGui.AlignTextToFramePadding();
ImGui.Text(name);
GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), textFrameHeight / 2f)); // push rect to stack
ImGui.SameLine(0, 0);
ImGui.Dummy(new Vector2(0f, textFrameHeight + itemSpacing.Y)); // shifts content by fh + is.y
}
ImGui.BeginGroup();
// content group
ImGui.BeginGroup();
}
ImGui.PopStyleVar(2);
ImGui.PushItemWidth(MathF.Max(0, ImGui.CalcItemWidth() - frameHeight));
}
public static void BeginGroupPanel(string name, float width = -1, bool addPadding = true)
{
ImGui.BeginGroup();
var itemSpacing = ImGui.GetStyle().ItemSpacing;
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));
return width;
}
public static void EndGroupPanel()
{
ImGui.PopItemWidth();
var itemSpacing = ImGui.GetStyle().ItemSpacing;
ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero);
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
var frameHeight = ImGui.GetFrameHeight();
ImGui.EndGroup();
ImGui.EndGroup();
ImGui.SameLine(0, 0);
ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0));
ImGui.Dummy(new Vector2(0f, frameHeight * 0.5f - itemSpacing.Y));
ImGui.EndGroup();
var itemMin = ImGui.GetItemRectMin();
var itemMax = ImGui.GetItemRectMax();
var labelRect = GroupPanelLabelStack.Pop();
var halfFrame = new Vector2(frameHeight * 0.25f, frameHeight) * 0.5f;
(Vector2 Min, Vector2 Max) frameRect = (itemMin + halfFrame, itemMax - new Vector2(halfFrame.X, 0));
labelRect.Min.X -= itemSpacing.X;
labelRect.Max.X += itemSpacing.X;
for (var i = 0; i < 4; ++i)
{
var (minClip, maxClip) = i switch
{
0 => (new Vector2(float.NegativeInfinity), new Vector2(labelRect.Min.X, float.PositiveInfinity)),
1 => (new Vector2(labelRect.Max.X, float.NegativeInfinity), new Vector2(float.PositiveInfinity)),
2 => (new Vector2(labelRect.Min.X, float.NegativeInfinity), new Vector2(labelRect.Max.X, labelRect.Min.Y)),
3 => (new Vector2(labelRect.Min.X, labelRect.Max.Y), new Vector2(labelRect.Max.X, float.PositiveInfinity)),
_ => (Vector2.Zero, Vector2.Zero)
};
using var noPadding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero);
using var noSpacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
ImGui.PushClipRect(minClip, maxClip, true);
ImGui.GetWindowDrawList().AddRect(
frameRect.Min, frameRect.Max,
ImGui.GetColorU32(ImGuiCol.Border),
halfFrame.X);
ImGui.PopClipRect();
// content group
ImGui.EndGroup();
// label group
ImGui.EndGroup();
ImGui.SameLine(0, 0);
// shifts full size by is (for rect placement)
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();
var (labelMin, labelMax, labelPadding) = GroupPanelLabelStack.Pop();
var innerMin = ImGui.GetItemRectMin();
var innerMax = ImGui.GetItemRectMax();
// If there was actual text
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)
{
var (minClip, maxClip) = i switch
{
0 => (new Vector2(float.NegativeInfinity), new Vector2(labelMin.X, float.PositiveInfinity)),
1 => (new Vector2(labelMax.X, float.NegativeInfinity), new Vector2(float.PositiveInfinity)),
2 => (new Vector2(labelMin.X, float.NegativeInfinity), new Vector2(labelMax.X, labelMin.Y)),
3 => (new Vector2(labelMin.X, labelMax.Y), new Vector2(labelMax.X, float.PositiveInfinity)),
_ => (Vector2.Zero, Vector2.Zero)
};
ImGui.PushClipRect(minClip, maxClip, true);
ImGui.GetWindowDrawList().AddRect(
innerMin, innerMax,
ImGui.GetColorU32(ImGuiCol.Border),
itemSpacing.X);
ImGui.PopClipRect();
}
ImGui.Dummy(Vector2.Zero);
}
ImGui.PopStyleVar(2);
ImGui.Dummy(Vector2.Zero);
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)
{
ImGui.PushFont(UiBuilder.IconFont);
using var font = ImRaii.PushFont(UiBuilder.IconFont);
var ret = ImGui.Button(icon.ToIconString(), size);
ImGui.PopFont();
return ret;
}
@@ -158,4 +496,62 @@ internal static class ImGuiUtils
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<Item> ItemSheet = Service.DataManager.GetExcelSheet<Item>()!;
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<BaseParam> BaseParamSheet = Service.DataManager.GetExcelSheet<BaseParam>()!;
public static readonly ExcelSheet<ItemFood> ItemFoodSheet = Service.DataManager.GetExcelSheet<ItemFood>()!;
+114 -38
View File
@@ -1,77 +1,127 @@
using Craftimizer.Plugin.Utils;
using Craftimizer.Plugin.Windows;
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using Craftimizer.Utils;
using Craftimizer.Windows;
using Dalamud.Game.Command;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Windowing;
using Dalamud.IoC;
using Dalamud.Plugin;
using ImGuiScene;
using Lumina.Excel.GeneratedSheets;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ClassJob = Craftimizer.Simulator.ClassJob;
namespace Craftimizer.Plugin;
public sealed class Plugin : IDalamudPlugin
{
public string Name => "Craftimizer";
public string Version { get; }
public string Author { get; }
public string Configuration { get; }
public TextureWrap Icon { get; }
public string BuildConfiguration { get; }
public IDalamudTextureWrap Icon { get; }
public WindowSystem WindowSystem { get; }
public Settings SettingsWindow { get; }
public CraftingLog RecipeNoteWindow { get; }
public Craft SynthesisWindow { get; }
public Windows.Simulator? SimulatorWindow { get; set; }
public RecipeNote RecipeNoteWindow { get; }
public MacroList ListWindow { get; private set; }
public MacroEditor? EditorWindow { get; private set; }
public MacroClipboard? ClipboardWindow { get; private set; }
public Configuration Configuration { get; }
public Hooks Hooks { get; }
public RecipeNote RecipeNote { get; }
public IconManager IconManager { get; }
public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface)
{
Service.Plugin = this;
pluginInterface.Create<Service>();
Service.Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new Configuration();
Service.Initialize(this, pluginInterface);
WindowSystem = new("Craftimizer");
Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new();
Hooks = new();
IconManager = new();
var assembly = Assembly.GetExecutingAssembly();
Version = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion;
Author = assembly.GetCustomAttribute<AssemblyCompanyAttribute>()!.Company;
Configuration = assembly.GetCustomAttribute<AssemblyConfigurationAttribute>()!.Configuration;
byte[] iconData;
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);
BuildConfiguration = assembly.GetCustomAttribute<AssemblyConfigurationAttribute>()!.Configuration;
Icon = IconManager.GetAssemblyTexture("icon.png");
SettingsWindow = 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.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)
{
SimulatorWindow.IsOpen = false;
WindowSystem.RemoveWindow(SimulatorWindow);
}
SimulatorWindow = new(item, isExpert, input, classJob, macro);
var editorWindow = (EditorWindow?.IsOpen ?? false) ? EditorWindow : null;
var recipeData = editorWindow?.RecipeData ?? Service.Plugin.RecipeNoteWindow.RecipeData;
var characterStats = editorWindow?.CharacterStats ?? Service.Plugin.RecipeNoteWindow.CharacterStats;
var buffs = editorWindow?.Buffs ?? (RecipeNoteWindow.CharacterStats != null ? new(Service.ClientState.LocalPlayer?.StatusList) : null);
return (characterStats, recipeData, buffs);
}
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()
{
SettingsWindow.IsOpen = true;
SettingsWindow.BringToFront();
if (SettingsWindow.IsOpen ^= true)
SettingsWindow.BringToFront();
}
public void OpenSettingsTab(string selectedTabLabel)
@@ -80,11 +130,37 @@ public sealed class Plugin : IDalamudPlugin
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()
{
SimulatorWindow?.Dispose();
SynthesisWindow.Dispose();
RecipeNote.Dispose();
Service.CommandManager.RemoveHandler("/craftimizer");
Service.CommandManager.RemoveHandler("/craftmacros");
Service.CommandManager.RemoveHandler("/crafteditor");
SettingsWindow.Dispose();
RecipeNoteWindow.Dispose();
ListWindow.Dispose();
EditorWindow?.Dispose();
ClipboardWindow?.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.ClientState;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.Interface.Windowing;
using Dalamud.IoC;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
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.
[PluginService] public static DalamudPluginInterface PluginInterface { get; private set; }
[PluginService] public static CommandManager CommandManager { get; private set; }
[PluginService] public static ObjectTable Objects { get; private set; }
[PluginService] public static SigScanner SigScanner { get; private set; }
[PluginService] public static GameGui GameGui { get; private set; }
[PluginService] public static ClientState ClientState { get; private set; }
[PluginService] public static DataManager DataManager { get; private set; }
[PluginService] public static TargetManager TargetManager { get; private set; }
[PluginService] public static Condition Condition { get; private set; }
[PluginService] public static Framework Framework { get; private set; }
[PluginService] public static ICommandManager CommandManager { get; private set; }
[PluginService] public static IObjectTable Objects { get; private set; }
[PluginService] public static ISigScanner SigScanner { get; private set; }
[PluginService] public static IGameGui GameGui { get; private set; }
[PluginService] public static IClientState ClientState { get; private set; }
[PluginService] public static IDataManager DataManager { get; private set; }
[PluginService] public static ITextureProvider TextureProvider { get; private set; }
[PluginService] public static ITargetManager TargetManager { get; private set; }
[PluginService] public static ICondition Condition { get; private set; }
[PluginService] public static IFramework Framework { get; private set; }
[PluginService] public static IPluginLog PluginLog { get; private set; }
[PluginService] public static IGameInteropProvider GameInteropProvider { get; private set; }
public static Plugin Plugin { get; internal set; }
public static Configuration Configuration { get; internal set; }
public static Plugin Plugin { get; private set; }
public static Configuration Configuration => Plugin.Configuration;
public static WindowSystem WindowSystem => Plugin.WindowSystem;
public static IconManager IconManager => Plugin.IconManager;
#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.Actions;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.Internal;
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 System;
using System.Globalization;
@@ -10,8 +13,10 @@ using System.Linq;
using System.Numerics;
using System.Text;
using Action = Lumina.Excel.GeneratedSheets.Action;
using ActionType = Craftimizer.Simulator.Actions.ActionType;
using ClassJob = Craftimizer.Simulator.ClassJob;
using Condition = Craftimizer.Simulator.Condition;
using Status = Lumina.Excel.GeneratedSheets.Status;
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) =>
ActionRows[(int)me, (int)classJob];
@@ -81,15 +88,15 @@ internal static class ActionUtils
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);
if (craftAction != null)
return Icons.GetIconFromId(craftAction.Icon);
return Service.IconManager.GetIcon(craftAction.Icon);
if (action != null)
return Icons.GetIconFromId(action.Icon);
return Service.IconManager.GetIcon(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)
@@ -142,12 +149,33 @@ internal static class ClassJobUtils
_ => 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);
}
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) =>
classJob switch
{
@@ -282,11 +310,14 @@ internal static class EffectUtils
EffectType.FinalAppraisal => 2190,
EffectType.WasteNot2 => 257,
EffectType.MuscleMemory => 2191,
EffectType.Manipulation => 258,
EffectType.Manipulation => 1164,
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) =>
LuminaSheets.StatusSheet.GetRow(me.StatusId())!;
@@ -299,8 +330,8 @@ internal static class EffectUtils
return (ushort)iconId;
}
public static TextureWrap GetIcon(this EffectType me, int strength) =>
Icons.GetIconFromId(me.GetIconId(strength));
public static IDalamudTextureWrap GetIcon(this EffectType me, int strength) =>
Service.IconManager.GetIcon(me.GetIconId(strength));
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.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using System;
using System.Runtime.InteropServices;
using System.Text;
@@ -8,7 +9,7 @@ using System.Text;
namespace Craftimizer.Plugin.Utils;
// https://github.com/Caraxi/SimpleTweaksPlugin/blob/0973b93931cdf8a1b01153984d62f76d998747ff/Utility/ChatHelper.cs#L17
public static class Chat
public static unsafe class Chat
{
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";
}
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 readonly unsafe delegate* unmanaged<Utf8String*, int, IntPtr, void> _sanitiseString = null!;
private static readonly unsafe delegate* unmanaged<Utf8String*, int, IntPtr, void> SanitiseString = null!;
static Chat()
{
@@ -33,7 +34,7 @@ public static class Chat
{
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");
}
var uiModule = (IntPtr)Framework.Instance()->GetUiModule();
var uiModule = Framework.Instance()->GetUiModule();
using var payload = new ChatPayload(message);
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>
public static unsafe string SanitiseText(string text)
{
if (_sanitiseString == null)
if (SanitiseString == null)
{
throw new InvalidOperationException("Could not find signature for chat sanitisation");
}
var uText = Utf8String.FromString(text);
_sanitiseString(uText, 0x27F, IntPtr.Zero);
SanitiseString(uText, 0x27F, IntPtr.Zero);
var sanitised = uText->ToString();
uText->Dtor();
@@ -151,19 +152,19 @@ public static class Chat
internal ChatPayload(byte[] stringBytes)
{
this.textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
Marshal.Copy(stringBytes, 0, this.textPtr, stringBytes.Length);
Marshal.WriteByte(this.textPtr + stringBytes.Length, 0);
textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30);
Marshal.Copy(stringBytes, 0, textPtr, stringBytes.Length);
Marshal.WriteByte(textPtr + stringBytes.Length, 0);
this.textLen = (ulong)(stringBytes.Length + 1);
textLen = (ulong)(stringBytes.Length + 1);
this.unk1 = 64;
this.unk2 = 0;
unk1 = 64;
unk2 = 0;
}
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,
}
+29 -8
View File
@@ -8,7 +8,7 @@ using System;
using System.Linq;
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 GearsetMateria(ushort Type, ushort Grade);
@@ -20,6 +20,24 @@ internal static unsafe class Gearsets
public const int ParamCraftsmanship = 70;
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)
{
var items = new GearsetItem[(int)container->Size];
@@ -33,7 +51,7 @@ internal static unsafe class Gearsets
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];
for (var i = 0; i < 14; ++i)
{
@@ -117,18 +135,21 @@ internal static unsafe class Gearsets
public static bool IsSpecialistSoulCrystal(GearsetItem item)
{
if (item.itemId == 0)
return false;
var luminaItem = LuminaSheets.ItemSheet.GetRow(item.itemId)!;
// Soul Crystal ItemUICategory DoH Category
return luminaItem.ItemUICategory.Row != 62 && luminaItem.ClassJobUse.Value!.ClassJobCategory.Row == 33;
// Soul Crystal ItemUICategory DoH Category
return luminaItem.ItemUICategory.Row == 62 && luminaItem.ClassJobUse.Value!.ClassJobCategory.Row == 33;
}
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);
public static int CalculateCLvl(int characterLevel) =>
characterLevel <= 80
? LuminaSheets.ParamGrowSheet.GetRow((uint)characterLevel)!.CraftingLevel
: (int)LuminaSheets.RecipeLevelTableSheet.First(r => r.ClassJobLevel == characterLevel).RowId;
public static int CalculateCLvl(int level) =>
(level > 0 && level <= 90) ?
LevelToCLvlLUT[level - 1] :
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
private static int CalculateParamCap(Item item, int paramId)
+2 -2
View File
@@ -20,7 +20,7 @@ public sealed unsafe class Hooks : IDisposable
public Hooks()
{
UseActionHook = Hook<UseActionDelegate>.FromAddress((nint)ActionManager.MemberFunctionPointers.UseAction, UseActionDetour);
UseActionHook = Service.GameInteropProvider.HookFromAddress<UseActionDelegate>((nint)ActionManager.MemberFunctionPointers.UseAction, UseActionDetour);
UseActionHook.Enable();
}
@@ -28,7 +28,7 @@ public sealed unsafe class Hooks : IDisposable
{
var canCast = manager->GetActionStatus(actionType, actionId) == 0;
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));
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();
}
}
+518 -271
View File
@@ -1,37 +1,37 @@
using Craftimizer.Solver.Crafty;
using Craftimizer.Solver;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using FFXIVClientStructs.FFXIV.Client.UI;
using ImGuiNET;
using System;
using System.Linq;
using System.Numerics;
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 const int OptionWidth = 200;
private static Vector2 OptionButtonSize => new(OptionWidth, ImGuiUtils.ButtonHeight);
public const string TabGeneral = "General";
public const string TabSimulator = "Simulator";
public const string TabSynthHelper = "Synthesis Helper";
public const string TabAbout = "About";
private static Vector2 OptionButtonSize => new(OptionWidth, ImGui.GetFrameHeight());
private string? SelectedTab { get; set; }
public Settings() : base("Craftimizer Settings")
public Settings() : base("Craftimizer Settings", WindowFlags)
{
Service.WindowSystem.AddWindow(this);
SizeConstraints = new WindowSizeConstraints()
{
MinimumSize = new Vector2(400, 400),
MaximumSize = new Vector2(float.MaxValue, float.MaxValue)
MinimumSize = new(450, 400),
MaximumSize = new(float.PositiveInfinity)
};
Size = SizeConstraints.Value.MinimumSize;
SizeCondition = ImGuiCond.Appearing;
}
public void SelectTab(string label)
@@ -39,16 +39,16 @@ public class Settings : Window
SelectedTab = label;
}
private bool BeginTabItem(string label)
private ImRaii.IEndObject TabItem(string label)
{
var isSelected = string.Equals(SelectedTab, label, StringComparison.Ordinal);
if (isSelected)
{
SelectedTab = null;
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)
@@ -62,25 +62,44 @@ public class Settings : Window
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);
if (ImGui.InputInt(label, ref val))
var text = value.ToString();
if (ImGui.InputText(label, ref text, 8, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CharsDecimal))
{
setter(val);
isDirty = true;
if (T.TryParse(text, null, out var newValue))
{
newValue = T.Clamp(newValue, min, max);
if (value != newValue)
{
setter(newValue);
isDirty = true;
}
}
}
if (ImGui.IsItemHovered())
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);
if (ImGui.InputFloat(label, ref val))
using (var combo = ImRaii.Combo(label, getName(value)))
{
setter(val);
isDirty = true;
if (combo)
{
foreach (var type in Enum.GetValues<T>())
{
if (ImGui.Selectable(getName(type), value.Equals(type)))
{
setter(type);
isDirty = true;
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(getTooltip(type));
}
}
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(tooltip);
@@ -111,14 +130,33 @@ public class Settings : Window
_ => "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()
{
if (ImGui.BeginTabBar("settingsTabBar"))
{
DrawTabGeneral();
DrawTabSimulator();
if (Config.EnableSynthHelper)
DrawTabSynthHelper();
//if (Config.EnableSynthHelper)
// DrawTabSynthHelper();
DrawTabAbout();
ImGui.EndTabBar();
@@ -127,36 +165,213 @@ public class Settings : Window
private void DrawTabGeneral()
{
if (!BeginTabItem("General"))
using var tab = TabItem("General");
if (!tab)
return;
ImGuiHelpers.ScaledDummy(5);
var isDirty = false;
using (var g = ImRaii.Group())
{
using var d = ImRaii.Disabled();
DrawOption(
"Enable Synthesis Helper",
"Adds a helper next to your synthesis window to help solve for the best craft.\n" +
"Extremely useful for expert recipes, where the condition can greatly affect\n" +
"which actions you take.",
Config.EnableSynthHelper,
v => Config.EnableSynthHelper = v,
ref isDirty
);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Disabled temporarily for testing");
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,
"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
);
DrawOption(
"Enable Synthesis Helper",
"Adds a helper next to your synthesis window to help solve for the best craft.\n" +
"Extremely useful for expert recipes, where the condition can greatly affect\n" +
"which actions you take.",
Config.EnableSynthHelper,
v => Config.EnableSynthHelper = 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)
Config.Save();
ImGui.EndTabItem();
}
private static void DrawSolverConfig(ref SolverConfig configRef, SolverConfig defaultConfig, out bool isDirty)
@@ -165,212 +380,240 @@ public class Settings : Window
var config = configRef;
ImGuiUtils.BeginGroupPanel("General");
if (ImGui.Button("Reset to Default", OptionButtonSize))
using (var panel = ImGuiUtils.GroupPanel("General", -1, out _))
{
config = defaultConfig;
isDirty = true;
}
ImGui.SetNextItemWidth(OptionWidth);
if (ImGui.BeginCombo("Algorithm", GetAlgorithmName(config.Algorithm)))
{
foreach (var alg in Enum.GetValues<SolverAlgorithm>())
if (ImGui.Button("Reset to Default", OptionButtonSize))
{
if (ImGui.Selectable(GetAlgorithmName(alg), config.Algorithm == alg))
{
config = config with { Algorithm = alg };
isDirty = true;
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(GetAlgorithmTooltip(alg));
config = defaultConfig;
isDirty = true;
}
ImGui.EndCombo();
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip(
DrawOption(
"Algorithm",
"The algorithm to use when solving for a macro. Different\n" +
"algorithms provide different pros and cons for using them.\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(
"Iterations",
"The total number of iterations to run per crafting step.\n" +
"Higher values require more computational power. Higher values\n" +
"also may decrease variance, so other values should be tweaked\n" +
"as necessary to get a more favorable outcome.",
config.Iterations,
v => config = config with { Iterations = v },
ref isDirty
);
DrawOption(
"Iterations",
"The total number of iterations to run per crafting step.\n" +
"Higher values require more computational power. Higher values\n" +
"also may decrease variance, so other values should be tweaked\n" +
"as necessary to get a more favorable outcome.",
config.Iterations,
1000,
500000,
v => config = config with { Iterations = v },
ref isDirty
);
DrawOption(
"Max Step Count",
"The maximum number of crafting steps; this is generally the only\n" +
"setting you should change, and it should be set to around 5 steps\n" +
"more than what you'd expect. If this value is too low, the solver\n" +
"won't learn much per iteration; too high and it will waste time\n" +
"on useless extra steps.",
config.MaxStepCount,
v => config = config with { MaxStepCount = v },
ref isDirty
);
DrawOption(
"Max Step Count",
"The maximum number of crafting steps; this is generally the only\n" +
"setting you should change, and it should be set to around 5 steps\n" +
"more than what you'd expect. If this value is too low, the solver\n" +
"won't learn much per iteration; too high and it will waste time\n" +
"on useless extra steps.",
config.MaxStepCount,
1,
100,
v => config = config with { MaxStepCount = v },
ref isDirty
);
DrawOption(
"Exploration Constant",
"A constant that decides how often the solver will explore new,\n" +
"possibly good paths. If this value is too high,\n" +
"moves will mostly be decided at random.",
config.ExplorationConstant,
v => config = config with { ExplorationConstant = v },
ref isDirty
);
DrawOption(
"Exploration Constant",
"A constant that decides how often the solver will explore new,\n" +
"possibly good paths. If this value is too high,\n" +
"moves will mostly be decided at random.",
config.ExplorationConstant,
0,
10,
v => config = config with { ExplorationConstant = v },
ref isDirty
);
DrawOption(
"Score Weighting Constant",
"A constant ranging from 0 to 1 that configures how the solver\n" +
"scores and picks paths to travel to next. A value of 0 means\n" +
"actions will be chosen based on their average outcome, whereas\n" +
"1 uses their best outcome achieved so far.",
config.MaxScoreWeightingConstant,
v => config = config with { MaxScoreWeightingConstant = v },
ref isDirty
);
DrawOption(
"Score Weighting Constant",
"A constant ranging from 0 to 1 that configures how the solver\n" +
"scores and picks paths to travel to next. A value of 0 means\n" +
"actions will be chosen based on their average outcome, whereas\n" +
"1 uses their best outcome achieved so far.",
config.MaxScoreWeightingConstant,
0,
1,
v => config = config with { MaxScoreWeightingConstant = v },
ref isDirty
);
ImGui.BeginDisabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated));
DrawOption(
"Fork Count",
"Split the number of iterations across different solvers. In general,\n" +
"you should increase this value to at least the number of cores in\n" +
$"your system (FYI, you have {Environment.ProcessorCount} cores) to attain the most speedup.\n" +
"The higher the number, the more chance you have of finding a\n" +
"better local maximum; this concept similar but not equivalent\n" +
"to the exploration constant.\n" +
"(Only used in the Forked and Furcated algorithms)",
config.ForkCount,
v => config = config with { ForkCount = v },
ref isDirty
);
ImGui.EndDisabled();
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
);
ImGui.BeginDisabled(config.Algorithm is not SolverAlgorithm.StepwiseFurcated);
DrawOption(
"Furcated Action Count",
"On every craft step, pick this many top solutions and use them as\n" +
"the input for the next craft step. For best results, use Fork Count / 2\n" +
"and add about 1 or 2 more if needed.\n" +
"(Only used in the Stepwise Furcated algorithm)",
config.FurcatedActionCount,
v => config = config with { FurcatedActionCount = v },
ref isDirty
);
ImGui.EndDisabled();
using (var d = ImRaii.Disabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated)))
DrawOption(
"Fork Count",
"Split the number of iterations across different solvers. In general,\n" +
"you should increase this value to at least the number of cores in\n" +
$"your system (FYI, you have {Environment.ProcessorCount} cores) to attain the most speedup.\n" +
"The higher the number, the more chance you have of finding a\n" +
"better local maximum; this concept similar but not equivalent\n" +
"to the exploration constant.\n" +
"(Only used in the Forked and Furcated algorithms)",
config.ForkCount,
1,
500,
v => config = config with { ForkCount = v },
ref isDirty
);
ImGuiUtils.EndGroupPanel();
ImGuiUtils.BeginGroupPanel("Advanced");
DrawOption(
"Score Storage Threshold",
"If a craft achieves this certain arbitrary score, the solver will\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.",
config.ScoreStorageThreshold,
v => config = config with { ScoreStorageThreshold = v },
ref isDirty
);
DrawOption(
"Max Rollout Step Count",
"The maximum number of crafting steps every iteration can consider.\n" +
"Decreasing this value can have unintended side effects. Only change\n" +
"this value if you absolutely know what you're doing.",
config.MaxRolloutStepCount,
v => config = config with { MaxRolloutStepCount = v },
ref isDirty
);
DrawOption(
"Strict Actions",
"When finding the next possible actions to execute, use a heuristic\n" +
"to restrict which actions to attempt taking. This results in a much\n" +
"better macro at the cost of not finding an extremely creative one.",
config.StrictActions,
v => config = config with { StrictActions = v },
ref isDirty
);
ImGuiUtils.EndGroupPanel();
ImGuiUtils.BeginGroupPanel("Score Weights (Advanced)");
ImGui.TextWrapped("All values should add up to 1. Otherwise, the Score Storage Threshold should be changed.");
ImGuiHelpers.ScaledDummy(10);
DrawOption(
"Progress",
"Amount of weight to give to the craft's progress.",
config.ScoreProgressBonus,
v => config = config with { ScoreProgressBonus = v },
ref isDirty
);
DrawOption(
"Quality",
"Amount of weight to give to the craft's quality.",
config.ScoreQualityBonus,
v => config = config with { ScoreQualityBonus = v },
ref isDirty
);
DrawOption(
"Durability",
"Amount of weight to give to the craft's remaining durability.",
config.ScoreDurabilityBonus,
v => config = config with { ScoreDurabilityBonus = v },
ref isDirty
);
DrawOption(
"CP",
"Amount of weight to give to the craft's remaining CP.",
config.ScoreCPBonus,
v => config = config with { ScoreCPBonus = v },
ref isDirty
);
DrawOption(
"Steps",
"Amount of weight to give to the craft's number of steps. The lower\n" +
"the step count, the higher the score.",
config.ScoreFewerStepsBonus,
v => config = config with { ScoreFewerStepsBonus = v },
ref isDirty
);
if (ImGui.Button("Normalize Weights", OptionButtonSize))
{
var total = config.ScoreProgressBonus +
config.ScoreQualityBonus +
config.ScoreDurabilityBonus +
config.ScoreCPBonus +
config.ScoreFewerStepsBonus;
config = config with
{
ScoreProgressBonus = config.ScoreProgressBonus / total,
ScoreQualityBonus = config.ScoreQualityBonus / total,
ScoreDurabilityBonus = config.ScoreDurabilityBonus / total,
ScoreCPBonus = config.ScoreCPBonus / total,
ScoreFewerStepsBonus = config.ScoreFewerStepsBonus / total
};
isDirty = true;
using (var d = ImRaii.Disabled(config.Algorithm is not SolverAlgorithm.StepwiseFurcated))
DrawOption(
"Furcated Action Count",
"On every craft step, pick this many top solutions and use them as\n" +
"the input for the next craft step. For best results, use Fork Count / 2\n" +
"and add about 1 or 2 more if needed.\n" +
"(Only used in the Stepwise Furcated algorithm)",
config.FurcatedActionCount,
1,
500,
v => config = config with { FurcatedActionCount = v },
ref isDirty
);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Normalize all weights to sum up to 1");
ImGuiUtils.EndGroupPanel();
using (var panel = ImGuiUtils.GroupPanel("Advanced", -1, out _))
{
DrawOption(
"Score Storage Threshold",
"If a craft achieves this certain arbitrary score, the solver will\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.",
config.ScoreStorageThreshold,
0,
1,
v => config = config with { ScoreStorageThreshold = v },
ref isDirty
);
DrawOption(
"Max Rollout Step Count",
"The maximum number of crafting steps every iteration can consider.\n" +
"Decreasing this value can have unintended side effects. Only change\n" +
"this value if you absolutely know what you're doing.",
config.MaxRolloutStepCount,
1,
50,
v => config = config with { MaxRolloutStepCount = v },
ref isDirty
);
DrawOption(
"Strict Actions",
"When finding the next possible actions to execute, use a heuristic\n" +
"to restrict which actions to attempt taking. This results in a much\n" +
"better macro at the cost of not finding an extremely creative one.",
config.StrictActions,
v => config = config with { StrictActions = v },
ref isDirty
);
}
using (var panel = ImGuiUtils.GroupPanel("Score Weights (Advanced)", -1, out _))
{
ImGui.TextWrapped("All values should add up to 1. Otherwise, the Score Storage Threshold should be changed.");
ImGuiHelpers.ScaledDummy(10);
DrawOption(
"Progress",
"Amount of weight to give to the craft's progress.",
config.ScoreProgress,
0,
1,
v => config = config with { ScoreProgress = v },
ref isDirty
);
DrawOption(
"Quality",
"Amount of weight to give to the craft's quality.",
config.ScoreQuality,
0,
1,
v => config = config with { ScoreQuality = v },
ref isDirty
);
DrawOption(
"Durability",
"Amount of weight to give to the craft's remaining durability.",
config.ScoreDurability,
0,
1,
v => config = config with { ScoreDurability = v },
ref isDirty
);
DrawOption(
"CP",
"Amount of weight to give to the craft's remaining CP.",
config.ScoreCP,
0,
1,
v => config = config with { ScoreCP = v },
ref isDirty
);
DrawOption(
"Steps",
"Amount of weight to give to the craft's number of steps. The lower\n" +
"the step count, the higher the score.",
config.ScoreSteps,
0,
1,
v => config = config with { ScoreSteps = v },
ref isDirty
);
if (ImGui.Button("Normalize Weights", OptionButtonSize))
{
var total = config.ScoreProgress +
config.ScoreQuality +
config.ScoreDurability +
config.ScoreCP +
config.ScoreSteps;
config = config with
{
ScoreProgress = config.ScoreProgress / total,
ScoreQuality = config.ScoreQuality / total,
ScoreDurability = config.ScoreDurability / total,
ScoreCP = config.ScoreCP / total,
ScoreSteps = config.ScoreSteps / total
};
isDirty = true;
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Normalize all weights to sum up to 1");
}
if (isDirty)
configRef = config;
@@ -378,30 +621,28 @@ public class Settings : Window
private void DrawTabSimulator()
{
if (!BeginTabItem("Simulator"))
using var tab = TabItem("Simulator");
if (!tab)
return;
ImGuiHelpers.ScaledDummy(5);
var isDirty = false;
DrawOption(
"Show Only Learned Actions",
"Don't show crafting actions that haven't been\n" +
"learned yet with your current job on the simulator sidebar",
Config.HideUnlearnedActions,
v => Config.HideUnlearnedActions = v,
ref isDirty
);
DrawOption(
"Condition Randomness",
"Allows the simulator condition to fluctuate randomly like a real craft.\n" +
"Turns off when generating a macro.",
Config.ConditionRandomness,
v => Config.ConditionRandomness = v,
ref isDirty
);
using (var g = ImRaii.Group())
{
using var d = ImRaii.Disabled();
DrawOption(
"Condition Randomness",
"Allows the simulator condition to fluctuate randomly like a real craft.\n" +
"Turns off when generating a macro.",
Config.ConditionRandomness,
v => Config.ConditionRandomness = v,
ref isDirty
);
}
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Disabled temporarily for testing");
ImGuiHelpers.ScaledDummy(5);
ImGui.Separator();
@@ -417,13 +658,12 @@ public class Settings : Window
if (isDirty)
Config.Save();
ImGui.EndTabItem();
}
private void DrawTabSynthHelper()
{
if (!BeginTabItem("Synthesis Helper"))
using var tab = TabItem("Synthesis Helper");
if (!tab)
return;
ImGuiHelpers.ScaledDummy(5);
@@ -434,6 +674,8 @@ public class Settings : Window
"Step Count",
"The number of future actions to solve for during an in-game craft.",
Config.SynthHelperStepCount,
1,
100,
v => Config.SynthHelperStepCount = v,
ref isDirty
);
@@ -452,13 +694,12 @@ public class Settings : Window
if (isDirty)
Config.Save();
ImGui.EndTabItem();
}
private void DrawTabAbout()
{
if (!BeginTabItem("About"))
using var tab = TabItem("About");
if (!tab)
return;
ImGuiHelpers.ScaledDummy(5);
@@ -466,28 +707,34 @@ public class Settings : Window
var plugin = Service.Plugin;
var icon = plugin.Icon;
ImGui.BeginTable("settingsAboutTable", 2);
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, icon.Width);
using (var table = ImRaii.Table("settingsAboutTable", 2))
{
if (table)
{
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, icon.Width);
ImGui.TableNextColumn();
ImGui.Image(icon.ImGuiHandle, new(icon.Width, icon.Height));
ImGui.TableNextColumn();
ImGui.Image(icon.ImGuiHandle, new(icon.Width, icon.Height));
ImGui.TableNextColumn();
ImGui.Text($"{plugin.Name} v{plugin.Version} {plugin.Configuration}");
ImGui.Text($"By {plugin.Author} (");
ImGui.SameLine(0, 0);
ImGuiUtils.Hyperlink("WorkingRobot", "https://github.com/WorkingRobot");
ImGui.SameLine(0, 0);
ImGui.Text(")");
ImGui.EndTable();
ImGui.TableNextColumn();
ImGui.Text($"Craftimizer v{plugin.Version} {plugin.BuildConfiguration}");
ImGui.Text($"By {plugin.Author} (");
ImGui.SameLine(0, 0);
ImGuiUtils.Hyperlink("WorkingRobot", "https://github.com/WorkingRobot");
ImGui.SameLine(0, 0);
ImGui.Text(")");
}
}
ImGui.Text("Credit to altosock's ");
ImGui.SameLine(0, 0);
ImGuiUtils.Hyperlink("Craftingway", "https://craftingway.app");
ImGui.SameLine(0, 0);
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": {
"DalamudPackager": {
"type": "Direct",
"requested": "[2.1.11, )",
"resolved": "2.1.11",
"contentHash": "9qlAWoRRTiL/geAvuwR/g6Bcbrd/bJJgVnB/RurBiyKs6srsP0bvpoo8IK+Eg8EA6jWeM6/YJWs66w4FIAzqPw=="
"requested": "[2.1.12, )",
"resolved": "2.1.12",
"contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg=="
},
"Meziantou.Analyzer": {
"type": "Direct",
"requested": "[2.0.62, )",
"resolved": "2.0.62",
"contentHash": "uG2CiDIm97q8KrUt8B34WdElpEDDLOe4YzrLWpwlQmesXrSX2WuJZ+HwIGWrJgDBBMi2a3tVjeF8oKjV+AhUdA=="
"requested": "[2.0.92, )",
"resolved": "2.0.92",
"contentHash": "gVyPM2gDPfxvZ2rGUKzTNZsNhdgsetfYd+OKowKwMqZ9K00j1amUU+SnlRI26629EKK4cbJWJwHs00UPXRr0BA=="
},
"craftimizer.simulator": {
"type": "Project"
+22
View File
@@ -1,3 +1,6 @@
using Craftimizer.Simulator.Actions;
using System.Collections.ObjectModel;
namespace Craftimizer.Simulator;
public enum ActionCategory
@@ -13,6 +16,25 @@ public enum ActionCategory
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) =>
category switch
{
+1 -1
View File
@@ -9,5 +9,5 @@ internal sealed class AdvancedTouch : BaseAction
public override bool IncreasesQuality => true;
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;
}
+3 -2
View File
@@ -22,7 +22,7 @@ public abstract class BaseAction
// Instanced properties
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 bool CanUse(Simulator s) =>
@@ -48,7 +48,8 @@ public abstract class BaseAction
s.ActionStates.MutateState(this);
s.ActionCount++;
s.ActiveEffects.DecrementDuration();
if (IncreasesStepCount)
s.ActiveEffects.DecrementDuration();
}
public virtual void UseSuccess(Simulator s)
+4 -1
View File
@@ -14,10 +14,13 @@ internal abstract class BaseBuffAction : BaseAction
public override void UseSuccess(Simulator s) =>
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));
builder.AppendLine($"{Duration} Steps");
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;
// 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 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 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);
+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 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;
// 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 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 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;
}
+1 -1
View File
@@ -9,6 +9,6 @@ internal sealed class FocusedTouch : BaseAction
public override bool IncreasesQuality => true;
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;
}
+2 -2
View File
@@ -10,10 +10,10 @@ internal sealed class Groundwork : BaseAction
public override int DurabilityCost => 20;
public override int CPCost(Simulator s) => 18;
public override float Efficiency(Simulator s)
public override int Efficiency(Simulator s)
{
// 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;
}
}
+1 -1
View File
@@ -9,6 +9,6 @@ internal sealed class HastyTouch : BaseAction
public override bool IncreasesQuality => true;
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;
}
+3
View File
@@ -14,4 +14,7 @@ internal sealed class HeartAndSoul : BaseBuffAction
public override int CPCost(Simulator s) => 0;
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 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) =>
(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)
{
if (s.HasEffect(EffectType.Manipulation))
s.RestoreDurability(5);
s.ReduceCP(CPCost(s));
s.ReduceDurability(DurabilityCost);
UseSuccess(s);
s.ReduceCP(CPCost(s));
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 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);
+1 -1
View File
@@ -9,7 +9,7 @@ internal sealed class PreciseTouch : BaseAction
public override bool IncreasesQuality => true;
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) =>
(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 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)
{
+1 -1
View File
@@ -10,7 +10,7 @@ internal sealed class PrudentSynthesis : BaseAction
public override int DurabilityCost => base.DurabilityCost / 2;
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) =>
!(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 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) =>
!(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;
// 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;
}
+1 -1
View File
@@ -9,7 +9,7 @@ internal sealed class Reflect : BaseAction
public override bool IncreasesQuality => true;
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);
+1 -1
View File
@@ -9,5 +9,5 @@ internal sealed class StandardTouch : BaseAction
public override bool IncreasesQuality => true;
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) =>
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 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) =>
s.GetEffectStrength(EffectType.InnerQuiet) == 10
+3 -1
View File
@@ -6,5 +6,7 @@ public enum CompletionState : byte
ProgressComplete,
NoMoreDurability,
Other
InvalidAction,
MaxActionCountReached,
NoMoreActions
}
+7 -1
View File
@@ -4,13 +4,19 @@
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.92">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<PropertyGroup Condition="'$(IS_BENCH)'=='1'">
<DefineConstants>$(DefineConstants);IS_DETERMINISTIC</DefineConstants>
</PropertyGroup>
</Project>
+5
View File
@@ -82,6 +82,11 @@ public record struct Effects
_ => 0
};
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsIndefinite(EffectType effect) =>
effect is EffectType.InnerQuiet or EffectType.HeartAndSoul;
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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
};
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;
+46 -30
View File
@@ -21,8 +21,17 @@ public class Simulator
public bool IsFirstStep => State.StepCount == 0;
public CompletionState CompletionState => CalculateCompletionState(State);
public virtual bool IsComplete => CompletionState != CompletionState.Incomplete;
public virtual CompletionState CompletionState {
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);
@@ -54,6 +63,8 @@ public class Simulator
return ActionResponse.ActionNotUnlocked;
if (action == ActionType.Manipulation && !Input.Stats.CanUseManipulation)
return ActionResponse.ActionNotUnlocked;
if (action is ActionType.CarefulObservation or ActionType.HeartAndSoul && !Input.Stats.IsSpecialist)
return ActionResponse.ActionNotUnlocked;
if (baseAction.CPCost(this) > CP)
return ActionResponse.NotEnoughCP;
return ActionResponse.CannotUseAction;
@@ -64,6 +75,20 @@ public class Simulator
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]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int GetEffectStrength(EffectType effect) =>
@@ -188,51 +213,51 @@ public class Simulator
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))
{
buffModifier += 1.00f;
buffModifier += 100;
if (!dryRun)
RemoveEffect(EffectType.MuscleMemory);
}
if (HasEffect(EffectType.Veneration))
buffModifier += 0.50f;
buffModifier += 50;
var conditionModifier = Condition switch
{
Condition.Malleable => 1.50f,
_ => 1.00f
Condition.Malleable => 150,
_ => 100
};
var progressGain = (int)(Input.BaseProgressGain * efficiency * conditionModifier * buffModifier);
var progressGain = (int)((long)Input.BaseProgressGain * efficiency * conditionModifier * buffModifier / 1e6);
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))
{
buffModifier += 1.00f;
buffModifier += 100;
if (!dryRun)
RemoveEffect(EffectType.GreatStrides);
}
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
{
Condition.Poor => 0.50f,
Condition.Good => Input.Stats.HasSplendorousBuff ? 1.75f : 1.50f,
Condition.Excellent => 4.00f,
_ => 1.00f,
Condition.Poor => 50,
Condition.Good => Input.Stats.HasSplendorousBuff ? 175 : 150,
Condition.Excellent => 400,
_ => 100,
};
var qualityGain = (int)(Input.BaseQualityGain * efficiency * conditionModifier * buffModifier);
var qualityGain = (int)((long)Input.BaseQualityGain * efficiency * conditionModifier * iqModifier * buffModifier / 1e8);
return qualityGain;
}
@@ -272,19 +297,10 @@ public class Simulator
ReduceCPRaw(CalculateCPCost(amount));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void IncreaseProgress(float efficiency) =>
public void IncreaseProgress(int efficiency) =>
IncreaseProgressRaw(CalculateProgressGain(efficiency, false));
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void IncreaseQuality(float efficiency) =>
public void IncreaseQuality(int efficiency) =>
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.Runtime.CompilerServices;
namespace Craftimizer.Solver.Crafty;
namespace Craftimizer.Solver;
// Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs
public struct ArenaBuffer<T> where T : struct
@@ -1,6 +1,6 @@
using System.Runtime.CompilerServices;
namespace Craftimizer.Solver.Crafty;
namespace Craftimizer.Solver;
public sealed class ArenaNode<T> where T : struct
{
+7 -1
View File
@@ -5,10 +5,12 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<Platforms>x64</Platforms>
<Configurations>Debug;Release</Configurations>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Meziantou.Analyzer" Version="2.0.62">
<PackageReference Include="Meziantou.Analyzer" Version="2.0.92">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -18,4 +20,8 @@
<ProjectReference Include="..\Simulator\Craftimizer.Simulator.csproj" />
</ItemGroup>
<PropertyGroup Condition="'$(IS_BENCH)'=='1'">
<DefineConstants>$(DefineConstants);IS_DETERMINISTIC</DefineConstants>
</PropertyGroup>
</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.X86;
namespace Craftimizer.Solver.Crafty;
namespace Craftimizer.Solver;
internal static class Intrinsics
{
[Pure]
@@ -50,7 +50,7 @@ internal static class Intrinsics
var vcmp = Avx.CompareEqual(vfilt, vmax);
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;
}
+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.Runtime.CompilerServices;
namespace Craftimizer.Solver.Crafty;
namespace Craftimizer.Solver;
// Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs
public struct NodeScoresBuffer
@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
namespace Craftimizer.Solver.Crafty;
namespace Craftimizer.Solver;
[StructLayout(LayoutKind.Auto)]
public sealed class RootScores
@@ -1,8 +1,10 @@
using Craftimizer.Simulator;
using Craftimizer.Simulator.Actions;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Craftimizer.Solver.Crafty;
namespace Craftimizer.Solver;
[StructLayout(LayoutKind.Auto)]
public struct SimulationNode
@@ -30,52 +32,48 @@ public struct SimulationNode
CompletionState.NoMoreActions :
simCompletionState;
public readonly float? CalculateScore(SolverConfig config) =>
public readonly float? CalculateScore(MCTSConfig config) =>
CalculateScoreForState(State, SimulationCompletionState, config);
private static bool CanByregot(SimulationState state)
{
if (state.ActiveEffects.InnerQuiet == 0)
return false;
return BaseComboAction.VerifyDurability2(state, 10);
}
public static float? CalculateScoreForState(SimulationState state, CompletionState completionState, SolverConfig config)
public static float? CalculateScoreForState(SimulationState state, CompletionState completionState, MCTSConfig config)
{
if (completionState != CompletionState.ProgressComplete)
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) =>
bonus * Math.Min(1f, value / target);
bonus * (target > 0 ? Math.Clamp(value / target, 0, 1) : 1);
var progressScore = Apply(
config.ScoreProgressBonus,
config.ScoreProgress,
state.Progress,
state.Input.Recipe.MaxProgress
);
var byregotBonus = CanByregot(state) ? (state.ActiveEffects.InnerQuiet * .2f + 1) * state.Input.BaseQualityGain : 0;
var qualityScore = Apply(
config.ScoreQualityBonus,
state.Quality + byregotBonus,
config.ScoreQuality,
state.Quality,
state.Input.Recipe.MaxQuality
);
var durabilityScore = Apply(
config.ScoreDurabilityBonus,
config.ScoreDurability,
state.Durability,
state.Input.Recipe.MaxDurability
);
var cpScore = Apply(
config.ScoreCPBonus,
config.ScoreCP,
state.CP,
state.Input.Stats.CP
);
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;
}

Some files were not shown because too many files have changed in this diff Show More