├── .editorconfig ├── .gitattributes ├── .github ├── create_unofficial.py └── workflows │ └── build.yml ├── .gitignore ├── Benchmark ├── Bench.cs ├── Craftimizer.Benchmark.csproj └── Program.cs ├── Craftimizer.sln ├── Craftimizer ├── Configuration.cs ├── Craftimizer.csproj ├── Craftimizer.json ├── Graphics │ ├── collectible_badge.png │ ├── expert.png │ ├── expert_badge.png │ ├── horse_icon.png │ ├── horse_icon.svg │ ├── icon.png │ ├── icon.svg │ ├── no_manip.png │ ├── specialist.png │ └── splendorous.png ├── ImGuiExtras.cs ├── ImGuiUtils.cs ├── ImRaii2.cs ├── LuminaSheets.cs ├── Plugin.cs ├── Service.cs ├── SimulatorUtils.cs ├── Utils │ ├── AttributeCommandManager.cs │ ├── BackgroundTask.cs │ ├── CSCraftEventHandler.cs │ ├── CSRecipeNote.cs │ ├── Chat.cs │ ├── Colors.cs │ ├── CommunityMacros.cs │ ├── DynamicBars.cs │ ├── FoodStatus.cs │ ├── FuzzyMatcher.cs │ ├── Gearsets.cs │ ├── Hooks.cs │ ├── IconManager.cs │ ├── Ipc.cs │ ├── Log.cs │ ├── MacroCopy.cs │ ├── MacroImport.cs │ ├── ReadOnlySeStringExtensions.cs │ ├── RecipeData.cs │ ├── SimulatedMacro.cs │ ├── SqText.cs │ └── SynthesisValues.cs ├── Windows │ ├── MacroClipboard.cs │ ├── MacroEditor.cs │ ├── MacroList.cs │ ├── RecipeNote.cs │ ├── Settings.cs │ └── SynthHelper.cs └── packages.lock.json ├── Images ├── MacroEditor.png ├── RecipeNote.png └── SynthHelper.png ├── LICENSE ├── README.md ├── Simulator ├── ActionCategory.cs ├── ActionProc.cs ├── ActionResponse.cs ├── ActionStates.cs ├── Actions │ ├── ActionType.cs │ ├── AdvancedTouch.cs │ ├── AdvancedTouchCombo.cs │ ├── BaseAction.cs │ ├── BaseBuffAction.cs │ ├── BaseComboAction.cs │ ├── BaseComboActionImpl.cs │ ├── BasicSynthesis.cs │ ├── BasicTouch.cs │ ├── ByregotsBlessing.cs │ ├── CarefulObservation.cs │ ├── CarefulSynthesis.cs │ ├── DaringTouch.cs │ ├── DelicateSynthesis.cs │ ├── FinalAppraisal.cs │ ├── GreatStrides.cs │ ├── Groundwork.cs │ ├── HastyTouch.cs │ ├── HeartAndSoul.cs │ ├── ImmaculateMend.cs │ ├── Innovation.cs │ ├── IntensiveSynthesis.cs │ ├── Manipulation.cs │ ├── MastersMend.cs │ ├── MuscleMemory.cs │ ├── Observe.cs │ ├── ObservedAdvancedTouchCombo.cs │ ├── PreciseTouch.cs │ ├── PreparatoryTouch.cs │ ├── PrudentSynthesis.cs │ ├── PrudentTouch.cs │ ├── QuickInnovation.cs │ ├── RapidSynthesis.cs │ ├── RefinedTouch.cs │ ├── RefinedTouchCombo.cs │ ├── Reflect.cs │ ├── StandardTouch.cs │ ├── StandardTouchCombo.cs │ ├── TrainedEye.cs │ ├── TrainedFinesse.cs │ ├── TrainedPerfection.cs │ ├── TricksOfTheTrade.cs │ ├── Veneration.cs │ ├── WasteNot.cs │ └── WasteNot2.cs ├── CharacterStats.cs ├── ClassJob.cs ├── CompletionState.cs ├── Condition.cs ├── Craftimizer.Simulator.csproj ├── EffectType.cs ├── Effects.cs ├── Recipe.cs ├── SimulationInput.cs ├── SimulationState.cs ├── Simulator.cs └── SimulatorNoRandom.cs ├── Solver ├── ActionSet.cs ├── ArenaBuffer.cs ├── ArenaNode.cs ├── Craftimizer.Solver.csproj ├── Intrinsics.cs ├── MCTS.cs ├── MCTSConfig.cs ├── NodeScoresBuffer.cs ├── RaphaelUtils.cs ├── RootScores.cs ├── SimulationNode.cs ├── Simulator.cs ├── Solver.cs ├── SolverConfig.cs └── SolverSolution.cs ├── Test ├── Craftimizer.Test.csproj ├── Simulator │ └── Simulator.cs ├── Solver │ └── ActionSet.cs └── Usings.cs └── icon.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text eol=crlf 3 | *.png binary -------------------------------------------------------------------------------- /.github/create_unofficial.py: -------------------------------------------------------------------------------- 1 | import shutil, os, subprocess, zipfile, json, sys 2 | 3 | from itertools import chain 4 | 5 | PROJECT_NAME = sys.argv[1] 6 | OFFICIAL_ZIP = f"{PROJECT_NAME}/bin/x64/Release/{PROJECT_NAME}/latest.zip" 7 | UNOFFICIAL_ZIP = f"{PROJECT_NAME}/bin/x64/Release/{PROJECT_NAME}/latestUnofficial.zip" 8 | 9 | shutil.copy(OFFICIAL_ZIP, UNOFFICIAL_ZIP) 10 | 11 | subprocess.check_call(['7z', 'd', UNOFFICIAL_ZIP, f"{PROJECT_NAME}.json"]) 12 | 13 | with zipfile.ZipFile(UNOFFICIAL_ZIP) as file: 14 | 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")] 15 | 16 | subprocess.check_call(['7z', 'rn', UNOFFICIAL_ZIP] + list(chain.from_iterable((m, m.replace(PROJECT_NAME, f"{PROJECT_NAME}Unofficial")) for m in members))) 17 | 18 | with open(f"{PROJECT_NAME}/bin/x64/Release/{PROJECT_NAME}/{PROJECT_NAME}.json") as file: 19 | manifest = json.load(file) 20 | 21 | manifest['Punchline'] = f"Unofficial/uncertified build of {manifest['Name']}. {manifest['Punchline']}" 22 | manifest['InternalName'] += 'Unofficial' 23 | manifest['Name'] += ' (Unofficial)' 24 | manifest['IconUrl'] = f"https://raw.githubusercontent.com/WorkingRobot/MyDalamudPlugins/main/icons/{manifest['InternalName']}.png" 25 | 26 | with zipfile.ZipFile(UNOFFICIAL_ZIP, "a", zipfile.ZIP_DEFLATED, compresslevel = 7) as file: 27 | file.writestr(f"{PROJECT_NAME}Unofficial.json", json.dumps(manifest, indent = 2)) -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push 5 | 6 | env: 7 | PLUGIN_REPO: WorkingRobot/MyDalamudPlugins 8 | PROJECT_NAME: Craftimizer 9 | IS_OFFICIAL: ${{true}} 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | env: 15 | DOTNET_CLI_TELEMETRY_OPTOUT: true 16 | 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | submodules: recursive 22 | 23 | - name: Setup .NET 24 | uses: actions/setup-dotnet@v4 25 | with: 26 | dotnet-version: '9.0' 27 | 28 | - name: Download Dalamud 29 | run: | 30 | wget https://goatcorp.github.io/dalamud-distrib/latest.zip 31 | unzip latest.zip -d dalamud/ 32 | echo "DALAMUD_HOME=$PWD/dalamud" >> $GITHUB_ENV 33 | 34 | - name: Restore 35 | run: | 36 | dotnet restore -r win 37 | 38 | - name: Build 39 | run: | 40 | dotnet build --configuration Release --no-restore 41 | 42 | - name: Test 43 | run: | 44 | dotnet test --configuration Release --logger "trx;logfilename=results.trx" --logger "html;logfilename=results.html" --logger "console;verbosity=detailed" --no-build --results-directory="TestResults" 45 | 46 | - name: Create Unofficial Builds 47 | if: ${{env.IS_OFFICIAL}} 48 | run: python ./.github/create_unofficial.py ${{env.PROJECT_NAME}} 49 | 50 | - name: Upload Artifacts 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: ${{env.PROJECT_NAME}} 54 | path: ${{env.PROJECT_NAME}}/bin/x64/Release/${{env.PROJECT_NAME}} 55 | if-no-files-found: error 56 | 57 | - name: Upload Test Results 58 | uses: actions/upload-artifact@v4 59 | if: ${{ !cancelled() }} 60 | with: 61 | name: TestResults 62 | path: TestResults 63 | 64 | - name: Create Release 65 | uses: softprops/action-gh-release@v1 66 | if: startsWith(github.ref, 'refs/tags/') 67 | id: release 68 | with: 69 | files: ${{env.PROJECT_NAME}}/bin/x64/Release/${{env.PROJECT_NAME}}/* 70 | 71 | - name: Trigger Plugin Repo Update 72 | uses: peter-evans/repository-dispatch@v2 73 | if: ${{ steps.release.conclusion == 'success' }} 74 | with: 75 | token: ${{secrets.PAT}} 76 | repository: ${{env.PLUGIN_REPO}} 77 | event-type: new-release 78 | 79 | bench: 80 | runs-on: windows-latest 81 | env: 82 | DOTNET_CLI_TELEMETRY_OPTOUT: true 83 | 84 | steps: 85 | - name: Checkout 86 | uses: actions/checkout@v4 87 | with: 88 | submodules: recursive 89 | 90 | - name: Setup .NET 91 | uses: actions/setup-dotnet@v4 92 | with: 93 | dotnet-version: '9.0' 94 | 95 | - name: Download Dalamud 96 | run: | 97 | Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip 98 | Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev\" 99 | 100 | - name: Restore 101 | run: | 102 | dotnet restore -r win 103 | 104 | - name: Benchmark 105 | run: | 106 | dotnet run --configuration Release --project Benchmark -- -e json html github csv -f * -d -m 107 | 108 | - name: Upload Test Results 109 | uses: actions/upload-artifact@v4 110 | with: 111 | name: BenchmarkResults 112 | path: BenchmarkDotNet.Artifacts 113 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | obj/ 3 | bin/ 4 | *.user 5 | BenchmarkDotNet.Artifacts/ -------------------------------------------------------------------------------- /Benchmark/Bench.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Diagnosers; 3 | using BenchmarkDotNet.Diagnostics.dotTrace; 4 | using BenchmarkDotNet.Jobs; 5 | using Craftimizer.Simulator; 6 | using Craftimizer.Solver; 7 | using System.Runtime.CompilerServices; 8 | 9 | namespace Craftimizer.Benchmark; 10 | 11 | [SimpleJob(RuntimeMoniker.Net80, baseline: true)] 12 | //[SimpleJob(RuntimeMoniker.Net90)] 13 | [MinColumn, Q1Column, Q3Column, MaxColumn] 14 | //[DotTraceDiagnoser] 15 | //[MemoryDiagnoser] 16 | [DisassemblyDiagnoser(maxDepth: 500, exportGithubMarkdown: false, exportHtml: true)] 17 | public class Bench 18 | { 19 | public record struct HashWrapper(T Data) where T : notnull 20 | { 21 | public static implicit operator T(HashWrapper wrapper) => wrapper.Data; 22 | 23 | public override readonly string ToString() => 24 | $"{HashCode.Combine(Data.ToString()!):X8}"; 25 | } 26 | 27 | private static SimulationInput[] Inputs { get; } = 28 | [ 29 | // https://craftingway.app/rotation/loud-namazu-jVe9Y 30 | // Chondrite Saw 31 | new(new() 32 | { 33 | Craftsmanship = 3304, 34 | Control = 3374, 35 | CP = 575, 36 | Level = 90, 37 | CanUseManipulation = true, 38 | HasSplendorousBuff = false, 39 | IsSpecialist = false, 40 | }, 41 | new() 42 | { 43 | IsExpert = false, 44 | ClassJobLevel = 90, 45 | ConditionsFlag = 0b1111, 46 | MaxDurability = 80, 47 | MaxQuality = 7200, 48 | MaxProgress = 3500, 49 | QualityModifier = 80, 50 | QualityDivider = 115, 51 | ProgressModifier = 90, 52 | ProgressDivider = 130 53 | }), 54 | 55 | // https://craftingway.app/rotation/sandy-fafnir-doVCs 56 | // Classical Longsword 57 | new(new() 58 | { 59 | Craftsmanship = 3290, 60 | Control = 3541, 61 | CP = 649, 62 | Level = 90, 63 | CanUseManipulation = true, 64 | HasSplendorousBuff = false, 65 | IsSpecialist = false, 66 | }, 67 | new() 68 | { 69 | IsExpert = false, 70 | ClassJobLevel = 90, 71 | ConditionsFlag = 0b1111, 72 | MaxDurability = 70, 73 | MaxQuality = 10920, 74 | MaxProgress = 3900, 75 | QualityModifier = 70, 76 | QualityDivider = 115, 77 | ProgressModifier = 80, 78 | ProgressDivider = 130 79 | }) 80 | ]; 81 | 82 | public static IEnumerable> States => Inputs.Select(i => new HashWrapper(new(i))); 83 | 84 | public static IEnumerable> Configs => new HashWrapper[] 85 | { 86 | new(new() 87 | { 88 | Algorithm = SolverAlgorithm.Stepwise, 89 | Iterations = 30_000, 90 | }) 91 | }; 92 | 93 | [ParamsSource(nameof(States))] 94 | public HashWrapper State { get; set; } 95 | 96 | [ParamsSource(nameof(Configs))] 97 | public HashWrapper Config { get; set; } 98 | 99 | // [Benchmark] 100 | public async Task SolveAsync() 101 | { 102 | var solver = new Solver.Solver(Config, State); 103 | solver.Start(); 104 | var (_, s) = await solver.GetTask().ConfigureAwait(false); 105 | return (float)s.Quality / s.Input.Recipe.MaxQuality; 106 | } 107 | 108 | [Benchmark] 109 | [MethodImpl(MethodImplOptions.NoInlining)] 110 | public (float MaxScore, SolverSolution Solution) Solve() 111 | { 112 | var config = new MCTSConfig(Config.Data); 113 | 114 | var solver = new MCTS(config, State); 115 | var progress = 0; 116 | solver.Search(Config.Data.Iterations, Config.Data.MaxIterations, ref progress, CancellationToken.None); 117 | var solution = solver.Solution(); 118 | 119 | return (solver.MaxScore, solution); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Benchmark/Craftimizer.Benchmark.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | Exe 6 | enable 7 | enable 8 | true 9 | x64 10 | Debug;Release;Deterministic 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | $(DefineConstants);IS_DETERMINISTIC 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Benchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Simulator; 2 | using Craftimizer.Simulator.Actions; 3 | using Craftimizer.Solver; 4 | using ObjectLayoutInspector; 5 | using System.Diagnostics; 6 | 7 | namespace Craftimizer.Benchmark; 8 | 9 | internal static class Program 10 | { 11 | private static void Main(string[] args) 12 | { 13 | #if IS_DETERMINISTIC 14 | var b = new Bench(); 15 | 16 | var initConfig = Bench.Configs.First(); 17 | var initState = Bench.States.First(); 18 | 19 | var config = new MCTSConfig(initConfig.Data); 20 | 21 | var s = Stopwatch.StartNew(); 22 | for (var i = 0; i < 100; ++i) 23 | { 24 | var solver = new MCTS(config, initState); 25 | var progress = 0; 26 | solver.Search(initConfig.Data.Iterations, initConfig.Data.MaxIterations, ref progress, CancellationToken.None); 27 | var solution = solver.Solution(); 28 | Console.WriteLine($"{i+1}"); 29 | } 30 | s.Stop(); 31 | Console.WriteLine($"{s.Elapsed.TotalMilliseconds:0.00}ms"); 32 | #else 33 | RunBench(args); 34 | #endif 35 | 36 | // return RunOther(); 37 | } 38 | 39 | private static void RunBench(string[] args) 40 | { 41 | Environment.SetEnvironmentVariable("IS_BENCH", "1"); 42 | BenchmarkDotNet.Running.BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); 43 | } 44 | 45 | private static async Task RunTrace() 46 | { 47 | var input = new SimulationInput( 48 | new() 49 | { 50 | Craftsmanship = 4041, 51 | Control = 3905, 52 | CP = 609, 53 | Level = 90, 54 | CanUseManipulation = true, 55 | HasSplendorousBuff = false, 56 | IsSpecialist = false, 57 | }, 58 | new RecipeInfo() 59 | { 60 | IsExpert = false, 61 | ClassJobLevel = 90, 62 | ConditionsFlag = 15, 63 | MaxDurability = 70, 64 | MaxQuality = 14040, 65 | MaxProgress = 6600, 66 | QualityModifier = 70, 67 | QualityDivider = 115, 68 | ProgressModifier = 80, 69 | ProgressDivider = 130, 70 | } 71 | ); 72 | var config = new SolverConfig() 73 | { 74 | Algorithm = SolverAlgorithm.Stepwise, 75 | Iterations = 30000, 76 | MaxStepCount = 25 77 | }; 78 | var solver = new Solver.Solver(config, new(input)); 79 | solver.OnNewAction += s => Console.WriteLine($">{s}"); 80 | solver.Start(); 81 | var (_, s) = await solver.GetTask().ConfigureAwait(false); 82 | Console.WriteLine($"Qual: {s.Quality}/{s.Input.Recipe.MaxQuality}"); 83 | } 84 | 85 | private static async Task RunOther() 86 | { 87 | TypeLayout.PrintLayout(true); 88 | TypeLayout.PrintLayout(true); 89 | TypeLayout.PrintLayout(true); 90 | TypeLayout.PrintLayout(true); 91 | return; 92 | 93 | var input = new SimulationInput( 94 | new CharacterStats 95 | { 96 | Craftsmanship = 4078, 97 | Control = 3897, 98 | CP = 704, 99 | Level = 90, 100 | CanUseManipulation = true, 101 | HasSplendorousBuff = false, 102 | IsSpecialist = false, 103 | }, 104 | new RecipeInfo() 105 | { 106 | IsExpert = false, 107 | ClassJobLevel = 90, 108 | ConditionsFlag = 15, 109 | MaxDurability = 70, 110 | MaxQuality = 14040, 111 | MaxProgress = 6600, 112 | QualityModifier = 70, 113 | QualityDivider = 115, 114 | ProgressModifier = 80, 115 | ProgressDivider = 130, 116 | } 117 | ); 118 | 119 | var config = new SolverConfig() 120 | { 121 | Iterations = 100_000, 122 | ForkCount = 32, 123 | FurcatedActionCount = 16, 124 | MaxStepCount = 30, 125 | }; 126 | 127 | var sim = new SimulatorNoRandom(); 128 | (_, var state) = sim.Execute(new(input), ActionType.MuscleMemory); 129 | (_, state) = sim.Execute(state, ActionType.PrudentTouch); 130 | //(_, state) = sim.Execute(state, ActionType.Manipulation); 131 | //(_, state) = sim.Execute(state, ActionType.Veneration); 132 | //(_, state) = sim.Execute(state, ActionType.WasteNot); 133 | //(_, state) = sim.Execute(state, ActionType.Groundwork); 134 | //(_, state) = sim.Execute(state, ActionType.Groundwork); 135 | //(_, state) = sim.Execute(state, ActionType.Groundwork); 136 | //(_, state) = sim.Execute(state, ActionType.Innovation); 137 | //(_, state) = sim.Execute(state, ActionType.PrudentTouch); 138 | //(_, state) = sim.Execute(state, ActionType.AdvancedTouchCombo); 139 | //(_, state) = sim.Execute(state, ActionType.Manipulation); 140 | //(_, state) = sim.Execute(state, ActionType.Innovation); 141 | //(_, state) = sim.Execute(state, ActionType.PrudentTouch); 142 | //(_, state) = sim.Execute(state, ActionType.AdvancedTouchCombo); 143 | //(_, state) = sim.Execute(state, ActionType.GreatStrides); 144 | //(_, state) = sim.Execute(state, ActionType.Innovation); 145 | //(_, state) = sim.Execute(state, ActionType.FocusedTouchCombo); 146 | //(_, state) = sim.Execute(state, ActionType.GreatStrides); 147 | //(_, state) = sim.Execute(state, ActionType.ByregotsBlessing); 148 | //(_, state) = sim.Execute(state, ActionType.CarefulSynthesis); 149 | //(_, state) = sim.Execute(state, ActionType.CarefulSynthesis); 150 | 151 | Console.WriteLine($"{state.Quality} {state.CP} {state.Progress} {state.Durability}"); 152 | //return; 153 | var solver = new Solver.Solver(config, state); 154 | solver.OnLog += Console.WriteLine; 155 | solver.OnNewAction += s => Console.WriteLine(s); 156 | solver.Start(); 157 | var (_, s) = await solver.GetTask().ConfigureAwait(false); 158 | Console.WriteLine($"Qual: {s.Quality}/{s.Input.Recipe.MaxQuality}"); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Craftimizer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33213.308 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer", "Craftimizer\Craftimizer.csproj", "{13C812E9-0D42-4B95-8646-40EEBF30636F}" 7 | ProjectSection(ProjectDependencies) = postProject 8 | {2B0EA452-6DFC-48DB-9049-EA782E600C21} = {2B0EA452-6DFC-48DB-9049-EA782E600C21} 9 | EndProjectSection 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Benchmark", "Benchmark\Craftimizer.Benchmark.csproj", "{057C4B64-4D99-4847-9BCF-966571CAE57C}" 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Simulator", "Simulator\Craftimizer.Simulator.csproj", "{172EE849-AC7E-4F2A-ACAB-EF9D065523B3}" 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Solver", "Solver\Craftimizer.Solver.csproj", "{2B0EA452-6DFC-48DB-9049-EA782E600C21}" 16 | ProjectSection(ProjectDependencies) = postProject 17 | {172EE849-AC7E-4F2A-ACAB-EF9D065523B3} = {172EE849-AC7E-4F2A-ACAB-EF9D065523B3} 18 | EndProjectSection 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Test", "Test\Craftimizer.Test.csproj", "{C3AEA981-9DA8-405C-995B-86528493891B}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Debug|x64 = Debug|x64 26 | Deterministic|Any CPU = Deterministic|Any CPU 27 | Deterministic|x64 = Deterministic|x64 28 | Release|Any CPU = Release|Any CPU 29 | Release|x64 = Release|x64 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|x64 33 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|x64 34 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 35 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 36 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Deterministic|Any CPU.ActiveCfg = Debug|x64 37 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Deterministic|Any CPU.Build.0 = Debug|x64 38 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Deterministic|x64.ActiveCfg = Release|x64 39 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|x64 40 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|x64 41 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 42 | {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 43 | {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|Any CPU.ActiveCfg = Debug|x64 44 | {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|Any CPU.Build.0 = Debug|x64 45 | {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.ActiveCfg = Debug|x64 46 | {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.Build.0 = Debug|x64 47 | {057C4B64-4D99-4847-9BCF-966571CAE57C}.Deterministic|Any CPU.ActiveCfg = Deterministic|x64 48 | {057C4B64-4D99-4847-9BCF-966571CAE57C}.Deterministic|Any CPU.Build.0 = Deterministic|x64 49 | {057C4B64-4D99-4847-9BCF-966571CAE57C}.Deterministic|x64.ActiveCfg = Deterministic|x64 50 | {057C4B64-4D99-4847-9BCF-966571CAE57C}.Deterministic|x64.Build.0 = Deterministic|x64 51 | {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|Any CPU.ActiveCfg = Release|x64 52 | {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|Any CPU.Build.0 = Release|x64 53 | {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.ActiveCfg = Release|x64 54 | {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.Build.0 = Release|x64 55 | {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|Any CPU.ActiveCfg = Debug|x64 56 | {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|Any CPU.Build.0 = Debug|x64 57 | {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.ActiveCfg = Debug|x64 58 | {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.Build.0 = Debug|x64 59 | {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Deterministic|Any CPU.ActiveCfg = Deterministic|x64 60 | {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Deterministic|Any CPU.Build.0 = Deterministic|x64 61 | {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Deterministic|x64.ActiveCfg = Deterministic|x64 62 | {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Deterministic|x64.Build.0 = Deterministic|x64 63 | {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|Any CPU.ActiveCfg = Release|x64 64 | {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|Any CPU.Build.0 = Release|x64 65 | {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.ActiveCfg = Release|x64 66 | {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.Build.0 = Release|x64 67 | {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|Any CPU.ActiveCfg = Debug|x64 68 | {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|Any CPU.Build.0 = Debug|x64 69 | {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.ActiveCfg = Debug|x64 70 | {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.Build.0 = Debug|x64 71 | {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Deterministic|Any CPU.ActiveCfg = Deterministic|x64 72 | {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Deterministic|Any CPU.Build.0 = Deterministic|x64 73 | {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Deterministic|x64.ActiveCfg = Deterministic|x64 74 | {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Deterministic|x64.Build.0 = Deterministic|x64 75 | {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|Any CPU.ActiveCfg = Release|x64 76 | {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|Any CPU.Build.0 = Release|x64 77 | {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.ActiveCfg = Release|x64 78 | {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.Build.0 = Release|x64 79 | {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|Any CPU.ActiveCfg = Debug|x64 80 | {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|Any CPU.Build.0 = Debug|x64 81 | {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.ActiveCfg = Debug|x64 82 | {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.Build.0 = Debug|x64 83 | {C3AEA981-9DA8-405C-995B-86528493891B}.Deterministic|Any CPU.ActiveCfg = Deterministic|x64 84 | {C3AEA981-9DA8-405C-995B-86528493891B}.Deterministic|Any CPU.Build.0 = Deterministic|x64 85 | {C3AEA981-9DA8-405C-995B-86528493891B}.Deterministic|x64.ActiveCfg = Deterministic|x64 86 | {C3AEA981-9DA8-405C-995B-86528493891B}.Release|Any CPU.ActiveCfg = Release|x64 87 | {C3AEA981-9DA8-405C-995B-86528493891B}.Release|Any CPU.Build.0 = Release|x64 88 | {C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.ActiveCfg = Release|x64 89 | {C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.Build.0 = Release|x64 90 | EndGlobalSection 91 | GlobalSection(SolutionProperties) = preSolution 92 | HideSolutionNode = FALSE 93 | EndGlobalSection 94 | GlobalSection(ExtensibilityGlobals) = postSolution 95 | SolutionGuid = {B17E85B1-5F60-4440-9F9A-3DDE877E8CDF} 96 | EndGlobalSection 97 | EndGlobal 98 | -------------------------------------------------------------------------------- /Craftimizer/Craftimizer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Asriel Camora 5 | 2.7.2.1 6 | https://github.com/WorkingRobot/Craftimizer.git 7 | Debug;Release 8 | 9 | 10 | 11 | net9.0-windows7.0 12 | x64 13 | enable 14 | true 15 | false 16 | win-x64 17 | false 18 | false 19 | true 20 | true 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | all 38 | runtime; build; native; contentfiles; analyzers; buildtransitive 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Craftimizer/Craftimizer.json: -------------------------------------------------------------------------------- 1 | { 2 | "Author": "Asriel", 3 | "Name": "Craftimizer", 4 | "Punchline": "Simulate crafts, create computer-assisted macros, and get mid-craft suggestions from the comfort of your own game!", 5 | "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!", 6 | "RepoUrl": "https://github.com/WorkingRobot/Craftimizer", 7 | "InternalName": "Craftimizer", 8 | "ApplicableVersion": "any", 9 | "Tags": [ 10 | "crafting", 11 | "doh", 12 | "craft", 13 | "macro", 14 | "solver", 15 | "generator", 16 | "generate", 17 | "simulate", 18 | "sim", 19 | "simulator" 20 | ], 21 | "CategoryTags": [ 22 | "Jobs" 23 | ], 24 | "IconUrl": "https://git.camora.dev/asriel/Craftimizer/raw/branch/main/icon.png", 25 | "ImageUrls": [ 26 | "https://git.camora.dev/asriel/Craftimizer/raw/branch/main/Images/RecipeNote.png", 27 | "https://git.camora.dev/asriel/Craftimizer/raw/branch/main/Images/SynthHelper.png", 28 | "https://git.camora.dev/asriel/Craftimizer/raw/branch/main/Images/MacroEditor.png" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /Craftimizer/Graphics/collectible_badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkingRobot/Craftimizer/fd1651cef90acaa0636316851a81b0137a7c8f86/Craftimizer/Graphics/collectible_badge.png -------------------------------------------------------------------------------- /Craftimizer/Graphics/expert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkingRobot/Craftimizer/fd1651cef90acaa0636316851a81b0137a7c8f86/Craftimizer/Graphics/expert.png -------------------------------------------------------------------------------- /Craftimizer/Graphics/expert_badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkingRobot/Craftimizer/fd1651cef90acaa0636316851a81b0137a7c8f86/Craftimizer/Graphics/expert_badge.png -------------------------------------------------------------------------------- /Craftimizer/Graphics/horse_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkingRobot/Craftimizer/fd1651cef90acaa0636316851a81b0137a7c8f86/Craftimizer/Graphics/horse_icon.png -------------------------------------------------------------------------------- /Craftimizer/Graphics/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkingRobot/Craftimizer/fd1651cef90acaa0636316851a81b0137a7c8f86/Craftimizer/Graphics/icon.png -------------------------------------------------------------------------------- /Craftimizer/Graphics/no_manip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkingRobot/Craftimizer/fd1651cef90acaa0636316851a81b0137a7c8f86/Craftimizer/Graphics/no_manip.png -------------------------------------------------------------------------------- /Craftimizer/Graphics/specialist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkingRobot/Craftimizer/fd1651cef90acaa0636316851a81b0137a7c8f86/Craftimizer/Graphics/specialist.png -------------------------------------------------------------------------------- /Craftimizer/Graphics/splendorous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkingRobot/Craftimizer/fd1651cef90acaa0636316851a81b0137a7c8f86/Craftimizer/Graphics/splendorous.png -------------------------------------------------------------------------------- /Craftimizer/ImRaii2.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Interface.Utility.Raii; 2 | using ImGuiNET; 3 | using ImPlotNET; 4 | using System; 5 | using System.Numerics; 6 | 7 | namespace Craftimizer.Plugin; 8 | 9 | public static class ImRaii2 10 | { 11 | private struct EndUnconditionally(Action endAction, bool success) : ImRaii.IEndObject, IDisposable 12 | { 13 | private Action EndAction { get; } = endAction; 14 | 15 | public bool Success { get; } = success; 16 | 17 | public bool Disposed { get; private set; } = false; 18 | 19 | public void Dispose() 20 | { 21 | if (!Disposed) 22 | { 23 | EndAction(); 24 | Disposed = true; 25 | } 26 | } 27 | } 28 | 29 | private struct EndConditionally(Action endAction, bool success) : ImRaii.IEndObject, IDisposable 30 | { 31 | public bool Success { get; } = success; 32 | 33 | public bool Disposed { get; private set; } = false; 34 | 35 | private Action EndAction { get; } = endAction; 36 | 37 | public void Dispose() 38 | { 39 | if (!Disposed) 40 | { 41 | if (Success) 42 | { 43 | EndAction(); 44 | } 45 | 46 | Disposed = true; 47 | } 48 | } 49 | } 50 | 51 | public static ImRaii.IEndObject GroupPanel(string name, float width, out float internalWidth) 52 | { 53 | internalWidth = ImGuiUtils.BeginGroupPanel(name, width); 54 | return new EndUnconditionally(ImGuiUtils.EndGroupPanel, true); 55 | } 56 | 57 | public static ImRaii.IEndObject Plot(string title_id, Vector2 size, ImPlotFlags flags) 58 | { 59 | return new EndConditionally(new Action(ImPlot.EndPlot), ImPlot.BeginPlot(title_id, size, flags)); 60 | } 61 | 62 | public static ImRaii.IEndObject PushStyle(ImPlotStyleVar idx, Vector2 val) 63 | { 64 | ImPlot.PushStyleVar(idx, val); 65 | return new EndUnconditionally(ImPlot.PopStyleVar, true); 66 | } 67 | 68 | public static ImRaii.IEndObject PushStyle(ImPlotStyleVar idx, float val) 69 | { 70 | ImPlot.PushStyleVar(idx, val); 71 | return new EndUnconditionally(ImPlot.PopStyleVar, true); 72 | } 73 | 74 | public static ImRaii.IEndObject PushColor(ImPlotCol idx, Vector4 col) 75 | { 76 | ImPlot.PushStyleColor(idx, col); 77 | return new EndUnconditionally(ImPlot.PopStyleColor, true); 78 | } 79 | 80 | public static ImRaii.IEndObject TextWrapPos(float wrap_local_pos_x) 81 | { 82 | ImGui.PushTextWrapPos(wrap_local_pos_x); 83 | return new EndUnconditionally(ImGui.PopTextWrapPos, true); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Craftimizer/LuminaSheets.cs: -------------------------------------------------------------------------------- 1 | using Lumina.Data; 2 | using Lumina.Excel; 3 | using Lumina.Excel.Sheets; 4 | 5 | namespace Craftimizer.Plugin; 6 | 7 | public static class LuminaSheets 8 | { 9 | private static readonly ExcelModule Module = Service.DataManager.GameData.Excel; 10 | 11 | public static readonly ExcelSheet RecipeSheet = Module.GetSheet(); 12 | public static readonly ExcelSheet ActionSheet = Module.GetSheet(); 13 | public static readonly ExcelSheet CraftActionSheet = Module.GetSheet(); 14 | public static readonly ExcelSheet StatusSheet = Module.GetSheet(); 15 | public static readonly ExcelSheet AddonSheet = Module.GetSheet(); 16 | public static readonly ExcelSheet ClassJobSheet = Module.GetSheet(); 17 | public static readonly ExcelSheet ItemSheet = Module.GetSheet(); 18 | public static readonly ExcelSheet ItemSheetEnglish = Module.GetSheet(Language.English)!; 19 | public static readonly ExcelSheet LevelSheet = Module.GetSheet(); 20 | public static readonly ExcelSheet QuestSheet = Module.GetSheet(); 21 | public static readonly ExcelSheet MateriaSheet = Module.GetSheet(); 22 | public static readonly ExcelSheet BaseParamSheet = Module.GetSheet(); 23 | public static readonly ExcelSheet ItemFoodSheet = Module.GetSheet(); 24 | public static readonly ExcelSheet WKSMissionToDoEvalutionRefinSheet = Module.GetSheet(); 25 | public static readonly ExcelSheet RecipeLevelTableSheet = Module.GetSheet(); 26 | public static readonly ExcelSheet GathererCrafterLvAdjustTableSheet = Module.GetSheet(); 27 | } 28 | -------------------------------------------------------------------------------- /Craftimizer/Plugin.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Plugin.Windows; 2 | using Craftimizer.Simulator; 3 | using Craftimizer.Simulator.Actions; 4 | using Craftimizer.Utils; 5 | using Craftimizer.Windows; 6 | using Dalamud.Interface.ImGuiNotification; 7 | using Dalamud.Interface.Windowing; 8 | using Dalamud.Plugin; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Reflection; 12 | 13 | namespace Craftimizer.Plugin; 14 | 15 | public sealed class Plugin : IDalamudPlugin 16 | { 17 | public string Version { get; } 18 | public string Author { get; } 19 | public string BuildConfiguration { get; } 20 | public ILoadedTextureIcon Icon { get; } 21 | public const string SupportLink = "https://ko-fi.com/camora"; 22 | 23 | public WindowSystem WindowSystem { get; } 24 | public Settings SettingsWindow { get; } 25 | public RecipeNote RecipeNoteWindow { get; } 26 | public SynthHelper SynthHelperWindow { get; } 27 | public MacroList ListWindow { get; private set; } 28 | public MacroEditor? EditorWindow { get; private set; } 29 | public MacroClipboard? ClipboardWindow { get; private set; } 30 | 31 | public Configuration Configuration { get; } 32 | public IconManager IconManager { get; } 33 | public Hooks Hooks { get; } 34 | public CommunityMacros CommunityMacros { get; } 35 | public Ipc Ipc { get; } 36 | public AttributeCommandManager AttributeCommandManager { get; } 37 | 38 | public Plugin(IDalamudPluginInterface pluginInterface) 39 | { 40 | Service.Initialize(this, pluginInterface); 41 | 42 | WindowSystem = new("Craftimizer"); 43 | Configuration = Configuration.Load(); 44 | IconManager = new(); 45 | Hooks = new(); 46 | CommunityMacros = new(); 47 | Ipc = new(); 48 | AttributeCommandManager = new(); 49 | 50 | var assembly = Assembly.GetExecutingAssembly(); 51 | Version = assembly.GetCustomAttribute()!.InformationalVersion.Split('+')[0]; 52 | Author = assembly.GetCustomAttribute()!.Company; 53 | BuildConfiguration = assembly.GetCustomAttribute()!.Configuration; 54 | if (DateTime.Now is { Day: 1, Month: 4 }) 55 | Icon = IconManager.GetAssemblyTexture("Graphics.horse_icon.png"); 56 | else 57 | Icon = IconManager.GetAssemblyTexture("Graphics.icon.png"); 58 | 59 | SettingsWindow = new(); 60 | RecipeNoteWindow = new(); 61 | SynthHelperWindow = new(); 62 | ListWindow = new(); 63 | 64 | // Trigger static constructors so a hitch doesn't occur on first RecipeNote frame. 65 | FoodStatus.Initialize(); 66 | ActionUtils.Initialize(); 67 | 68 | Service.PluginInterface.UiBuilder.Draw += WindowSystem.Draw; 69 | Service.PluginInterface.UiBuilder.OpenConfigUi += OpenSettingsWindowForced; 70 | Service.PluginInterface.UiBuilder.OpenMainUi += OpenCraftingLog; 71 | } 72 | 73 | public (CharacterStats? Character, RecipeData? Recipe, MacroEditor.CrafterBuffs? Buffs) GetOpenedStats() 74 | { 75 | var editorWindow = (EditorWindow?.IsOpen ?? false) ? EditorWindow : null; 76 | var recipeData = editorWindow?.RecipeData ?? Service.Plugin.RecipeNoteWindow.RecipeData; 77 | var characterStats = editorWindow?.CharacterStats ?? Service.Plugin.RecipeNoteWindow.CharacterStats; 78 | var buffs = editorWindow?.Buffs ?? (RecipeNoteWindow.CharacterStats != null ? new(Service.ClientState.LocalPlayer?.StatusList) : null); 79 | 80 | return (characterStats, recipeData, buffs); 81 | } 82 | 83 | public (CharacterStats Character, RecipeData Recipe, MacroEditor.CrafterBuffs Buffs) GetDefaultStats() 84 | { 85 | var stats = GetOpenedStats(); 86 | return ( 87 | stats.Character ?? new() 88 | { 89 | Craftsmanship = 100, 90 | Control = 100, 91 | CP = 200, 92 | Level = 10, 93 | CanUseManipulation = false, 94 | HasSplendorousBuff = false, 95 | IsSpecialist = false, 96 | }, 97 | stats.Recipe ?? new(1023), 98 | stats.Buffs ?? new(null) 99 | ); 100 | } 101 | 102 | [Command(name: "/crafteditor", aliases: "/macroeditor", description: "Open the crafting macro editor.")] 103 | public void OpenEmptyMacroEditor() 104 | { 105 | var stats = GetDefaultStats(); 106 | OpenMacroEditor(stats.Character, stats.Recipe, stats.Buffs, null, [], null); 107 | } 108 | 109 | public void OpenMacroEditor(CharacterStats characterStats, RecipeData recipeData, MacroEditor.CrafterBuffs buffs, IEnumerable? ingredientHqCounts, IEnumerable actions, Action>? setter) 110 | { 111 | EditorWindow?.Dispose(); 112 | EditorWindow = new(characterStats, recipeData, buffs, ingredientHqCounts, actions, setter); 113 | } 114 | 115 | [Command(name: "/craftaction", description: "Execute the suggested action in the synthesis helper. Can also be run inside a macro. This command is useful for controller players.")] 116 | public void ExecuteSuggestedSynthHelperAction() => 117 | SynthHelperWindow.ExecuteNextAction(); 118 | 119 | [Command(name: "/craftretry", description: "Clicks \"Retry\" in the synthesis helper. Can also be run inside a macro. This command is useful for controller players.")] 120 | public void ExecuteRetrySynthHelper() => 121 | SynthHelperWindow.AttemptRetry(); 122 | 123 | [Command(name: "/craftimizer", description: "Open the settings window.")] 124 | private void OpenSettingsWindowForced() => 125 | OpenSettingsWindow(true); 126 | 127 | public void OpenSettingsWindow(bool force = false) 128 | { 129 | if (SettingsWindow.IsOpen ^= !force || !SettingsWindow.IsOpen) 130 | SettingsWindow.BringToFront(); 131 | } 132 | 133 | public void OpenSettingsTab(string selectedTabLabel) 134 | { 135 | OpenSettingsWindow(true); 136 | SettingsWindow.SelectTab(selectedTabLabel); 137 | } 138 | 139 | [Command(name: "/craftmacros", aliases: "/macrolist", description: "Open the crafting macros window.")] 140 | public void OpenMacroListWindow() 141 | { 142 | ListWindow.IsOpen = true; 143 | ListWindow.BringToFront(); 144 | } 145 | 146 | public void OpenCraftingLog() 147 | { 148 | Chat.SendMessage("/craftinglog"); 149 | } 150 | 151 | public void OpenMacroClipboard(List macros) 152 | { 153 | ClipboardWindow?.Dispose(); 154 | ClipboardWindow = new(macros); 155 | } 156 | 157 | public IActiveNotification DisplaySolverWarning(string text) => 158 | DisplayNotification(new() 159 | { 160 | Content = text, 161 | Title = "Solver Warning", 162 | Type = NotificationType.Warning 163 | }); 164 | 165 | public IActiveNotification DisplayNotification(Notification notification) 166 | { 167 | var ret = Service.NotificationManager.AddNotification(notification); 168 | // ret.SetIconTexture(Icon.RentAsync().ContinueWith(t => (IDalamudTextureWrap?)t)); 169 | return ret; 170 | } 171 | 172 | public void Dispose() 173 | { 174 | AttributeCommandManager.Dispose(); 175 | SettingsWindow.Dispose(); 176 | RecipeNoteWindow.Dispose(); 177 | SynthHelperWindow.Dispose(); 178 | ListWindow.Dispose(); 179 | EditorWindow?.Dispose(); 180 | ClipboardWindow?.Dispose(); 181 | IconManager.Dispose(); 182 | Hooks.Dispose(); 183 | Icon.Dispose(); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Craftimizer/Service.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Utils; 2 | using Dalamud.Game; 3 | using Dalamud.Game.ClientState.Objects; 4 | using Dalamud.Interface.Windowing; 5 | using Dalamud.IoC; 6 | using Dalamud.Plugin; 7 | using Dalamud.Plugin.Services; 8 | using Dalamud.Storage.Assets; 9 | 10 | namespace Craftimizer.Plugin; 11 | 12 | #pragma warning disable SeStringEvaluator 13 | 14 | public sealed class Service 15 | { 16 | #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. 17 | [PluginService] public static IDalamudPluginInterface PluginInterface { get; private set; } 18 | [PluginService] public static ICommandManager CommandManager { get; private set; } 19 | [PluginService] public static IObjectTable Objects { get; private set; } 20 | [PluginService] public static ISigScanner SigScanner { get; private set; } 21 | [PluginService] public static IGameGui GameGui { get; private set; } 22 | [PluginService] public static IClientState ClientState { get; private set; } 23 | [PluginService] public static IDataManager DataManager { get; private set; } 24 | [PluginService] public static ITextureProvider TextureProvider { get; private set; } 25 | [PluginService] public static IDalamudAssetManager DalamudAssetManager { get; private set; } 26 | [PluginService] public static ITargetManager TargetManager { get; private set; } 27 | [PluginService] public static ICondition Condition { get; private set; } 28 | [PluginService] public static IFramework Framework { get; private set; } 29 | [PluginService] public static IPluginLog PluginLog { get; private set; } 30 | [PluginService] public static IGameInteropProvider GameInteropProvider { get; private set; } 31 | [PluginService] public static INotificationManager NotificationManager { get; private set; } 32 | [PluginService] public static ISeStringEvaluator SeStringEvaluator { get; private set; } 33 | 34 | public static Plugin Plugin { get; private set; } 35 | public static Configuration Configuration => Plugin.Configuration; 36 | public static IconManager IconManager => Plugin.IconManager; 37 | public static WindowSystem WindowSystem => Plugin.WindowSystem; 38 | public static CommunityMacros CommunityMacros => Plugin.CommunityMacros; 39 | public static Ipc Ipc => Plugin.Ipc; 40 | #pragma warning restore CS8618 41 | 42 | internal static void Initialize(Plugin plugin, IDalamudPluginInterface iface) 43 | { 44 | Plugin = plugin; 45 | iface.Create(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Craftimizer/Utils/AttributeCommandManager.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Plugin; 2 | using Dalamud.Game.Command; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Reflection; 6 | 7 | namespace Craftimizer.Utils; 8 | 9 | [AttributeUsage(AttributeTargets.Method)] 10 | public sealed class CommandAttribute(string name, string description, bool hidden = false, params string[] aliases) : Attribute 11 | { 12 | public string Name { get; } = name; 13 | public string Description { get; } = description; 14 | public bool Hidden { get; } = hidden; 15 | public string[] Aliases { get; } = aliases; 16 | } 17 | 18 | public sealed class AttributeCommandManager : IDisposable 19 | { 20 | private HashSet RegisteredCommands { get; } = []; 21 | 22 | public AttributeCommandManager() 23 | { 24 | var target = Service.Plugin; 25 | foreach (var method in target.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) 26 | { 27 | if (method.GetCustomAttribute() is not { } command) 28 | continue; 29 | 30 | var takesParams = method.GetParameters().Length != 0; 31 | 32 | IReadOnlyCommandInfo.HandlerDelegate handler; 33 | if (takesParams) 34 | handler = method.CreateDelegate(target); 35 | else 36 | { 37 | var invoker = method.CreateDelegate(target); 38 | handler = (_, _) => invoker(); 39 | } 40 | 41 | var info = new CommandInfo(handler) 42 | { 43 | HelpMessage = command.Description, 44 | ShowInHelp = !command.Hidden, 45 | }; 46 | 47 | var aliasInfo = new CommandInfo(handler) 48 | { 49 | HelpMessage = $"An alias for {command.Name}", 50 | ShowInHelp = !command.Hidden, 51 | }; 52 | 53 | if (!RegisteredCommands.Add(command.Name)) 54 | throw new InvalidOperationException($"Command '{command.Name}' is already registered."); 55 | 56 | if (!Service.CommandManager.AddHandler(command.Name, info)) 57 | throw new InvalidOperationException($"Failed to register command '{command.Name}'."); 58 | 59 | foreach (var alias in command.Aliases) 60 | { 61 | if (!RegisteredCommands.Add(alias)) 62 | throw new InvalidOperationException($"Command '{alias}' is already registered."); 63 | 64 | if (!Service.CommandManager.AddHandler(alias, aliasInfo)) 65 | throw new InvalidOperationException($"Failed to register command '{alias}'."); 66 | } 67 | } 68 | } 69 | 70 | public void Dispose() 71 | { 72 | foreach (var command in RegisteredCommands) 73 | Service.CommandManager.RemoveHandler(command); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Craftimizer/Utils/BackgroundTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Craftimizer.Utils; 6 | 7 | public sealed class BackgroundTask(Func func) : IDisposable where T : struct 8 | { 9 | public T? Result { get; private set; } 10 | public Exception? Exception { get; private set; } 11 | public bool Completed { get; private set; } 12 | public bool Cancelling => !Completed && TokenSource.IsCancellationRequested; 13 | 14 | private CancellationTokenSource TokenSource { get; } = new(); 15 | private Func Func { get; } = func; 16 | 17 | public void Start() 18 | { 19 | var token = TokenSource.Token; 20 | var task = Task.Run(() => Result = Func(token), token); 21 | _ = task.ContinueWith(t => Completed = true); 22 | _ = task.ContinueWith(t => 23 | { 24 | if (token.IsCancellationRequested) 25 | return; 26 | 27 | try 28 | { 29 | t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException); 30 | } 31 | catch (AggregateException e) 32 | { 33 | Exception = e; 34 | Log.Error(e, "Background task failed"); 35 | } 36 | }, TaskContinuationOptions.OnlyOnFaulted); 37 | } 38 | 39 | public void Cancel() => 40 | TokenSource.Cancel(); 41 | 42 | public void Dispose() => 43 | Cancel(); 44 | } 45 | -------------------------------------------------------------------------------- /Craftimizer/Utils/CSCraftEventHandler.cs: -------------------------------------------------------------------------------- 1 | using FFXIVClientStructs.FFXIV.Client.Game.Event; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Craftimizer.Utils; 5 | 6 | [StructLayout(LayoutKind.Explicit)] 7 | public unsafe struct CSCraftEventHandler 8 | { 9 | [FieldOffset(0x48A)] public unsafe fixed ushort WKSClassLevels[2]; 10 | [FieldOffset(0x48E)] public unsafe fixed byte WKSClassJobs[2]; 11 | 12 | public static CSCraftEventHandler* Instance() 13 | { 14 | return (CSCraftEventHandler*)EventFramework.Instance()->GetCraftEventHandler(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Craftimizer/Utils/CSRecipeNote.cs: -------------------------------------------------------------------------------- 1 | using FFXIVClientStructs.FFXIV.Client.Game.UI; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Craftimizer.Utils; 5 | 6 | [StructLayout(LayoutKind.Explicit, Size = 2880)] 7 | public unsafe struct CSRecipeNote 8 | { 9 | [FieldOffset(0x118)] public ushort ActiveCraftRecipeId; 10 | 11 | public static CSRecipeNote* Instance() 12 | { 13 | return (CSRecipeNote*)RecipeNote.Instance(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Craftimizer/Utils/Chat.cs: -------------------------------------------------------------------------------- 1 | using FFXIVClientStructs.FFXIV.Client.System.String; 2 | using FFXIVClientStructs.FFXIV.Client.UI; 3 | using System; 4 | 5 | namespace Craftimizer.Utils; 6 | 7 | // https://github.com/Caraxi/SimpleTweaksPlugin/blob/0973b93931cdf8a1b01153984d62f76d998747ff/Utility/ChatHelper.cs#L17 8 | public static unsafe class Chat 9 | { 10 | public static void SendMessage(string message) 11 | { 12 | ArgumentException.ThrowIfNullOrWhiteSpace(message); 13 | 14 | var str = Utf8String.FromString(message); 15 | try 16 | { 17 | ArgumentOutOfRangeException.ThrowIfZero(str->Length, nameof(message)); 18 | ArgumentOutOfRangeException.ThrowIfGreaterThan(str->Length, 500, nameof(message)); 19 | 20 | var unsanitizedLength = str->Length; 21 | str->SanitizeString( 22 | AllowedEntities.Unknown9 | // 200 23 | AllowedEntities.Payloads | // 40 24 | AllowedEntities.OtherCharacters | // 20 25 | AllowedEntities.CharacterList | // 10 26 | AllowedEntities.SpecialCharacters | // 8 27 | AllowedEntities.Numbers | // 4 28 | AllowedEntities.LowercaseLetters | // 2 29 | AllowedEntities.UppercaseLetters, // 1 30 | null); 31 | ArgumentOutOfRangeException.ThrowIfNotEqual(unsanitizedLength, str->Length, nameof(message)); 32 | 33 | UIModule.Instance()->ProcessChatBoxEntry(str); 34 | } 35 | finally 36 | { 37 | str->Dtor(true); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Craftimizer/Utils/Colors.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Plugin; 2 | using Dalamud.Interface.Colors; 3 | using ImGuiNET; 4 | using System; 5 | using System.Numerics; 6 | 7 | namespace Craftimizer.Utils; 8 | 9 | public static class Colors 10 | { 11 | public static readonly Vector4 Progress = new(0.44f, 0.65f, 0.18f, 1f); 12 | public static readonly Vector4 Quality = new(0.26f, 0.71f, 0.69f, 1f); 13 | public static readonly Vector4 Durability = new(0.13f, 0.52f, 0.93f, 1f); 14 | public static readonly Vector4 HQ = new(0.592f, 0.863f, 0.376f, 1f); 15 | public static readonly Vector4 Collectability = new(0.99f, 0.56f, 0.57f, 1f); 16 | public static readonly Vector4 CP = new(0.63f, 0.37f, 0.75f, 1f); 17 | 18 | private static Vector4 SolverProgressBg => ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.TableBorderLight)); 19 | private static Vector4 SolverProgressFgBland => ImGuiColors.DalamudWhite2; 20 | 21 | private static readonly Vector4[] SolverProgressFgColorful = 22 | [ 23 | new(0.87f, 0.19f, 0.30f, 1f), 24 | new(0.96f, 0.62f, 0.12f, 1f), 25 | new(0.97f, 0.84f, 0.00f, 1f), 26 | new(0.37f, 0.69f, 0.35f, 1f), 27 | new(0.21f, 0.30f, 0.98f, 1f), 28 | new(0.26f, 0.62f, 0.94f, 1f), 29 | new(0.70f, 0.49f, 0.88f, 1f), 30 | ]; 31 | 32 | private static readonly Vector4[] SolverProgressFgMonochromatic = 33 | [ 34 | new(0.33f, 0.33f, 0.33f, 1f), 35 | new(0.44f, 0.44f, 0.44f, 1f), 36 | new(0.56f, 0.56f, 0.56f, 1f), 37 | new(0.68f, 0.68f, 0.68f, 1f), 38 | new(0.81f, 0.81f, 0.81f, 1f), 39 | new(0.93f, 0.93f, 0.93f, 1f), 40 | ]; 41 | 42 | public static readonly Vector4[] CollectabilityThreshold = 43 | [ 44 | new(0.47f, 0.78f, 0.93f, 1f), // Blue 45 | new(0.99f, 0.79f, 0f, 1f), // Yellow 46 | new(0.75f, 1f, 0.75f, 1f), // Green 47 | ]; 48 | 49 | public static (Vector4 Background, Vector4 Foreground) GetSolverProgressColors(int? stageValue) 50 | { 51 | var fg = Service.Configuration.ProgressType switch 52 | { 53 | Configuration.ProgressBarType.Colorful => SolverProgressFgColorful, 54 | Configuration.ProgressBarType.Simple => SolverProgressFgMonochromatic, 55 | _ => throw new InvalidOperationException("No progress bar should be visible") 56 | }; 57 | 58 | if (stageValue is not { } stage) 59 | return (SolverProgressBg, SolverProgressFgBland); 60 | 61 | if (stage == 0) 62 | return (SolverProgressBg, fg[0]); 63 | 64 | return (fg[(stage - 1) % fg.Length], fg[stage % fg.Length]); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Craftimizer/Utils/FoodStatus.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Plugin; 2 | using Lumina.Excel.Sheets; 3 | using System.Collections.Frozen; 4 | using System.Collections.Generic; 5 | using System.Collections.Immutable; 6 | using System.Linq; 7 | 8 | namespace Craftimizer.Utils; 9 | 10 | public static class FoodStatus 11 | { 12 | private static readonly FrozenDictionary ItemFoodToItemLUT; 13 | private static readonly FrozenDictionary FoodItems; 14 | private static readonly FrozenDictionary MedicineItems; 15 | private static readonly ImmutableArray FoodOrder; 16 | private static readonly ImmutableArray MedicineOrder; 17 | 18 | public readonly record struct FoodStat(bool IsRelative, int Value, int Max, int ValueHQ, int MaxHQ); 19 | public readonly record struct Food(Item Item, FoodStat? Craftsmanship, FoodStat? Control, FoodStat? CP); 20 | 21 | static FoodStatus() 22 | { 23 | var lut = new Dictionary(); 24 | var foods = new Dictionary(); 25 | var medicines = new Dictionary(); 26 | foreach (var item in LuminaSheets.ItemSheet) 27 | { 28 | var isFood = item.ItemUICategory.RowId == 46; 29 | var isMedicine = item.ItemUICategory.RowId == 44; 30 | if (!isFood && !isMedicine) 31 | continue; 32 | 33 | if (item.ItemAction.ValueNullable is not { } itemAction) 34 | continue; 35 | 36 | if (itemAction.Type is not (844 or 845 or 846)) 37 | continue; 38 | 39 | if (LuminaSheets.ItemFoodSheet.GetRowOrDefault(itemAction.Data[1]) is not { } itemFood) 40 | continue; 41 | 42 | FoodStat? craftsmanship = null, control = null, cp = null; 43 | foreach (var stat in itemFood.Params) 44 | { 45 | if (stat.BaseParam.RowId == 0) 46 | continue; 47 | var foodStat = new FoodStat(stat.IsRelative, stat.Value, stat.Max, stat.ValueHQ, stat.MaxHQ); 48 | switch (stat.BaseParam.RowId) 49 | { 50 | case Gearsets.ParamCraftsmanship: craftsmanship = foodStat; break; 51 | case Gearsets.ParamControl: control = foodStat; break; 52 | case Gearsets.ParamCP: cp = foodStat; break; 53 | default: continue; 54 | } 55 | } 56 | 57 | if (craftsmanship != null || control != null || cp != null) 58 | { 59 | var food = new Food(item, craftsmanship, control, cp); 60 | if (isFood) 61 | foods.Add(item.RowId, food); 62 | if (isMedicine) 63 | medicines.Add(item.RowId, food); 64 | } 65 | 66 | lut.TryAdd(itemFood.RowId, item.RowId); 67 | } 68 | 69 | ItemFoodToItemLUT = lut.ToFrozenDictionary(); 70 | FoodItems = foods.ToFrozenDictionary(); 71 | MedicineItems = medicines.ToFrozenDictionary(); 72 | 73 | FoodOrder = FoodItems.OrderByDescending(a => a.Value.Item.LevelItem.RowId).Select(a => a.Key).ToImmutableArray(); 74 | MedicineOrder = MedicineItems.OrderByDescending(a => a.Value.Item.LevelItem.RowId).Select(a => a.Key).ToImmutableArray(); 75 | } 76 | 77 | public static void Initialize() { } 78 | 79 | public static IEnumerable OrderedFoods => FoodOrder.Select(id => FoodItems[id]); 80 | public static IEnumerable OrderedMedicines => MedicineOrder.Select(id => MedicineItems[id]); 81 | 82 | public static (uint ItemId, bool IsHQ)? ResolveFoodParam(ushort param) 83 | { 84 | var isHq = param > 10000; 85 | param -= 10000; 86 | 87 | if (!ItemFoodToItemLUT.TryGetValue(param, out var itemId)) 88 | return null; 89 | 90 | return (itemId, isHq); 91 | } 92 | 93 | public static Food? TryGetFood(uint itemId) 94 | { 95 | if (FoodItems.TryGetValue(itemId, out var food)) 96 | return food; 97 | if (MedicineItems.TryGetValue(itemId, out food)) 98 | return food; 99 | return null; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Craftimizer/Utils/FuzzyMatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | 5 | namespace Craftimizer.Utils; 6 | 7 | internal readonly struct FuzzyMatcher 8 | { 9 | private const bool IsBorderMatching = true; 10 | private static readonly (int, int)[] EmptySegArray = []; 11 | 12 | private readonly string needleString = string.Empty; 13 | private readonly int needleFinalPosition = -1; 14 | private readonly (int Start, int End)[] needleSegments = EmptySegArray; 15 | private readonly MatchMode mode = MatchMode.Simple; 16 | 17 | public FuzzyMatcher(string term, MatchMode matchMode) 18 | { 19 | needleString = term; 20 | needleFinalPosition = needleString.Length - 1; 21 | mode = matchMode; 22 | 23 | needleSegments = matchMode switch 24 | { 25 | MatchMode.FuzzyParts => FindNeedleSegments(needleString), 26 | MatchMode.Fuzzy or MatchMode.Simple => EmptySegArray, 27 | _ => throw new ArgumentOutOfRangeException(nameof(matchMode), matchMode, "Invalid match mode"), 28 | }; 29 | } 30 | 31 | private static (int Start, int End)[] FindNeedleSegments(ReadOnlySpan span) 32 | { 33 | var segments = new List<(int, int)>(); 34 | var wordStart = -1; 35 | 36 | for (var i = 0; i < span.Length; i++) 37 | { 38 | if (span[i] is not ' ' and not '\u3000') 39 | { 40 | if (wordStart < 0) 41 | wordStart = i; 42 | } 43 | else if (wordStart >= 0) 44 | { 45 | segments.Add((wordStart, i - 1)); 46 | wordStart = -1; 47 | } 48 | } 49 | 50 | if (wordStart >= 0) 51 | segments.Add((wordStart, span.Length - 1)); 52 | 53 | return [.. segments]; 54 | } 55 | 56 | public int Matches(string value) 57 | { 58 | if (needleFinalPosition < 0) 59 | return 0; 60 | 61 | if (mode == MatchMode.Simple) 62 | return value.Contains(needleString, StringComparison.InvariantCultureIgnoreCase) ? 1 : 0; 63 | 64 | if (mode == MatchMode.Fuzzy) 65 | return GetRawScore(value, 0, needleFinalPosition); 66 | 67 | if (mode == MatchMode.FuzzyParts) 68 | { 69 | if (needleSegments.Length < 2) 70 | return GetRawScore(value, 0, needleFinalPosition); 71 | 72 | var total = 0; 73 | for (var i = 0; i < needleSegments.Length; i++) 74 | { 75 | var (start, end) = needleSegments[i]; 76 | var cur = GetRawScore(value, start, end); 77 | if (cur == 0) 78 | return 0; 79 | 80 | total += cur; 81 | } 82 | 83 | return total; 84 | } 85 | 86 | return 8; 87 | } 88 | 89 | public int MatchesAny(params string[] values) 90 | { 91 | var max = 0; 92 | for (var i = 0; i < values.Length; i++) 93 | { 94 | var cur = Matches(values[i]); 95 | if (cur > max) 96 | max = cur; 97 | } 98 | 99 | return max; 100 | } 101 | 102 | private int GetRawScore(ReadOnlySpan haystack, int needleStart, int needleEnd) 103 | { 104 | var (startPos, gaps, consecutive, borderMatches, endPos) = FindForward(haystack, needleStart, needleEnd); 105 | if (startPos < 0) 106 | return 0; 107 | 108 | var needleSize = needleEnd - needleStart + 1; 109 | 110 | var score = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches); 111 | 112 | (startPos, gaps, consecutive, borderMatches) = FindReverse(haystack, endPos, needleStart, needleEnd); 113 | var revScore = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches); 114 | 115 | return int.Max(score, revScore); 116 | } 117 | 118 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 119 | private static int CalculateRawScore(int needleSize, int startPos, int gaps, int consecutive, int borderMatches) 120 | { 121 | var score = 100 122 | + needleSize * 3 123 | + borderMatches * 3 124 | + consecutive * 5 125 | - startPos 126 | - gaps * 2; 127 | if (startPos == 0) 128 | score += 5; 129 | return score < 1 ? 1 : score; 130 | } 131 | 132 | private (int StartPos, int Gaps, int Consecutive, int BorderMatches, int HaystackIndex) FindForward( 133 | ReadOnlySpan haystack, int needleStart, int needleEnd) 134 | { 135 | var needleIndex = needleStart; 136 | var lastMatchIndex = -10; 137 | 138 | var startPos = 0; 139 | var gaps = 0; 140 | var consecutive = 0; 141 | var borderMatches = 0; 142 | 143 | for (var haystackIndex = 0; haystackIndex < haystack.Length; haystackIndex++) 144 | { 145 | if (haystack[haystackIndex] == needleString[needleIndex]) 146 | { 147 | if (IsBorderMatching) 148 | { 149 | if (haystackIndex > 0) 150 | { 151 | if (!char.IsLetterOrDigit(haystack[haystackIndex - 1])) 152 | borderMatches++; 153 | } 154 | } 155 | 156 | needleIndex++; 157 | 158 | if (haystackIndex == lastMatchIndex + 1) 159 | consecutive++; 160 | 161 | if (needleIndex > needleEnd) 162 | return (startPos, gaps, consecutive, borderMatches, haystackIndex); 163 | 164 | lastMatchIndex = haystackIndex; 165 | } 166 | else 167 | { 168 | if (needleIndex > needleStart) 169 | gaps++; 170 | else 171 | startPos++; 172 | } 173 | } 174 | 175 | return (-1, 0, 0, 0, 0); 176 | } 177 | 178 | private (int StartPos, int Gaps, int Consecutive, int BorderMatches) FindReverse( 179 | ReadOnlySpan haystack, int haystackLastMatchIndex, int needleStart, int needleEnd) 180 | { 181 | var needleIndex = needleEnd; 182 | var revLastMatchIndex = haystack.Length + 10; 183 | 184 | var gaps = 0; 185 | var consecutive = 0; 186 | var borderMatches = 0; 187 | 188 | for (var haystackIndex = haystackLastMatchIndex; haystackIndex >= 0; haystackIndex--) 189 | { 190 | if (haystack[haystackIndex] == needleString[needleIndex]) 191 | { 192 | if (IsBorderMatching) 193 | { 194 | if (haystackIndex > 0) 195 | { 196 | if (!char.IsLetterOrDigit(haystack[haystackIndex - 1])) 197 | borderMatches++; 198 | } 199 | } 200 | 201 | needleIndex--; 202 | 203 | if (haystackIndex == revLastMatchIndex - 1) 204 | consecutive++; 205 | 206 | if (needleIndex < needleStart) 207 | return (haystackIndex, gaps, consecutive, borderMatches); 208 | 209 | revLastMatchIndex = haystackIndex; 210 | } 211 | else 212 | gaps++; 213 | } 214 | 215 | return (-1, 0, 0, 0); 216 | } 217 | } 218 | 219 | internal enum MatchMode 220 | { 221 | Simple, 222 | Fuzzy, 223 | FuzzyParts, 224 | } 225 | -------------------------------------------------------------------------------- /Craftimizer/Utils/Gearsets.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Simulator; 2 | using FFXIVClientStructs.FFXIV.Client.Game; 3 | using FFXIVClientStructs.FFXIV.Client.Game.UI; 4 | using FFXIVClientStructs.FFXIV.Client.UI.Misc; 5 | using Lumina.Excel.Sheets; 6 | using System; 7 | using System.Linq; 8 | using Craftimizer.Plugin; 9 | 10 | namespace Craftimizer.Utils; 11 | 12 | public static unsafe class Gearsets 13 | { 14 | public record struct GearsetStats(int CP, int Craftsmanship, int Control); 15 | public record struct GearsetMateria(ushort Type, ushort Grade); 16 | public record struct GearsetItem(uint ItemId, bool IsHq, GearsetMateria[] Materia); 17 | 18 | private static readonly GearsetStats BaseStats = new(180, 0, 0); 19 | 20 | public const int ParamCP = 11; 21 | public const int ParamCraftsmanship = 70; 22 | public const int ParamControl = 71; 23 | 24 | public static GearsetItem[] GetGearsetItems(InventoryContainer* container) 25 | { 26 | var items = new GearsetItem[(int)container->Size]; 27 | for (var i = 0; i < container->Size; ++i) 28 | { 29 | var item = container->Items[i]; 30 | items[i] = new(item.ItemId, item.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality), GetMaterias(item.Materia, item.MateriaGrades)); 31 | } 32 | return items; 33 | } 34 | 35 | public static GearsetItem[] GetGearsetItems(RaptureGearsetModule.GearsetEntry* entry) 36 | { 37 | var gearsetItems = entry->Items; 38 | var items = new GearsetItem[14]; 39 | for (var i = 0; i < 14; ++i) 40 | { 41 | var item = gearsetItems[i]; 42 | items[i] = new(item.ItemId % 1000000, item.ItemId > 1000000, GetMaterias(item.Materia, item.MateriaGrades)); 43 | } 44 | return items; 45 | } 46 | 47 | public static GearsetStats CalculateGearsetItemStats(GearsetItem gearsetItem) 48 | { 49 | var item = LuminaSheets.ItemSheet.GetRow(gearsetItem.ItemId)!; 50 | 51 | int cp = 0, craftsmanship = 0, control = 0; 52 | 53 | void IncreaseStat(uint baseParam, int amount) 54 | { 55 | if (baseParam == ParamCP) 56 | cp += amount; 57 | else if (baseParam == ParamCraftsmanship) 58 | craftsmanship += amount; 59 | else if (baseParam == ParamControl) 60 | control += amount; 61 | } 62 | 63 | foreach (var statIncrease in item.BaseParam.Zip(item.BaseParamValue)) 64 | IncreaseStat(statIncrease.First.RowId, statIncrease.Second); 65 | if (gearsetItem.IsHq) 66 | foreach (var statIncrease in item.BaseParamSpecial.Zip(item.BaseParamValueSpecial)) 67 | IncreaseStat(statIncrease.First.RowId, statIncrease.Second); 68 | 69 | foreach (var gearsetMateria in gearsetItem.Materia) 70 | { 71 | if (gearsetMateria.Type == 0) 72 | continue; 73 | 74 | var materia = LuminaSheets.MateriaSheet.GetRow(gearsetMateria.Type)!; 75 | IncreaseStat(materia.BaseParam.RowId, materia.Value[gearsetMateria.Grade]); 76 | } 77 | 78 | cp = Math.Min(cp, CalculateParamCap(item, ParamCP)); 79 | craftsmanship = Math.Min(craftsmanship, CalculateParamCap(item, ParamCraftsmanship)); 80 | control = Math.Min(control, CalculateParamCap(item, ParamControl)); 81 | 82 | return new(cp, craftsmanship, control); 83 | } 84 | 85 | public static GearsetStats CalculateGearsetStats(GearsetItem[] gearsetItems) => 86 | gearsetItems.Select(CalculateGearsetItemStats).Aggregate(BaseStats, (a, b) => new(a.CP + b.CP, a.Craftsmanship + b.Craftsmanship, a.Control + b.Control)); 87 | 88 | public static GearsetStats CalculateGearsetCurrentStats() 89 | { 90 | var attributes = UIState.Instance()->PlayerState.Attributes; 91 | 92 | return new() 93 | { 94 | CP = attributes[ParamCP], 95 | Craftsmanship = attributes[ParamCraftsmanship], 96 | Control = attributes[ParamControl], 97 | }; 98 | } 99 | 100 | public static CharacterStats CalculateCharacterStats(GearsetItem[] gearsetItems, int characterLevel, bool canUseManipulation) => 101 | CalculateCharacterStats(CalculateGearsetStats(gearsetItems), gearsetItems, characterLevel, canUseManipulation); 102 | 103 | public static CharacterStats CalculateCharacterStats(GearsetStats gearsetStats, GearsetItem[] gearsetItems, int characterLevel, bool canUseManipulation) => 104 | new() 105 | { 106 | CP = gearsetStats.CP, 107 | Craftsmanship = gearsetStats.Craftsmanship, 108 | Control = gearsetStats.Control, 109 | Level = characterLevel, 110 | CanUseManipulation = canUseManipulation, 111 | HasSplendorousBuff = gearsetItems.Any(IsSplendorousTool), 112 | IsSpecialist = gearsetItems.Any(IsSpecialistSoulCrystal), 113 | }; 114 | 115 | public static bool HasDelineations() => 116 | InventoryManager.Instance()->GetInventoryItemCount(28724) > 0; 117 | 118 | public static bool IsItem(GearsetItem item, uint itemId) => 119 | item.ItemId == itemId; 120 | 121 | public static bool IsSpecialistSoulCrystal(GearsetItem item) 122 | { 123 | if (item.ItemId == 0) 124 | return false; 125 | 126 | var luminaItem = LuminaSheets.ItemSheet.GetRow(item.ItemId)!; 127 | // Soul Crystal ItemUICategory DoH Category 128 | return luminaItem.ItemUICategory.RowId == 62 && luminaItem.ClassJobUse.Value.ClassJobCategory.RowId == 33; 129 | } 130 | 131 | public static bool IsSplendorousTool(GearsetItem item) => 132 | LuminaSheets.ItemSheetEnglish.GetRow(item.ItemId).Description.ExtractText().Contains("Increases to quality are 1.75 times higher than normal when material condition is Good.", StringComparison.Ordinal); 133 | 134 | // https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/24d0db2d9676f264edf53651b21005305267c84c/apps/client/src/app/modules/gearsets/materia.service.ts#L265 135 | private static int CalculateParamCap(Item item, uint paramId) 136 | { 137 | var ilvl = item.LevelItem.Value; 138 | var param = LuminaSheets.BaseParamSheet.GetRow(paramId)!; 139 | 140 | var baseValue = paramId switch 141 | { 142 | ParamCP => ilvl.CP, 143 | ParamCraftsmanship => ilvl.Craftsmanship, 144 | ParamControl => ilvl.Control, 145 | _ => 0 146 | }; 147 | // https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/24d0db2d9676f264edf53651b21005305267c84c/apps/data-extraction/src/extractors/items.extractor.ts#L6 148 | var slotMod = item.EquipSlotCategory.RowId switch 149 | { 150 | 1 => param.OneHandWeaponPercent, // column 4 151 | 2 => param.OffHandPercent, // column 5 152 | 3 => param.HeadPercent, // ... 153 | 4 => param.ChestPercent, 154 | 5 => param.HandsPercent, 155 | 6 => param.WaistPercent, 156 | 7 => param.LegsPercent, 157 | 8 => param.FeetPercent, 158 | 9 => param.EarringPercent, 159 | 10 => param.NecklacePercent, 160 | 11 => param.BraceletPercent, 161 | 12 => param.RingPercent, 162 | 13 => param.TwoHandWeaponPercent, 163 | 14 => param.OneHandWeaponPercent, 164 | 15 => param.ChestHeadPercent, 165 | 16 => param.ChestHeadLegsFeetPercent, 166 | 17 => 0, 167 | 18 => param.LegsFeetPercent, 168 | 19 => param.HeadChestHandsLegsFeetPercent, 169 | 20 => param.ChestLegsGlovesPercent, 170 | 21 => param.ChestLegsFeetPercent, 171 | _ => 0 172 | }; 173 | var roleMod = param.MeldParam[item.BaseParamModifier]; 174 | 175 | // https://github.com/Caraxi/SimpleTweaksPlugin/pull/595 176 | var cap = (int)Math.Round((float)baseValue * slotMod / (roleMod * 10f), MidpointRounding.AwayFromZero); 177 | return cap == 0 ? int.MaxValue : cap; 178 | } 179 | 180 | private static GearsetMateria[] GetMaterias(ReadOnlySpan types, ReadOnlySpan grades) 181 | { 182 | var materia = new GearsetMateria[5]; 183 | for (var i = 0; i < 5; ++i) 184 | materia[i] = new(types[i], grades[i]); 185 | return materia; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /Craftimizer/Utils/Hooks.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Plugin; 2 | using Dalamud.Hooking; 3 | using FFXIVClientStructs.FFXIV.Client.Game; 4 | using System; 5 | using ActionType = Craftimizer.Simulator.Actions.ActionType; 6 | using ActionUtils = Craftimizer.Plugin.ActionUtils; 7 | using CSActionType = FFXIVClientStructs.FFXIV.Client.Game.ActionType; 8 | 9 | namespace Craftimizer.Utils; 10 | 11 | public sealed unsafe class Hooks : IDisposable 12 | { 13 | public delegate void OnActionUsedDelegate(ActionType action); 14 | 15 | public event OnActionUsedDelegate? OnActionUsed; 16 | 17 | public delegate bool UseActionDelegate(ActionManager* manager, CSActionType actionType, uint actionId, ulong targetId, uint extraParam, ActionManager.UseActionMode mode, uint comboRouteId, bool* outOptAreaTargeted); 18 | 19 | public readonly Hook UseActionHook = null!; 20 | 21 | public delegate byte IsActionHighlightedDelegate(ActionManager* manager, CSActionType actionType, uint actionId); 22 | 23 | public readonly Hook IsActionHighlightedHook = null!; 24 | 25 | public Hooks() 26 | { 27 | UseActionHook = Service.GameInteropProvider.HookFromAddress((nint)ActionManager.MemberFunctionPointers.UseAction, UseActionDetour); 28 | IsActionHighlightedHook = Service.GameInteropProvider.HookFromAddress((nint)ActionManager.MemberFunctionPointers.IsActionHighlighted, IsActionHighlightedDetour); 29 | 30 | UseActionHook.Enable(); 31 | IsActionHighlightedHook.Enable(); 32 | } 33 | 34 | private bool UseActionDetour(ActionManager* manager, CSActionType actionType, uint actionId, ulong targetId, uint extraParam, ActionManager.UseActionMode mode, uint comboRouteId, bool* optOutAreaTargeted) 35 | { 36 | var canCast = manager->GetActionStatus(actionType, actionId) == 0; 37 | var ret = UseActionHook.Original(manager, actionType, actionId, targetId, extraParam, mode, comboRouteId, optOutAreaTargeted); 38 | if (canCast && ret && actionType is CSActionType.CraftAction or CSActionType.Action) 39 | { 40 | var classJob = ClassJobUtils.GetClassJobFromIdx((byte)(Service.ClientState.LocalPlayer?.ClassJob.RowId ?? 0)); 41 | if (classJob != null) 42 | { 43 | var simActionType = ActionUtils.GetActionTypeFromId(actionId, classJob.Value, actionType == CSActionType.CraftAction); 44 | if (simActionType != null) 45 | { 46 | try 47 | { 48 | OnActionUsed?.Invoke(simActionType.Value); 49 | } 50 | catch (Exception e) 51 | { 52 | Log.Error(e, "Failed to invoke OnActionUsed"); 53 | } 54 | } 55 | } 56 | } 57 | return ret; 58 | } 59 | 60 | private byte IsActionHighlightedDetour(ActionManager* manager, CSActionType actionType, uint actionId) 61 | { 62 | var ret = IsActionHighlightedHook.Original(manager, actionType, actionId); 63 | 64 | try 65 | { 66 | if (!Service.Configuration.SynthHelperAbilityAnts) 67 | return ret; 68 | 69 | if (Service.Plugin.SynthHelperWindow is not { } window) 70 | return ret; 71 | 72 | if (!window.ShouldDrawAnts) 73 | return ret; 74 | 75 | if (actionType is not (CSActionType.CraftAction or CSActionType.Action)) 76 | return ret; 77 | 78 | var jobId = Service.ClientState.LocalPlayer?.ClassJob.RowId; 79 | if (jobId == null) 80 | return ret; 81 | 82 | var classJob = ClassJobUtils.GetClassJobFromIdx((byte)jobId.Value); 83 | if (classJob == null) 84 | return ret; 85 | 86 | var simActionType = ActionUtils.GetActionTypeFromId(actionId, classJob.Value, actionType == CSActionType.CraftAction); 87 | if (simActionType == null) 88 | return ret; 89 | 90 | if (window.NextAction != simActionType) 91 | return 0; 92 | } 93 | catch (Exception ex) 94 | { 95 | Log.Error(ex, "Failed to check if action should be highlighted"); 96 | return ret; 97 | } 98 | 99 | return 1; 100 | } 101 | 102 | public void Dispose() 103 | { 104 | UseActionHook.Dispose(); 105 | IsActionHighlightedHook.Dispose(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Craftimizer/Utils/IconManager.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Plugin; 2 | using Dalamud.Interface.Textures; 3 | using Dalamud.Interface.Textures.TextureWraps; 4 | using Dalamud.Utility; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Numerics; 8 | using System.Reflection; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace Craftimizer.Utils; 13 | 14 | public interface ITextureIcon 15 | { 16 | ISharedImmediateTexture Source { get; } 17 | 18 | Vector2? Dimensions { get; } 19 | 20 | float? AspectRatio => Dimensions is { } d ? d.X / d.Y : null; 21 | 22 | nint ImGuiHandle { get; } 23 | } 24 | 25 | public interface ILoadedTextureIcon : ITextureIcon, IDisposable { } 26 | 27 | public sealed class IconManager : IDisposable 28 | { 29 | private sealed class LoadedIcon : ILoadedTextureIcon 30 | { 31 | public ISharedImmediateTexture Source { get; } 32 | 33 | public Vector2? Dimensions => GetWrap()?.Size; 34 | 35 | public nint ImGuiHandle => GetWrapOrEmpty().ImGuiHandle; 36 | 37 | private Task TextureWrapTask { get; } 38 | private CancellationTokenSource DisposeToken { get; } 39 | 40 | public LoadedIcon(ISharedImmediateTexture source) 41 | { 42 | Source = source; 43 | DisposeToken = new(); 44 | TextureWrapTask = source.RentAsync(DisposeToken.Token); 45 | } 46 | 47 | public IDalamudTextureWrap? GetWrap() 48 | { 49 | if (TextureWrapTask.IsCompletedSuccessfully) 50 | return TextureWrapTask.Result; 51 | return null; 52 | } 53 | 54 | public IDalamudTextureWrap GetWrapOrEmpty() => GetWrap() ?? Service.DalamudAssetManager.Empty4X4; 55 | 56 | public void Dispose() 57 | { 58 | DisposeToken.Cancel(); 59 | TextureWrapTask.ToContentDisposedTask(true).Wait(); 60 | } 61 | } 62 | 63 | // TODO: Unload when unused, but with a custom timer? 64 | private sealed class CachedIcon(ISharedImmediateTexture source) : ITextureIcon 65 | { 66 | private LoadedIcon Base { get; } = new(source); 67 | 68 | public ISharedImmediateTexture Source => Base.Source; 69 | 70 | public Vector2? Dimensions => Base.Dimensions; 71 | 72 | public nint ImGuiHandle => Base.ImGuiHandle; 73 | 74 | public void Release() 75 | { 76 | Base.Dispose(); 77 | } 78 | } 79 | 80 | private Dictionary<(uint Id, bool IsHq), CachedIcon> IconCache { get; } = []; 81 | private Dictionary AssemblyTextureCache { get; } = []; 82 | 83 | private static ISharedImmediateTexture GetIconInternal(uint id, bool isHq = false) => 84 | Service.TextureProvider.GetFromGameIcon(new GameIconLookup(id, itemHq: isHq)); 85 | 86 | private static ISharedImmediateTexture GetAssemblyTextureInternal(string filename) => 87 | Service.TextureProvider.GetFromManifestResource(Assembly.GetExecutingAssembly(), $"Craftimizer.{filename}"); 88 | 89 | public static ILoadedTextureIcon GetIcon(uint id, bool isHq = false) => 90 | new LoadedIcon(GetIconInternal(id, isHq)); 91 | 92 | public static ILoadedTextureIcon GetAssemblyTexture(string filename) => 93 | new LoadedIcon(GetAssemblyTextureInternal(filename)); 94 | 95 | public ITextureIcon GetIconCached(uint id, bool isHq = false) 96 | { 97 | if (IconCache.TryGetValue((id, isHq), out var icon)) 98 | return icon; 99 | return IconCache[(id, isHq)] = new(GetIconInternal(id, isHq)); 100 | } 101 | 102 | public ITextureIcon GetAssemblyTextureCached(string filename) 103 | { 104 | if (AssemblyTextureCache.TryGetValue(filename, out var texture)) 105 | return texture; 106 | return AssemblyTextureCache[filename] = new(GetAssemblyTextureInternal(filename)); 107 | } 108 | 109 | public void Dispose() 110 | { 111 | foreach (var value in IconCache.Values) 112 | value.Release(); 113 | foreach (var value in AssemblyTextureCache.Values) 114 | value.Release(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Craftimizer/Utils/Ipc.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Plugin; 2 | using Dalamud.Plugin; 3 | using System; 4 | using System.Reflection; 5 | using System.Runtime.CompilerServices; 6 | using DotNext.Reflection; 7 | using DotNext.Collections.Generic; 8 | 9 | namespace Craftimizer.Utils; 10 | 11 | public sealed class Ipc 12 | { 13 | [AttributeUsage(AttributeTargets.Property)] 14 | private sealed class IPCCallAttribute(string? name) : Attribute 15 | { 16 | public string? Name { get; } = name; 17 | } 18 | 19 | public Ipc() 20 | { 21 | foreach (var prop in typeof(Ipc).GetProperties(BindingFlags.Instance | BindingFlags.Public)) 22 | { 23 | if (prop.GetCustomAttribute() is not { } attr) 24 | continue; 25 | 26 | if (prop.GetMethod is not { } getMethod) 27 | throw new InvalidOperationException("Property must have a getter"); 28 | 29 | if (getMethod.GetCustomAttribute() is null) 30 | throw new InvalidOperationException("Property must have an auto getter"); 31 | 32 | var type = prop.PropertyType; 33 | 34 | if (!typeof(Delegate).IsAssignableFrom(type)) 35 | throw new InvalidOperationException("Property type must be a delegate"); 36 | 37 | if (type.GetMethod("Invoke") is not { } typeMethod) 38 | throw new InvalidOperationException("Delegate type has no Invoke"); 39 | 40 | var returnsVoid = typeMethod.ReturnType == typeof(void); 41 | 42 | var propSubscriber = typeof(IDalamudPluginInterface).GetMethod("GetIpcSubscriber", typeMethod.GetParameters().Length + 1, [typeof(string)]); 43 | if (propSubscriber is null) 44 | throw new InvalidOperationException("GetIpcSubscriber method not found"); 45 | 46 | var callGateSubscriber = propSubscriber.MakeGenericMethod([.. typeMethod.GetParameterTypes(), returnsVoid ? typeof(int) : typeMethod.ReturnType]).Invoke(Service.PluginInterface, [attr.Name ?? prop.Name]); 47 | 48 | if (callGateSubscriber is null) 49 | throw new InvalidOperationException("CallGateSubscriber is null"); 50 | 51 | var invokeFunc = callGateSubscriber.GetType().GetMethod(returnsVoid ? "InvokeAction" : "InvokeFunc"); 52 | if (invokeFunc is null) 53 | throw new InvalidOperationException("Subscriber Invoke method not found"); 54 | 55 | prop.SetValue(this, Delegate.CreateDelegate(type, callGateSubscriber, invokeFunc)); 56 | 57 | Log.Debug($"Bound {prop.Name} IPC to {type}"); 58 | } 59 | } 60 | 61 | [IPCCall("MacroMate.IsAvailable")] 62 | public Func MacroMateIsAvailable { get; private set; } = null!; 63 | 64 | [IPCCall("MacroMate.CreateOrUpdateMacro")] 65 | public Func MacroMateCreateMacro { get; private set; } = null!; 66 | 67 | [IPCCall("MacroMate.ValidateGroupPath")] 68 | public Func MacroMateValidateGroupPath { get; private set; } = null!; 69 | } 70 | -------------------------------------------------------------------------------- /Craftimizer/Utils/Log.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Plugin; 2 | using System; 3 | 4 | namespace Craftimizer.Utils; 5 | 6 | public static class Log 7 | { 8 | public static void Debug(string line) => Service.PluginLog.Debug(line); 9 | 10 | public static void Error(string line) => Service.PluginLog.Error(line); 11 | public static void Error(Exception e, string line) => Service.PluginLog.Error(e, line); 12 | } 13 | -------------------------------------------------------------------------------- /Craftimizer/Utils/MacroImport.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Plugin; 2 | using Craftimizer.Simulator; 3 | using Craftimizer.Simulator.Actions; 4 | using Dalamud.Networking.Http; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Diagnostics; 8 | using System.Net; 9 | using System.Net.Http; 10 | using System.Net.Http.Json; 11 | using System.Text.Json; 12 | using System.Text.Json.Serialization; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | using static Craftimizer.Utils.CommunityMacros; 16 | 17 | namespace Craftimizer.Utils; 18 | 19 | public static class MacroImport 20 | { 21 | public static IReadOnlyList? TryParseMacro(string inputMacro) 22 | { 23 | var actions = new List(); 24 | foreach (var line in inputMacro.ReplaceLineEndings("\n").Split("\n")) 25 | { 26 | if (TryParseLine(line) is { } action) 27 | actions.Add(action); 28 | } 29 | return actions.Count > 0 ? actions : null; 30 | } 31 | 32 | private static ActionType? TryParseLine(string line) 33 | { 34 | if (line.StartsWith("/ac", StringComparison.OrdinalIgnoreCase)) 35 | line = line[3..]; 36 | else if (line.StartsWith("/action", StringComparison.OrdinalIgnoreCase)) 37 | line = line[7..]; 38 | else 39 | return null; 40 | 41 | line = line.TrimStart(); 42 | 43 | // get first word 44 | if (line.StartsWith('"')) 45 | { 46 | line = line[1..]; 47 | 48 | var end = line.IndexOf('"', 1); 49 | if (end != -1) 50 | line = line[..end]; 51 | } 52 | else 53 | { 54 | var end = line.IndexOf(' ', 1); 55 | if (end != -1) 56 | line = line[..end]; 57 | } 58 | 59 | foreach (var action in Enum.GetValues()) 60 | { 61 | if (line.Equals(action.GetName(ClassJob.Carpenter), StringComparison.OrdinalIgnoreCase)) 62 | return action; 63 | } 64 | return null; 65 | } 66 | 67 | public static bool TryParseUrl(string url, out Uri uri) 68 | { 69 | if (!Uri.TryCreate(url, UriKind.Absolute, out uri!)) 70 | return false; 71 | 72 | if (!string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) 73 | return false; 74 | 75 | if (!uri.IsDefaultPort) 76 | return false; 77 | 78 | return uri.DnsSafeHost is "ffxivteamcraft.com" or "craftingway.app"; 79 | } 80 | 81 | public static Task RetrieveUrl(string url, CancellationToken token) 82 | { 83 | if (!TryParseUrl(url, out var uri)) 84 | throw new ArgumentException("Unsupported url", nameof(url)); 85 | 86 | return uri.DnsSafeHost switch 87 | { 88 | "ffxivteamcraft.com" => RetrieveTeamcraftUrl(uri, token), 89 | "craftingway.app" => RetrieveCraftingwayUrl(uri, token), 90 | _ => throw new UnreachableException("TryParseUrl should handle miscellaneous edge cases"), 91 | }; 92 | } 93 | 94 | private static async Task RetrieveTeamcraftUrl(Uri uri, CancellationToken token) 95 | { 96 | using var heCallback = new HappyEyeballsCallback(); 97 | using var client = new HttpClient(new SocketsHttpHandler 98 | { 99 | AutomaticDecompression = DecompressionMethods.All, 100 | ConnectCallback = heCallback.ConnectCallback, 101 | }); 102 | 103 | var path = uri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped); 104 | if (!path.StartsWith("simulator/", StringComparison.Ordinal)) 105 | throw new ArgumentException("Teamcraft macro url should start with /simulator", nameof(uri)); 106 | path = path[10..]; 107 | 108 | var lastSlash = path.LastIndexOf('/'); 109 | if (lastSlash == -1) 110 | throw new ArgumentException("Teamcraft macro url is not in the right format", nameof(uri)); 111 | 112 | var id = path[(lastSlash + 1)..]; 113 | 114 | var resp = await client.GetFromJsonAsync( 115 | $"https://firestore.googleapis.com/v1beta1/projects/ffxivteamcraft/databases/(default)/documents/rotations/{id}", 116 | token). 117 | ConfigureAwait(false); 118 | if (resp is null) 119 | throw new Exception("Internal error; failed to retrieve macro"); 120 | if (resp.Error is { } error) 121 | throw new Exception($"Internal server error ({error.Status}); {error.Message}"); 122 | return new(resp); 123 | } 124 | 125 | private static async Task RetrieveCraftingwayUrl(Uri uri, CancellationToken token) 126 | { 127 | using var heCallback = new HappyEyeballsCallback(); 128 | using var client = new HttpClient(new SocketsHttpHandler 129 | { 130 | AutomaticDecompression = DecompressionMethods.All, 131 | ConnectCallback = heCallback.ConnectCallback, 132 | }); 133 | 134 | // https://craftingway.app/rotation/variable-blueprint-KmrvS 135 | 136 | var path = uri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped); 137 | if (!path.StartsWith("rotation/", StringComparison.Ordinal)) 138 | throw new ArgumentException("Craftingway macro url should start with /rotation", nameof(uri)); 139 | path = path[9..]; 140 | 141 | var lastSlash = path.LastIndexOf('/'); 142 | if (lastSlash != -1) 143 | throw new ArgumentException("Craftingway macro url is not in the right format", nameof(uri)); 144 | 145 | var id = path; 146 | 147 | var resp = await client.GetFromJsonAsync( 148 | $"https://servingway.fly.dev/rotation/{id}", 149 | token) 150 | .ConfigureAwait(false); 151 | if (resp is null) 152 | throw new Exception("Internal error; failed to retrieve macro"); 153 | if (resp.Error is { } error) 154 | throw new Exception($"Internal server error; {error}"); 155 | 156 | return new(resp); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /Craftimizer/Utils/ReadOnlySeStringExtensions.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Utility; 2 | using Lumina.Text.ReadOnly; 3 | 4 | namespace Craftimizer.Utils; 5 | 6 | public static class ReadOnlySeStringExtensions 7 | { 8 | public static string ExtractCleanText(this ReadOnlySeString self) 9 | { 10 | return self.ExtractText().StripSoftHyphen(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Craftimizer/Utils/RecipeData.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Plugin; 2 | using Craftimizer.Simulator; 3 | using Lumina.Excel.Sheets; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using ClassJob = Craftimizer.Simulator.ClassJob; 8 | 9 | namespace Craftimizer.Utils; 10 | 11 | public sealed record RecipeData 12 | { 13 | public ushort RecipeId { get; } 14 | 15 | public Recipe Recipe { get; } 16 | public RecipeLevelTable Table { get; } 17 | 18 | public ClassJob ClassJob { get; } 19 | public RecipeInfo RecipeInfo { get; } 20 | public bool IsCollectable => Recipe.ItemResult.ValueNullable?.AlwaysCollectable ?? false; 21 | public IReadOnlyList? CollectableThresholds { get; } 22 | public IReadOnlyList<(Item Item, int Amount)> Ingredients { get; } 23 | public int MaxStartingQuality { get; } 24 | public ushort? AdjustedJobLevel { get; } 25 | private int TotalHqILvls { get; } 26 | 27 | public RecipeData(ushort recipeId, ushort? explicitlyAdjustedJobLevel = null) 28 | { 29 | RecipeId = recipeId; 30 | 31 | Recipe = LuminaSheets.RecipeSheet.GetRowOrDefault(recipeId) ?? 32 | throw new ArgumentException($"Invalid recipe id {recipeId}", nameof(recipeId)); 33 | 34 | ClassJob = (ClassJob)Recipe.CraftType.RowId; 35 | 36 | var resolvedLevelTableRow = Recipe.RecipeLevelTable.RowId; 37 | if (Recipe.Unknown0 != 0) 38 | { 39 | AdjustedJobLevel = Math.Min(explicitlyAdjustedJobLevel ?? ClassJob.GetWKSSyncedLevel(), Recipe.Unknown0); 40 | resolvedLevelTableRow = LuminaSheets.GathererCrafterLvAdjustTableSheet.GetRow(AdjustedJobLevel.Value).Unknown0; 41 | } 42 | Table = LuminaSheets.RecipeLevelTableSheet.GetRow(resolvedLevelTableRow); 43 | 44 | RecipeInfo = new() 45 | { 46 | IsExpert = Recipe.IsExpert, 47 | ClassJobLevel = Table.ClassJobLevel, 48 | ConditionsFlag = Table.ConditionsFlag, 49 | MaxDurability = (Recipe.Unknown0 != 0 ? 80 : Table.Durability) * Recipe.DurabilityFactor / 100, 50 | MaxQuality = (Recipe.CanHq || Recipe.IsExpert) ? (int)Table.Quality * Recipe.QualityFactor / 100 : 0, 51 | MaxProgress = Table.Difficulty * Recipe.DifficultyFactor / 100, 52 | QualityModifier = Table.QualityModifier, 53 | QualityDivider = Table.QualityDivider, 54 | ProgressModifier = Table.ProgressModifier, 55 | ProgressDivider = Table.ProgressDivider, 56 | }; 57 | 58 | int[]? thresholds = null; 59 | if (Recipe.CollectableMetadata.GetValueOrDefault() is { } row) 60 | thresholds = [row.LowCollectability, row.MidCollectability, row.HighCollectability]; 61 | else if (Recipe.CollectableMetadata.GetValueOrDefault() is { } row2) 62 | { 63 | foreach (var entry in row2.HWDCrafterSupplyParams) 64 | { 65 | if (entry.ItemTradeIn.RowId == Recipe.ItemResult.RowId) 66 | { 67 | thresholds = [entry.BaseCollectableRating, entry.MidCollectableRating, entry.HighCollectableRating]; 68 | break; 69 | } 70 | } 71 | } 72 | else if (Recipe.CollectableMetadata.GetValueOrDefaultSubrow() is { } row3) 73 | { 74 | foreach (var subrow in row3) 75 | { 76 | if (subrow.Item.RowId == Recipe.ItemResult.RowId) 77 | { 78 | thresholds = [subrow.CollectabilityLow, subrow.CollectabilityMid, subrow.CollectabilityHigh]; 79 | break; 80 | } 81 | } 82 | } 83 | else if (Recipe.CollectableMetadata.GetValueOrDefault() is { } row5) 84 | { 85 | foreach (var item in row5.Item) 86 | { 87 | if (item.ItemId.RowId == Recipe.ItemResult.RowId) 88 | { 89 | thresholds = [item.CollectabilityMid, item.CollectabilityHigh]; 90 | break; 91 | } 92 | } 93 | } 94 | else if (Recipe.CollectableMetadata.GetValueOrDefault() is { } row6) 95 | thresholds = [row6.CollectabilityLow, row6.CollectabilityMid, row6.CollectabilityHigh]; 96 | else if (Recipe.CollectableMetadataKey == 7 && LuminaSheets.WKSMissionToDoEvalutionRefinSheet.TryGetRow(Recipe.CollectableMetadata.RowId, out var row7)) 97 | { 98 | thresholds = [row7.Unknown0, row7.Unknown1, row7.Unknown2]; 99 | thresholds = [.. thresholds.Select(percentage => RecipeInfo.MaxQuality * percentage / 1000)]; 100 | } 101 | 102 | if (thresholds != null) 103 | { 104 | var t = thresholds.Where(t => t != 0).Cast(); 105 | t = Enumerable.Concat(Enumerable.Repeat((int?)null, 3 - t.Count()), t); 106 | CollectableThresholds = t.ToArray(); 107 | } 108 | 109 | Ingredients = Recipe.Ingredient.Zip(Recipe.AmountIngredient) 110 | .Take(6) 111 | .Where(i => i.First.IsValid) 112 | .Select(i => (i.First.Value, (int)i.Second)) 113 | .ToList(); 114 | MaxStartingQuality = (int)Math.Floor(Recipe.MaterialQualityFactor * RecipeInfo.MaxQuality / 100f); 115 | 116 | TotalHqILvls = (int)Ingredients.Where(i => i.Item.CanBeHq).Sum(i => i.Item.LevelItem.RowId * i.Amount); 117 | } 118 | 119 | public int CalculateItemStartingQuality(int itemIdx, int amount) 120 | { 121 | ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(itemIdx, Ingredients.Count); 122 | 123 | var ingredient = Ingredients[itemIdx]; 124 | ArgumentOutOfRangeException.ThrowIfGreaterThan(amount, ingredient.Amount); 125 | 126 | if (!ingredient.Item.CanBeHq) 127 | return 0; 128 | 129 | var iLvls = ingredient.Item.LevelItem.RowId * amount; 130 | return (int)Math.Floor((float)iLvls / TotalHqILvls * MaxStartingQuality); 131 | } 132 | 133 | public int CalculateStartingQuality(IEnumerable hqQuantities) 134 | { 135 | if (TotalHqILvls == 0) 136 | return 0; 137 | 138 | var iLvls = Ingredients.Zip(hqQuantities).Sum(i => i.First.Item.LevelItem.RowId * i.Second); 139 | 140 | return (int)Math.Floor((float)iLvls / TotalHqILvls * MaxStartingQuality); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /Craftimizer/Utils/SqText.cs: -------------------------------------------------------------------------------- 1 | using Dalamud.Game.Text; 2 | using System; 3 | using System.Collections.Frozen; 4 | using System.Collections.Generic; 5 | using System.Numerics; 6 | 7 | namespace Craftimizer.Utils; 8 | 9 | public static class SqText 10 | { 11 | public static SeIconChar LevelPrefix => SeIconChar.LevelEn; 12 | 13 | public static readonly FrozenDictionary LevelNumReplacements = new Dictionary 14 | { 15 | ['0'] = SeIconChar.Number0, 16 | ['1'] = SeIconChar.Number1, 17 | ['2'] = SeIconChar.Number2, 18 | ['3'] = SeIconChar.Number3, 19 | ['4'] = SeIconChar.Number4, 20 | ['5'] = SeIconChar.Number5, 21 | ['6'] = SeIconChar.Number6, 22 | ['7'] = SeIconChar.Number7, 23 | ['8'] = SeIconChar.Number8, 24 | ['9'] = SeIconChar.Number9, 25 | }.ToFrozenDictionary(); 26 | 27 | public static string ToLevelString(T value) where T : IBinaryInteger 28 | { 29 | var str = value.ToString() ?? throw new FormatException("Failed to format value"); 30 | foreach(var (k, v) in LevelNumReplacements) 31 | str = str.Replace(k, v.ToIconChar()); 32 | return str; 33 | } 34 | 35 | public static bool TryParseLevelString(string str, out int result) 36 | { 37 | foreach(var (k, v) in LevelNumReplacements) 38 | str = str.Replace(v.ToIconChar(), k); 39 | return int.TryParse(str, out result); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Craftimizer/Utils/SynthesisValues.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Simulator; 2 | using Dalamud.Game.Text.SeStringHandling; 3 | using Dalamud.Memory; 4 | using FFXIVClientStructs.FFXIV.Client.UI; 5 | using FFXIVClientStructs.FFXIV.Component.GUI; 6 | using System; 7 | using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; 8 | 9 | namespace Craftimizer.Utils; 10 | 11 | internal sealed unsafe class SynthesisValues(AddonSynthesis* addon) 12 | { 13 | private AddonSynthesis* Addon { get; } = addon; 14 | 15 | private ReadOnlySpan Values => new(Addon->AtkUnitBase.AtkValues, Addon->AtkUnitBase.AtkValuesCount); 16 | 17 | // Always 0? 18 | private uint IsInitializing => GetUInt(0); 19 | private bool IsTrialSynthesis => TryGetBool(1) ?? false; 20 | public SeString ItemName => GetString(2); 21 | public uint ItemIconId => GetUInt(3); 22 | public uint ItemCount => GetUInt(4); 23 | public uint Progress => GetUInt(5); 24 | public uint MaxProgress => GetUInt(6); 25 | public uint Durability => GetUInt(7); 26 | public uint MaxDurability => GetUInt(8); 27 | public uint Quality => GetUInt(9); 28 | public uint HQChance => GetUInt(10); 29 | private uint IsShowingCollectibleInfoValue => GetUInt(11); 30 | private uint ConditionValue => GetUInt(12); 31 | public SeString ConditionName => GetString(13); 32 | public SeString ConditionNameAndTooltip => GetString(14); 33 | public uint StepCount => GetUInt(15); 34 | public uint ResultItemId => GetUInt(16); 35 | public uint MaxQuality => GetUInt(17); 36 | public uint RequiredQuality => GetUInt(18); 37 | private uint IsCollectibleValue => GetUInt(19); 38 | public uint Collectability => GetUInt(20); 39 | public uint MaxCollectability => GetUInt(21); 40 | public uint CollectabilityCheckpoint1 => GetUInt(22); 41 | public uint CollectabilityCheckpoint2 => GetUInt(23); 42 | public uint CollectabilityCheckpoint3 => GetUInt(24); 43 | public bool IsExpertRecipe => GetBool(25); 44 | 45 | public bool IsShowingCollectibleInfo => IsShowingCollectibleInfoValue != 0; 46 | public Condition Condition => (Condition)ConditionValue; 47 | public bool IsCollectible => IsCollectibleValue != 0; 48 | 49 | private uint? TryGetUInt(int i) 50 | { 51 | if (Addon == null) 52 | return null; 53 | var value = Values[i]; 54 | return value.Type == ValueType.UInt ? 55 | value.UInt : 56 | null; 57 | } 58 | 59 | private bool? TryGetBool(int i) 60 | { 61 | if (Addon == null) 62 | return null; 63 | var value = Values[i]; 64 | return value.Type == ValueType.Bool ? 65 | value.Byte != 0 : 66 | null; 67 | } 68 | 69 | private SeString? TryGetString(int i) 70 | { 71 | if (Addon == null) 72 | return null; 73 | var value = Values[i]; 74 | return value.Type switch 75 | { 76 | ValueType.ManagedString or 77 | ValueType.String => 78 | MemoryHelper.ReadSeStringNullTerminated((nint)value.String.Value), 79 | _ => null 80 | }; 81 | } 82 | 83 | private uint GetUInt(int i) => 84 | TryGetUInt(i) ?? throw new ArgumentException($"Value {i} is not a uint", nameof(i)); 85 | 86 | private bool GetBool(int i) => 87 | TryGetBool(i) ?? throw new ArgumentException($"Value {i} is not a boolean", nameof(i)); 88 | 89 | private SeString GetString(int i) => 90 | TryGetString(i) ?? throw new ArgumentException($"Value {i} is not a string", nameof(i)); 91 | } 92 | -------------------------------------------------------------------------------- /Craftimizer/Windows/MacroClipboard.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Plugin; 2 | using Dalamud.Interface; 3 | using Dalamud.Interface.Utility.Raii; 4 | using Dalamud.Interface.Windowing; 5 | using ImGuiNET; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Numerics; 9 | using System.Linq; 10 | using Dalamud.Interface.ImGuiNotification; 11 | 12 | namespace Craftimizer.Windows; 13 | 14 | public sealed class MacroClipboard : Window, IDisposable 15 | { 16 | private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoCollapse; 17 | 18 | private List Macros { get; } 19 | 20 | public MacroClipboard(IEnumerable macros) : base("Macro Clipboard", WindowFlags) 21 | { 22 | Macros = new(macros); 23 | 24 | IsOpen = true; 25 | AllowPinning = false; 26 | AllowClickthrough = false; 27 | BringToFront(); 28 | 29 | Service.WindowSystem.AddWindow(this); 30 | } 31 | 32 | public override void Draw() 33 | { 34 | var idx = 0; 35 | foreach(var macro in Macros) 36 | DrawMacro(idx++, macro); 37 | } 38 | 39 | private void DrawMacro(int idx, string macro) 40 | { 41 | using var id = ImRaii.PushId(idx); 42 | using var panel = ImRaii2.GroupPanel(Macros.Count == 1 ? "Macro" : $"Macro {idx + 1}", -1, out var availWidth); 43 | 44 | var cursor = ImGui.GetCursorPos(); 45 | 46 | ImGuiUtils.AlignRight(ImGui.GetFrameHeight(), availWidth); 47 | var buttonCursor = ImGui.GetCursorPos(); 48 | ImGui.InvisibleButton("##copyInvButton", new(ImGui.GetFrameHeight())); 49 | var buttonHovered = ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenOverlapped | ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); 50 | var buttonActive = buttonHovered && ImGui.GetIO().MouseDown[(int)ImGuiMouseButton.Left]; 51 | var buttonClicked = buttonHovered && ImGui.GetIO().MouseReleased[(int)ImGuiMouseButton.Left]; 52 | ImGui.SetCursorPos(buttonCursor); 53 | { 54 | using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(buttonActive ? ImGuiCol.ButtonActive : ImGuiCol.ButtonHovered), buttonHovered); 55 | ImGuiUtils.IconButtonSquare(FontAwesomeIcon.Paste); 56 | if (buttonClicked) 57 | { 58 | ImGui.SetClipboardText(macro); 59 | if (Service.Configuration.MacroCopy.ShowCopiedMessage) 60 | { 61 | Service.Plugin.DisplayNotification(new() 62 | { 63 | Content = Macros.Count == 1 ? "Copied macro to clipboard." : $"Copied macro {idx + 1} to clipboard.", 64 | MinimizedText = Macros.Count == 1 ? "Copied macro" : $"Copied macro {idx + 1}", 65 | Title = "Macro Copied", 66 | Type = NotificationType.Success 67 | }); 68 | } 69 | } 70 | } 71 | if (buttonHovered) 72 | ImGuiUtils.Tooltip("Copy to Clipboard"); 73 | 74 | ImGui.SetCursorPos(cursor); 75 | { 76 | using var font = ImRaii.PushFont(UiBuilder.MonoFont); 77 | using var padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero); 78 | using var bg = ImRaii.PushColor(ImGuiCol.FrameBg, Vector4.Zero); 79 | var lineCount = macro.Count(c => c == '\n') + 1; 80 | ImGui.InputTextMultiline("", ref macro, (uint)macro.Length + 1, new(availWidth, ImGui.GetTextLineHeight() * Math.Max(15, lineCount) + ImGui.GetStyle().FramePadding.Y), ImGuiInputTextFlags.ReadOnly | ImGuiInputTextFlags.AutoSelectAll); 81 | } 82 | 83 | if (buttonHovered) 84 | ImGui.SetMouseCursor(ImGuiMouseCursor.Arrow); 85 | } 86 | 87 | public void Dispose() 88 | { 89 | Service.WindowSystem.RemoveWindow(this); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Craftimizer/packages.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": { 4 | "net9.0-windows7.0": { 5 | "DalamudPackager": { 6 | "type": "Direct", 7 | "requested": "[12.0.0, )", 8 | "resolved": "12.0.0", 9 | "contentHash": "J5TJLV3f16T/E2H2P17ClWjtfEBPpq3yxvqW46eN36JCm6wR+EaoaYkqG9Rm5sHqs3/nK/vKjWWyvEs/jhKoXw==" 10 | }, 11 | "DotNet.ReproducibleBuilds": { 12 | "type": "Direct", 13 | "requested": "[1.2.25, )", 14 | "resolved": "1.2.25", 15 | "contentHash": "xCXiw7BCxHJ8pF6wPepRUddlh2dlQlbr81gXA72hdk4FLHkKXas7EH/n+fk5UCA/YfMqG1Z6XaPiUjDbUNBUzg==" 16 | }, 17 | "MathNet.Numerics": { 18 | "type": "Direct", 19 | "requested": "[5.0.0, )", 20 | "resolved": "5.0.0", 21 | "contentHash": "pg1W2VwaEQMAiTpGK840hZgzavnqjlCMTVSbtVCXVyT+7AX4mc1o89SPv4TBlAjhgCOo9c1Y+jZ5m3ti2YgGgA==" 22 | }, 23 | "Meziantou.Analyzer": { 24 | "type": "Direct", 25 | "requested": "[2.0.199, )", 26 | "resolved": "2.0.199", 27 | "contentHash": "y8oRrTLDBw1b10pWci/PnFoahdIMflNSlVheL9kzUylAASnoJPFyvuyaNXcrbOTNOEk1aMLFRr1mSX/xvYR15g==" 28 | }, 29 | "DotNext": { 30 | "type": "Transitive", 31 | "resolved": "5.21.0", 32 | "contentHash": "fU63OJVSDSsOl6adjNM8e5zmyhdZkX2ztvmSeW6lBjFdvFG8ZwMOrJ+L8Ih/2yKr0cuaV8PNwnhDrlS4MFf14A==", 33 | "dependencies": { 34 | "System.IO.Hashing": "8.0.0" 35 | } 36 | }, 37 | "Raphael.Net": { 38 | "type": "Transitive", 39 | "resolved": "2.1.1", 40 | "contentHash": "4+7HyDa7lVokXObqv8ADeTgi74Cz7W99N07DYtzuOXEt6tJtMlJt8RxLGoPtakVf1mURDMuD5kDKw0oz3EaoDA==" 41 | }, 42 | "System.IO.Hashing": { 43 | "type": "Transitive", 44 | "resolved": "8.0.0", 45 | "contentHash": "ne1843evDugl0md7Fjzy6QjJrzsjh46ZKbhf8GwBXb5f/gw97J4bxMs0NQKifDuThh/f0bZ0e62NPl1jzTuRqA==" 46 | }, 47 | "craftimizer.simulator": { 48 | "type": "Project" 49 | }, 50 | "craftimizer.solver": { 51 | "type": "Project", 52 | "dependencies": { 53 | "Craftimizer.Simulator": "[1.0.0, )", 54 | "DotNext": "[5.21.0, )", 55 | "Raphael.Net": "[2.1.1, )" 56 | } 57 | } 58 | }, 59 | "net9.0-windows7.0/win-x64": { 60 | "Raphael.Net": { 61 | "type": "Transitive", 62 | "resolved": "2.1.1", 63 | "contentHash": "4+7HyDa7lVokXObqv8ADeTgi74Cz7W99N07DYtzuOXEt6tJtMlJt8RxLGoPtakVf1mURDMuD5kDKw0oz3EaoDA==" 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /Images/MacroEditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkingRobot/Craftimizer/fd1651cef90acaa0636316851a81b0137a7c8f86/Images/MacroEditor.png -------------------------------------------------------------------------------- /Images/RecipeNote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkingRobot/Craftimizer/fd1651cef90acaa0636316851a81b0137a7c8f86/Images/RecipeNote.png -------------------------------------------------------------------------------- /Images/SynthHelper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkingRobot/Craftimizer/fd1651cef90acaa0636316851a81b0137a7c8f86/Images/SynthHelper.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Asriel Camora 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Craftimizer 2 | 3 | soon(tm) -------------------------------------------------------------------------------- /Simulator/ActionCategory.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Simulator.Actions; 2 | using System.Collections.Frozen; 3 | 4 | namespace Craftimizer.Simulator; 5 | 6 | public enum ActionCategory 7 | { 8 | FirstTurn, 9 | Synthesis, 10 | Quality, 11 | Durability, 12 | Buffs, 13 | Combo, 14 | Other 15 | } 16 | 17 | public static class ActionCategoryUtils 18 | { 19 | private static readonly FrozenDictionary SortedActions; 20 | 21 | static ActionCategoryUtils() 22 | { 23 | SortedActions = 24 | Enum.GetValues() 25 | .GroupBy(a => a.Category()) 26 | .ToFrozenDictionary(g => g.Key, g => g.OrderBy(a => a.Level()).ToArray()); 27 | } 28 | 29 | public static IReadOnlyList GetActions(this ActionCategory me) 30 | { 31 | if (SortedActions.TryGetValue(me, out var actions)) 32 | return actions; 33 | 34 | throw new ArgumentException($"Unknown action category {me}", nameof(me)); 35 | } 36 | 37 | public static string GetDisplayName(this ActionCategory category) => 38 | category switch 39 | { 40 | ActionCategory.FirstTurn => "First Turn", 41 | ActionCategory.Synthesis => "Synthesis", 42 | ActionCategory.Quality => "Quality", 43 | ActionCategory.Durability => "Durability", 44 | ActionCategory.Buffs => "Buffs", 45 | ActionCategory.Other => "Other", 46 | _ => category.ToString() 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /Simulator/ActionProc.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator; 2 | 3 | public enum ActionProc : byte 4 | { 5 | None, 6 | UsedBasicTouch, 7 | AdvancedTouch 8 | } 9 | -------------------------------------------------------------------------------- /Simulator/ActionResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator; 2 | 3 | public enum ActionResponse 4 | { 5 | SimulationComplete, 6 | ActionNotUnlocked, 7 | NotEnoughCP, 8 | NoDurability, 9 | CannotUseAction, 10 | UsedAction, 11 | } 12 | -------------------------------------------------------------------------------- /Simulator/ActionStates.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Simulator.Actions; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Craftimizer.Simulator; 6 | 7 | [StructLayout(LayoutKind.Auto)] 8 | public record struct ActionStates 9 | { 10 | public ActionProc Combo; 11 | public byte CarefulObservationCount; 12 | public bool UsedHeartAndSoul; 13 | public bool UsedQuickInnovation; 14 | public bool UsedTrainedPerfection; 15 | 16 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 17 | public void MutateState(BaseAction baseAction) 18 | { 19 | if (baseAction is BasicTouch) 20 | Combo = ActionProc.UsedBasicTouch; 21 | else if ((Combo == ActionProc.UsedBasicTouch && baseAction is StandardTouch) || baseAction is Observe) 22 | Combo = ActionProc.AdvancedTouch; 23 | else 24 | Combo = ActionProc.None; 25 | 26 | if (baseAction is CarefulObservation) 27 | CarefulObservationCount++; 28 | 29 | if (baseAction is HeartAndSoul) 30 | UsedHeartAndSoul = true; 31 | 32 | if (baseAction is QuickInnovation) 33 | UsedQuickInnovation = true; 34 | 35 | if (baseAction is TrainedPerfection) 36 | UsedTrainedPerfection = true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Simulator/Actions/ActionType.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Craftimizer.Simulator.Actions; 5 | 6 | public enum ActionType : byte 7 | { 8 | AdvancedTouch, 9 | BasicSynthesis, 10 | BasicTouch, 11 | ByregotsBlessing, 12 | CarefulObservation, 13 | CarefulSynthesis, 14 | DaringTouch, 15 | DelicateSynthesis, 16 | FinalAppraisal, 17 | GreatStrides, 18 | Groundwork, 19 | HastyTouch, 20 | HeartAndSoul, 21 | ImmaculateMend, 22 | Innovation, 23 | IntensiveSynthesis, 24 | Manipulation, 25 | MastersMend, 26 | MuscleMemory, 27 | Observe, 28 | PreciseTouch, 29 | PreparatoryTouch, 30 | PrudentSynthesis, 31 | PrudentTouch, 32 | QuickInnovation, 33 | RapidSynthesis, 34 | RefinedTouch, 35 | Reflect, 36 | StandardTouch, 37 | TrainedEye, 38 | TrainedFinesse, 39 | TrainedPerfection, 40 | TricksOfTheTrade, 41 | Veneration, 42 | WasteNot, 43 | WasteNot2, 44 | 45 | StandardTouchCombo, 46 | AdvancedTouchCombo, 47 | ObservedAdvancedTouchCombo, 48 | RefinedTouchCombo, 49 | } 50 | 51 | public static class ActionUtils 52 | { 53 | private static readonly BaseAction[] Actions; 54 | 55 | static ActionUtils() 56 | { 57 | var types = typeof(BaseAction).Assembly.GetTypes() 58 | .Where(t => t.IsAssignableTo(typeof(BaseAction)) && !t.IsAbstract); 59 | Actions = Enum.GetNames() 60 | .Select(a => types.First(t => t.Name.Equals(a, StringComparison.Ordinal))) 61 | .Select(t => (Activator.CreateInstance(t) as BaseAction)!) 62 | .ToArray(); 63 | } 64 | 65 | [Pure] 66 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 67 | public static BaseAction Base(this ActionType me) => Actions[(int)me]; 68 | 69 | public static int Level(this ActionType me) => 70 | me.Base().Level; 71 | 72 | public static ActionCategory Category(this ActionType me) => 73 | me.Base().Category; 74 | } 75 | -------------------------------------------------------------------------------- /Simulator/Actions/AdvancedTouch.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class AdvancedTouch() : BaseAction( 4 | ActionCategory.Quality, level: 68, actionId: 100411, 5 | increasesQuality: true, 6 | defaultCPCost: 46, defaultEfficiency: 150) 7 | { 8 | public override int CPCost(Simulator s) => 9 | (s.ActionStates.Combo == ActionProc.AdvancedTouch) ? 18 : 46; 10 | } 11 | -------------------------------------------------------------------------------- /Simulator/Actions/AdvancedTouchCombo.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class AdvancedTouchCombo() : BaseComboAction( 4 | ActionType.StandardTouchCombo, ActionType.AdvancedTouch, 18 * 3 5 | ) 6 | { 7 | public override bool CouldUse(Simulator s) => 8 | BaseCouldUse(s) && VerifyDurability3(s, StandardTouchCombo.ActionA.DurabilityCost, StandardTouchCombo.ActionB.DurabilityCost); 9 | } 10 | -------------------------------------------------------------------------------- /Simulator/Actions/BaseAction.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Text; 3 | 4 | namespace Craftimizer.Simulator.Actions; 5 | 6 | public abstract class BaseAction( 7 | ActionCategory category, int level, uint actionId, 8 | int macroWaitTime = 3, 9 | bool increasesProgress = false, bool increasesQuality = false, 10 | int durabilityCost = 10, bool increasesStepCount = true, 11 | int defaultCPCost = 0, 12 | int defaultEfficiency = 0, 13 | int defaultSuccessRate = 100) 14 | { 15 | // Non-instanced properties 16 | 17 | // Metadata 18 | public readonly ActionCategory Category = category; 19 | 20 | public readonly int Level = level; 21 | // Doesn't matter from which class, we'll use the sheet to extrapolate the rest 22 | public readonly uint ActionId = actionId; 23 | // Seconds 24 | public readonly int MacroWaitTime = macroWaitTime; 25 | 26 | // Action properties 27 | public readonly bool IncreasesProgress = increasesProgress; 28 | public readonly bool IncreasesQuality = increasesQuality; 29 | public readonly int DurabilityCost = durabilityCost; 30 | public readonly bool IncreasesStepCount = increasesStepCount; 31 | 32 | // Instanced properties 33 | public readonly int DefaultCPCost = defaultCPCost; 34 | public readonly int DefaultEfficiency = defaultEfficiency; 35 | public readonly int DefaultSuccessRate = defaultSuccessRate; // out of 100 36 | 37 | // Instanced properties 38 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 39 | public virtual int CPCost(Simulator s) => 40 | DefaultCPCost; 41 | 42 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 43 | public virtual int Efficiency(Simulator s) => 44 | DefaultEfficiency; 45 | 46 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 47 | public virtual int SuccessRate(Simulator s) => 48 | DefaultSuccessRate; 49 | 50 | // Return true if it can be in the action pool now or in the future 51 | // e.g. if Heart and Soul is already used, it is impossible to use it again 52 | // or if it's a first step action and IsFirstStep is false 53 | public virtual bool IsPossible(Simulator s) => 54 | s.Input.Stats.Level >= Level; 55 | 56 | // Return true if it can be used now 57 | // This already assumes that IsPossible returns true *at some point before* 58 | public virtual bool CouldUse(Simulator s) => 59 | s.CP >= CPCost(s); 60 | 61 | public bool CanUse(Simulator s) => 62 | IsPossible(s) && CouldUse(s); 63 | 64 | public virtual void Use(Simulator s) 65 | { 66 | if (s.RollSuccess(SuccessRate(s))) 67 | UseSuccess(s); 68 | 69 | s.ReduceCP(CPCost(s)); 70 | s.ReduceDurability(DurabilityCost); 71 | 72 | if (IncreasesStepCount) 73 | { 74 | if (s.Durability > 0) 75 | if (s.HasEffect(EffectType.Manipulation)) 76 | s.RestoreDurability(5); 77 | 78 | s.IncreaseStepCount(); 79 | 80 | s.ActiveEffects.DecrementDuration(); 81 | } 82 | 83 | s.ActionStates.MutateState(this); 84 | s.ActionCount++; 85 | } 86 | 87 | public virtual void UseSuccess(Simulator s) 88 | { 89 | var eff = Efficiency(s); 90 | if (eff != 0) 91 | { 92 | if (IncreasesProgress) 93 | s.IncreaseProgress(eff); 94 | if (IncreasesQuality) 95 | s.IncreaseQuality(eff); 96 | } 97 | } 98 | 99 | public virtual string GetTooltip(Simulator s, bool addUsability) 100 | { 101 | var cost = CPCost(s); 102 | var eff = Efficiency(s); 103 | var success = SuccessRate(s); 104 | 105 | var builder = new StringBuilder(); 106 | if (addUsability && !CanUse(s)) 107 | builder.AppendLine($"Cannot Use"); 108 | builder.AppendLine($"Level {Level}"); 109 | if (cost != 0) 110 | builder.AppendLine($"-{s.CalculateCPCost(cost)} CP"); 111 | if (DurabilityCost != 0) 112 | builder.AppendLine($"-{s.CalculateDurabilityCost(DurabilityCost)} Durability"); 113 | if (eff != 0) 114 | { 115 | if (IncreasesProgress) 116 | builder.AppendLine($"+{s.CalculateProgressGain(eff)} Progress"); 117 | if (IncreasesQuality) 118 | builder.AppendLine($"+{s.CalculateQualityGain(eff)} Quality"); 119 | } 120 | if (!IncreasesStepCount) 121 | builder.AppendLine($"Does Not Increase Step Count"); 122 | if (success != 100) 123 | builder.AppendLine($"{s.CalculateSuccessRate(success)}% Success Rate"); 124 | return builder.ToString(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Simulator/Actions/BaseBuffAction.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Craftimizer.Simulator.Actions; 4 | 5 | internal abstract class BaseBuffAction( 6 | ActionCategory category, int level, uint actionId, 7 | EffectType effect, int duration, 8 | int macroWaitTime = 2, 9 | bool increasesProgress = false, bool increasesQuality = false, 10 | int durabilityCost = 0, bool increasesStepCount = true, 11 | int defaultCPCost = 0, 12 | int defaultEfficiency = 0, 13 | int defaultSuccessRate = 100) : 14 | BaseAction( 15 | category, level, actionId, 16 | macroWaitTime, 17 | increasesProgress, increasesQuality, 18 | durabilityCost, increasesStepCount, 19 | defaultCPCost, defaultEfficiency, defaultSuccessRate) 20 | { 21 | // Non-instanced properties 22 | public readonly EffectType Effect = effect; 23 | public readonly int Duration = duration; 24 | private readonly int trueDuration = increasesStepCount ? duration + 1 : duration; 25 | 26 | public override void UseSuccess(Simulator s) => 27 | s.AddEffect(Effect, trueDuration); 28 | 29 | public override string GetTooltip(Simulator s, bool addUsability) 30 | { 31 | var builder = new StringBuilder(base.GetTooltip(s, addUsability)); 32 | builder.AppendLine(Duration != 1 ? $"{Duration} Steps" : $"{Duration} Step"); 33 | return builder.ToString(); 34 | } 35 | 36 | protected string GetBaseTooltip(Simulator s, bool addUsability) => 37 | base.GetTooltip(s, addUsability); 38 | } 39 | -------------------------------------------------------------------------------- /Simulator/Actions/BaseComboAction.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | public abstract class BaseComboAction( 4 | ActionType actionTypeA, ActionType actionTypeB, 5 | BaseAction actionA, BaseAction actionB, 6 | int? defaultCPCost = null) : 7 | BaseAction( 8 | ActionCategory.Combo, Math.Max(actionA.Level, actionA.Level), actionB.ActionId, 9 | increasesProgress: actionA.IncreasesProgress || actionB.IncreasesProgress, 10 | increasesQuality: actionA.IncreasesQuality || actionB.IncreasesQuality, 11 | defaultCPCost: defaultCPCost ?? (actionA.DefaultCPCost + actionB.DefaultCPCost)) 12 | { 13 | public readonly ActionType ActionTypeA = actionTypeA; 14 | public readonly ActionType ActionTypeB = actionTypeB; 15 | 16 | protected bool BaseCouldUse(Simulator s) => 17 | base.CouldUse(s); 18 | 19 | private static bool VerifyDurability2(int durabilityA, int durability, in Effects effects) 20 | { 21 | if (!effects.HasEffect(EffectType.TrainedPerfection)) 22 | { 23 | var wasteNots = effects.HasEffect(EffectType.WasteNot) || effects.HasEffect(EffectType.WasteNot2); 24 | // -A 25 | durability -= (int)MathF.Ceiling(durabilityA * (wasteNots ? .5f : 1f)); 26 | if (durability <= 0) 27 | return false; 28 | } 29 | 30 | // If we can do the first action and still have durability left to survive to the next 31 | // step (even before the Manipulation modifier), we can certainly do the next action. 32 | return true; 33 | } 34 | 35 | public static bool VerifyDurability2(Simulator s, int durabilityA) => 36 | VerifyDurability2(durabilityA, s.Durability, s.ActiveEffects); 37 | 38 | public static bool VerifyDurability3(int durabilityA, int durabilityB, int durability, in Effects effects) 39 | { 40 | var wasteNots = Math.Max(effects.GetDuration(EffectType.WasteNot), effects.GetDuration(EffectType.WasteNot2)); 41 | var manips = effects.HasEffect(EffectType.Manipulation); 42 | var perfection = effects.HasEffect(EffectType.TrainedPerfection); 43 | 44 | if (!perfection) 45 | { 46 | durability -= (int)MathF.Ceiling(durabilityA * (wasteNots > 0 ? .5f : 1f)); 47 | 48 | if (durability <= 0) 49 | return false; 50 | } 51 | 52 | if (manips) 53 | durability += 5; 54 | 55 | if (wasteNots > 0) 56 | wasteNots--; 57 | 58 | durability -= (int)MathF.Ceiling(durabilityB * (wasteNots > 0 ? .5f : 1f)); 59 | 60 | if (durability <= 0) 61 | return false; 62 | 63 | // If we can do the second action and still have durability left to survive to the next 64 | // step (even before the Manipulation modifier), we can certainly do the next action. 65 | return true; 66 | } 67 | 68 | public static bool VerifyDurability3(Simulator s, int durabilityA, int durabilityB) => 69 | VerifyDurability3(durabilityA, durabilityB, s.Durability, s.ActiveEffects); 70 | } 71 | -------------------------------------------------------------------------------- /Simulator/Actions/BaseComboActionImpl.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal abstract class BaseComboAction( 4 | ActionType actionTypeA, ActionType actionTypeB, 5 | int? baseCPCost = null) : 6 | BaseComboAction( 7 | actionTypeA, actionTypeB, 8 | ActionA, ActionB, 9 | baseCPCost 10 | ) where A : BaseAction, new() where B : BaseAction, new() 11 | { 12 | protected static readonly A ActionA = new(); 13 | protected static readonly B ActionB = new(); 14 | 15 | public override bool IsPossible(Simulator s) => ActionA.IsPossible(s) && ActionB.IsPossible(s); 16 | 17 | public override bool CouldUse(Simulator s) => 18 | BaseCouldUse(s) && VerifyDurability2(s, ActionA.DurabilityCost); 19 | 20 | public override void Use(Simulator s) 21 | { 22 | ActionA.Use(s); 23 | ActionB.Use(s); 24 | } 25 | 26 | public override string GetTooltip(Simulator s, bool addUsability) => 27 | $"{ActionA.GetTooltip(s, addUsability)}\n\n{ActionB.GetTooltip(s, addUsability)}"; 28 | } 29 | -------------------------------------------------------------------------------- /Simulator/Actions/BasicSynthesis.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class BasicSynthesis() : BaseAction( 4 | ActionCategory.Synthesis, 1, 100001, 5 | increasesProgress: true, 6 | defaultCPCost: 0, 7 | defaultEfficiency: 100 8 | ) 9 | { 10 | // Basic Synthesis Mastery Trait 11 | public override int Efficiency(Simulator s) => 12 | s.Input.Stats.Level >= 31 ? 120 : 100; 13 | } 14 | -------------------------------------------------------------------------------- /Simulator/Actions/BasicTouch.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class BasicTouch() : BaseAction( 4 | ActionCategory.Quality, 5, 100002, 5 | increasesQuality: true, 6 | defaultCPCost: 18, 7 | defaultEfficiency: 100) 8 | { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /Simulator/Actions/ByregotsBlessing.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class ByregotsBlessing() : BaseAction( 4 | ActionCategory.Quality, 50, 100339, 5 | increasesQuality: true, 6 | defaultCPCost: 24, 7 | defaultEfficiency: 100) 8 | { 9 | public override int Efficiency(Simulator s) => 10 | 100 + (20 * s.GetEffectStrength(EffectType.InnerQuiet)); 11 | 12 | public override bool CouldUse(Simulator s) => 13 | s.HasEffect(EffectType.InnerQuiet) && base.CouldUse(s); 14 | 15 | public override void UseSuccess(Simulator s) 16 | { 17 | base.UseSuccess(s); 18 | s.RemoveEffect(EffectType.InnerQuiet); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Simulator/Actions/CarefulObservation.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class CarefulObservation() : BaseAction( 4 | ActionCategory.Other, 55, 100395, 5 | durabilityCost: 0, increasesStepCount: false, 6 | defaultCPCost: 0 7 | ) 8 | { 9 | public override bool IsPossible(Simulator s) => 10 | base.IsPossible(s) && s.Input.Stats.IsSpecialist && s.ActionStates.CarefulObservationCount < 3; 11 | 12 | public override bool CouldUse(Simulator s) => 13 | s.ActionStates.CarefulObservationCount < 3; 14 | 15 | public override void UseSuccess(Simulator s) => 16 | s.StepCondition(); 17 | 18 | public override string GetTooltip(Simulator s, bool addUsability) => 19 | $"{base.GetTooltip(s, addUsability)}Specialist Only\n"; 20 | } 21 | -------------------------------------------------------------------------------- /Simulator/Actions/CarefulSynthesis.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class CarefulSynthesis() : BaseAction( 4 | ActionCategory.Synthesis, 62, 100203, 5 | increasesProgress: true, 6 | defaultCPCost: 7, 7 | defaultEfficiency: 150 8 | ) 9 | { 10 | // Careful Synthesis Mastery Trait 11 | public override int Efficiency(Simulator s) => 12 | s.Input.Stats.Level >= 82 ? 180 : 150; 13 | } 14 | -------------------------------------------------------------------------------- /Simulator/Actions/DaringTouch.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class DaringTouch() : BaseAction( 4 | ActionCategory.Quality, 96, 100451, 5 | increasesQuality: true, 6 | defaultCPCost: 0, 7 | defaultEfficiency: 150, 8 | defaultSuccessRate: 60 9 | ) 10 | { 11 | public override bool CouldUse(Simulator s) => 12 | s.HasEffect(EffectType.Expedience) && base.CouldUse(s); 13 | } 14 | -------------------------------------------------------------------------------- /Simulator/Actions/DelicateSynthesis.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class DelicateSynthesis() : BaseAction( 4 | ActionCategory.Synthesis, 76, 100323, 5 | increasesProgress: true, increasesQuality: true, 6 | defaultCPCost: 32, 7 | defaultEfficiency: 100 8 | ) 9 | { 10 | public override void UseSuccess(Simulator s) 11 | { 12 | // Delicate Synthesis Mastery Trait 13 | var hasTrait = s.Input.Stats.Level >= 94; 14 | s.IncreaseProgress(hasTrait ? 150 : 100); 15 | s.IncreaseQuality(100); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Simulator/Actions/FinalAppraisal.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class FinalAppraisal() : BaseBuffAction( 4 | ActionCategory.Other, 42, 19012, 5 | EffectType.FinalAppraisal, duration: 5, 6 | increasesStepCount: false, 7 | defaultCPCost: 1) 8 | { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /Simulator/Actions/GreatStrides.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class GreatStrides() : BaseBuffAction( 4 | ActionCategory.Buffs, 21, 260, 5 | EffectType.GreatStrides, duration: 3, 6 | defaultCPCost: 32) 7 | { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /Simulator/Actions/Groundwork.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class Groundwork() : BaseAction( 4 | ActionCategory.Synthesis, 72, 100403, 5 | increasesProgress: true, 6 | durabilityCost: 20, 7 | defaultCPCost: 18, 8 | defaultEfficiency: 300 9 | ) 10 | { 11 | public override int Efficiency(Simulator s) 12 | { 13 | // Groundwork Mastery Trait 14 | var eff = s.Input.Stats.Level >= 86 ? 360 : 300; 15 | if (s.Durability < s.CalculateDurabilityCost(DurabilityCost)) 16 | eff /= 2; 17 | return eff; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Simulator/Actions/HastyTouch.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class HastyTouch() : BaseAction( 4 | ActionCategory.Quality, 9, 100355, 5 | increasesQuality: true, 6 | defaultCPCost: 0, 7 | defaultEfficiency: 100, 8 | defaultSuccessRate: 60 9 | ) 10 | { 11 | public override void UseSuccess(Simulator s) 12 | { 13 | base.UseSuccess(s); 14 | 15 | if (s.Input.Stats.Level >= 96) 16 | s.AddEffect(EffectType.Expedience, 1 + 1); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Simulator/Actions/HeartAndSoul.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class HeartAndSoul() : BaseBuffAction( 4 | ActionCategory.Other, 86, 100419, 5 | EffectType.HeartAndSoul, duration: 1, 6 | macroWaitTime: 3, 7 | increasesStepCount: false 8 | ) 9 | { 10 | public override bool IsPossible(Simulator s) => 11 | base.IsPossible(s) && s.Input.Stats.IsSpecialist && !s.ActionStates.UsedHeartAndSoul; 12 | 13 | public override bool CouldUse(Simulator s) => 14 | !s.ActionStates.UsedHeartAndSoul; 15 | 16 | public override string GetTooltip(Simulator s, bool addUsability) => 17 | $"{GetBaseTooltip(s, addUsability)}Specialist Only\n"; 18 | } 19 | -------------------------------------------------------------------------------- /Simulator/Actions/ImmaculateMend.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class ImmaculateMend() : BaseAction( 4 | ActionCategory.Durability, 98, 100467, 5 | durabilityCost: 0, 6 | defaultCPCost: 112 7 | ) 8 | { 9 | public override void UseSuccess(Simulator s) => 10 | s.RestoreAllDurability(); 11 | } 12 | -------------------------------------------------------------------------------- /Simulator/Actions/Innovation.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class Innovation() : BaseBuffAction( 4 | ActionCategory.Buffs, 26, 19004, 5 | EffectType.Innovation, duration: 4, 6 | defaultCPCost: 18) 7 | { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /Simulator/Actions/IntensiveSynthesis.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class IntensiveSynthesis() : BaseAction( 4 | ActionCategory.Synthesis, 78, 100315, 5 | increasesProgress: true, 6 | defaultCPCost: 6, 7 | defaultEfficiency: 400 8 | ) 9 | { 10 | public override bool CouldUse(Simulator s) => 11 | (s.Condition is Condition.Good or Condition.Excellent || s.HasEffect(EffectType.HeartAndSoul)) 12 | && base.CouldUse(s); 13 | 14 | public override void UseSuccess(Simulator s) 15 | { 16 | base.UseSuccess(s); 17 | if (s.Condition is not (Condition.Good or Condition.Excellent)) 18 | s.RemoveEffect(EffectType.HeartAndSoul); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Simulator/Actions/Manipulation.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class Manipulation() : BaseBuffAction( 4 | ActionCategory.Durability, 65, 4574, 5 | EffectType.Manipulation, duration: 8, 6 | defaultCPCost: 96) 7 | { 8 | public override bool IsPossible(Simulator s) => 9 | s.Input.Stats.CanUseManipulation && base.IsPossible(s); 10 | 11 | public override void Use(Simulator s) 12 | { 13 | UseSuccess(s); 14 | 15 | s.ReduceCP(CPCost(s)); 16 | 17 | s.IncreaseStepCount(); 18 | 19 | s.ActionStates.MutateState(this); 20 | s.ActionCount++; 21 | 22 | s.ActiveEffects.DecrementDuration(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Simulator/Actions/MastersMend.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class MastersMend() : BaseAction( 4 | ActionCategory.Durability, 7, 100003, 5 | durabilityCost: 0, 6 | defaultCPCost: 88 7 | ) 8 | { 9 | public override void UseSuccess(Simulator s) => 10 | s.RestoreDurability(30); 11 | } 12 | -------------------------------------------------------------------------------- /Simulator/Actions/MuscleMemory.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class MuscleMemory() : BaseAction( 4 | ActionCategory.FirstTurn, 54, 100379, 5 | increasesProgress: true, 6 | defaultCPCost: 6, 7 | defaultEfficiency: 300 8 | ) 9 | { 10 | public override bool IsPossible(Simulator s) => s.IsFirstStep && base.IsPossible(s); 11 | 12 | public override bool CouldUse(Simulator s) => s.IsFirstStep && base.CouldUse(s); 13 | 14 | public override void UseSuccess(Simulator s) 15 | { 16 | base.UseSuccess(s); 17 | s.AddEffect(EffectType.MuscleMemory, 5 + 1); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Simulator/Actions/Observe.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class Observe() : BaseAction( 4 | ActionCategory.Other, 13, 100010, 5 | durabilityCost: 0, 6 | defaultCPCost: 7 7 | ) 8 | { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /Simulator/Actions/ObservedAdvancedTouchCombo.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class ObservedAdvancedTouchCombo() : BaseComboAction( 4 | ActionType.Observe, ActionType.AdvancedTouch, 7 + 18 5 | ) 6 | { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /Simulator/Actions/PreciseTouch.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class PreciseTouch() : BaseAction( 4 | ActionCategory.Quality, 53, 100128, 5 | increasesQuality: true, 6 | defaultCPCost: 18, 7 | defaultEfficiency: 150 8 | ) 9 | { 10 | public override bool CouldUse(Simulator s) => 11 | (s.Condition is Condition.Good or Condition.Excellent || s.HasEffect(EffectType.HeartAndSoul)) 12 | && base.CouldUse(s); 13 | 14 | public override void UseSuccess(Simulator s) 15 | { 16 | base.UseSuccess(s); 17 | s.StrengthenEffect(EffectType.InnerQuiet); 18 | if (s.Condition is not (Condition.Good or Condition.Excellent)) 19 | s.RemoveEffect(EffectType.HeartAndSoul); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Simulator/Actions/PreparatoryTouch.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class PreparatoryTouch() : BaseAction( 4 | ActionCategory.Quality, 71, 100299, 5 | increasesQuality: true, 6 | durabilityCost: 20, 7 | defaultCPCost: 40, 8 | defaultEfficiency: 200) 9 | { 10 | public override void UseSuccess(Simulator s) 11 | { 12 | base.UseSuccess(s); 13 | s.StrengthenEffect(EffectType.InnerQuiet); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Simulator/Actions/PrudentSynthesis.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class PrudentSynthesis() : BaseAction( 4 | ActionCategory.Synthesis, 88, 100427, 5 | increasesProgress: true, 6 | durabilityCost: 5, 7 | defaultCPCost: 18, 8 | defaultEfficiency: 180 9 | ) 10 | { 11 | public override bool CouldUse(Simulator s) => 12 | !(s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2)) 13 | && base.CouldUse(s); 14 | } 15 | -------------------------------------------------------------------------------- /Simulator/Actions/PrudentTouch.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class PrudentTouch() : BaseAction( 4 | ActionCategory.Quality, 66, 100227, 5 | increasesQuality: true, 6 | durabilityCost: 5, 7 | defaultCPCost: 25, 8 | defaultEfficiency: 100 9 | ) 10 | { 11 | public override bool CouldUse(Simulator s) => 12 | !(s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2)) 13 | && base.CouldUse(s); 14 | } 15 | -------------------------------------------------------------------------------- /Simulator/Actions/QuickInnovation.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class QuickInnovation() : BaseBuffAction( 4 | ActionCategory.Other, 96, 100459, 5 | EffectType.Innovation, duration: 1, 6 | macroWaitTime: 3, 7 | increasesStepCount: false 8 | ) 9 | { 10 | public override bool IsPossible(Simulator s) => 11 | base.IsPossible(s) && s.Input.Stats.IsSpecialist && !s.ActionStates.UsedQuickInnovation; 12 | 13 | public override bool CouldUse(Simulator s) => 14 | !s.ActionStates.UsedQuickInnovation && !s.HasEffect(EffectType.Innovation); 15 | 16 | public override string GetTooltip(Simulator s, bool addUsability) => 17 | $"{base.GetTooltip(s, addUsability)}Specialist Only\n"; 18 | } 19 | -------------------------------------------------------------------------------- /Simulator/Actions/RapidSynthesis.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class RapidSynthesis() : BaseAction( 4 | ActionCategory.Synthesis, 9, 100363, 5 | increasesProgress: true, 6 | defaultCPCost: 0, 7 | defaultEfficiency: 250, 8 | defaultSuccessRate: 50 9 | ) 10 | { 11 | // Rapid Synthesis Mastery Trait 12 | public override int Efficiency(Simulator s) => 13 | s.Input.Stats.Level >= 63 ? 500 : 250; 14 | } 15 | -------------------------------------------------------------------------------- /Simulator/Actions/RefinedTouch.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class RefinedTouch() : BaseAction( 4 | ActionCategory.Quality, 92, 100443, 5 | increasesQuality: true, 6 | defaultCPCost: 24, 7 | defaultEfficiency: 100 8 | ) 9 | { 10 | public override void UseSuccess(Simulator s) 11 | { 12 | base.UseSuccess(s); 13 | if (s.ActionStates.Combo == ActionProc.UsedBasicTouch) 14 | s.StrengthenEffect(EffectType.InnerQuiet); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Simulator/Actions/RefinedTouchCombo.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class RefinedTouchCombo() : BaseComboAction( 4 | ActionType.BasicTouch, ActionType.RefinedTouch 5 | ) 6 | { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /Simulator/Actions/Reflect.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class Reflect() : BaseAction( 4 | ActionCategory.FirstTurn, 69, 100387, 5 | increasesQuality: true, 6 | defaultCPCost: 6, 7 | defaultEfficiency: 300 8 | ) 9 | { 10 | public override bool IsPossible(Simulator s) => s.IsFirstStep && base.IsPossible(s); 11 | 12 | public override bool CouldUse(Simulator s) => s.IsFirstStep && base.CouldUse(s); 13 | 14 | public override void UseSuccess(Simulator s) 15 | { 16 | base.UseSuccess(s); 17 | s.StrengthenEffect(EffectType.InnerQuiet); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Simulator/Actions/StandardTouch.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class StandardTouch() : BaseAction( 4 | ActionCategory.Quality, 18, 100004, 5 | increasesQuality: true, 6 | defaultCPCost: 32, 7 | defaultEfficiency: 125 8 | ) 9 | { 10 | public override int CPCost(Simulator s) => 11 | s.ActionStates.Combo == ActionProc.UsedBasicTouch ? 18 : 32; 12 | } 13 | -------------------------------------------------------------------------------- /Simulator/Actions/StandardTouchCombo.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class StandardTouchCombo() : BaseComboAction( 4 | ActionType.BasicTouch, ActionType.StandardTouch, 18 * 2 5 | ) 6 | { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /Simulator/Actions/TrainedEye.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class TrainedEye() : BaseAction( 4 | ActionCategory.FirstTurn, 80, 100283, 5 | increasesQuality: true, 6 | durabilityCost: 0, 7 | defaultCPCost: 250 8 | ) 9 | { 10 | public override bool IsPossible(Simulator s) => 11 | s.IsFirstStep && 12 | !s.Input.Recipe.IsExpert && 13 | s.Input.Stats.Level >= (s.Input.Recipe.ClassJobLevel + 10) && 14 | base.IsPossible(s); 15 | 16 | public override bool CouldUse(Simulator s) => 17 | s.IsFirstStep && base.CouldUse(s); 18 | 19 | public override void UseSuccess(Simulator s) => 20 | s.IncreaseQualityRaw(s.Input.Recipe.MaxQuality - s.Quality); 21 | 22 | public override string GetTooltip(Simulator s, bool addUsability) => 23 | $"{base.GetTooltip(s, addUsability)}+{s.Input.Recipe.MaxQuality - s.Quality} Quality"; 24 | } 25 | -------------------------------------------------------------------------------- /Simulator/Actions/TrainedFinesse.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class TrainedFinesse() : BaseAction( 4 | ActionCategory.Quality, 90, 100435, 5 | increasesQuality: true, 6 | durabilityCost: 0, 7 | defaultCPCost: 32, 8 | defaultEfficiency: 100 9 | ) 10 | { 11 | public override bool CouldUse(Simulator s) => 12 | s.GetEffectStrength(EffectType.InnerQuiet) == 10 13 | && base.CouldUse(s); 14 | } 15 | -------------------------------------------------------------------------------- /Simulator/Actions/TrainedPerfection.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class TrainedPerfection() : BaseBuffAction( 4 | ActionCategory.Durability, 100, 100475, 5 | EffectType.TrainedPerfection, duration: 1, 6 | macroWaitTime: 3 7 | ) 8 | { 9 | public override bool IsPossible(Simulator s) => 10 | base.IsPossible(s) && !s.ActionStates.UsedTrainedPerfection; 11 | 12 | public override bool CouldUse(Simulator s) => 13 | !s.ActionStates.UsedTrainedPerfection; 14 | 15 | public override string GetTooltip(Simulator s, bool addUsability) => 16 | GetBaseTooltip(s, addUsability); 17 | } 18 | -------------------------------------------------------------------------------- /Simulator/Actions/TricksOfTheTrade.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class TricksOfTheTrade() : BaseAction( 4 | ActionCategory.Other, 13, 100371, 5 | durabilityCost: 0, 6 | defaultCPCost: 0 7 | ) 8 | { 9 | public override bool CouldUse(Simulator s) => 10 | (s.Condition is Condition.Good or Condition.Excellent || s.HasEffect(EffectType.HeartAndSoul)) 11 | && base.CouldUse(s); 12 | 13 | public override void UseSuccess(Simulator s) 14 | { 15 | s.RestoreCP(20); 16 | if (s.Condition is not (Condition.Good or Condition.Excellent)) 17 | s.RemoveEffect(EffectType.HeartAndSoul); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Simulator/Actions/Veneration.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class Veneration() : BaseBuffAction( 4 | ActionCategory.Buffs, 15, 19297, 5 | EffectType.Veneration, duration: 4, 6 | defaultCPCost: 18 7 | ) 8 | { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /Simulator/Actions/WasteNot.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class WasteNot() : BaseBuffAction( 4 | ActionCategory.Durability, 15, 4631, 5 | EffectType.WasteNot, duration: 4, 6 | defaultCPCost: 56 7 | ) 8 | { 9 | public override void UseSuccess(Simulator s) 10 | { 11 | base.UseSuccess(s); 12 | s.RemoveEffect(EffectType.WasteNot2); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Simulator/Actions/WasteNot2.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator.Actions; 2 | 3 | internal sealed class WasteNot2() : BaseBuffAction( 4 | ActionCategory.Durability, 47, 4639, 5 | EffectType.WasteNot2, duration: 8, 6 | defaultCPCost: 98 7 | ) 8 | { 9 | public override void UseSuccess(Simulator s) 10 | { 11 | base.UseSuccess(s); 12 | s.RemoveEffect(EffectType.WasteNot); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Simulator/CharacterStats.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator; 2 | 3 | public sealed record CharacterStats 4 | { 5 | public int Craftsmanship { get; init; } 6 | public int Control { get; init; } 7 | public int CP { get; init; } 8 | public int Level { get; init; } 9 | public bool CanUseManipulation { get; init; } 10 | public bool HasSplendorousBuff { get; init; } 11 | public bool IsSpecialist { get; init; } 12 | } 13 | -------------------------------------------------------------------------------- /Simulator/ClassJob.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator; 2 | 3 | public enum ClassJob 4 | { 5 | Carpenter, 6 | Blacksmith, 7 | Armorer, 8 | Goldsmith, 9 | Leatherworker, 10 | Weaver, 11 | Alchemist, 12 | Culinarian 13 | } 14 | -------------------------------------------------------------------------------- /Simulator/CompletionState.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator; 2 | 3 | public enum CompletionState : byte 4 | { 5 | Incomplete, 6 | ProgressComplete, 7 | NoMoreDurability, 8 | 9 | InvalidAction, 10 | MaxActionCountReached, 11 | NoMoreActions 12 | } 13 | -------------------------------------------------------------------------------- /Simulator/Condition.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator; 2 | 3 | public enum Condition : byte 4 | { 5 | Normal, 6 | Good, 7 | Excellent, 8 | Poor, 9 | 10 | Centered, 11 | Sturdy, 12 | Pliant, 13 | Malleable, 14 | Primed, 15 | GoodOmen, 16 | } 17 | 18 | public static class ConditionUtils 19 | { 20 | [Flags] 21 | private enum ConditionMask : ushort 22 | { 23 | Normal = 1 << 0, // 0x0001 24 | Good = 1 << 1, // 0x0002 25 | Excellent = 1 << 2, // 0x0004 26 | Poor = 1 << 3, // 0x0008 27 | 28 | Centered = 1 << 4, // 0x0010 29 | Sturdy = 1 << 5, // 0x0020 30 | Pliant = 1 << 6, // 0x0040 31 | Malleable = 1 << 7, // 0x0080 32 | Primed = 1 << 8, // 0x0100 33 | GoodOmen = 1 << 9, // 0x0200 34 | } 35 | 36 | public static Condition[] GetPossibleConditions(ushort conditionsFlag) => 37 | Enum.GetValues().Where(c => ((ConditionMask)conditionsFlag).HasFlag((ConditionMask)(1 << (ushort)c))).ToArray(); 38 | } 39 | -------------------------------------------------------------------------------- /Simulator/Craftimizer.Simulator.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | x64 8 | Debug;Release;Deterministic 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | $(DefineConstants);IS_DETERMINISTIC 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Simulator/EffectType.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator; 2 | 3 | public enum EffectType 4 | { 5 | InnerQuiet, 6 | WasteNot, 7 | Veneration, 8 | GreatStrides, 9 | Innovation, 10 | FinalAppraisal, 11 | WasteNot2, 12 | MuscleMemory, 13 | Manipulation, 14 | HeartAndSoul, 15 | Expedience, 16 | TrainedPerfection 17 | } 18 | -------------------------------------------------------------------------------- /Simulator/Effects.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Craftimizer.Simulator; 6 | 7 | [StructLayout(LayoutKind.Auto)] 8 | public record struct Effects 9 | { 10 | public byte InnerQuiet; 11 | public byte WasteNot; 12 | public byte Veneration; 13 | public byte GreatStrides; 14 | public byte Innovation; 15 | public byte FinalAppraisal; 16 | public byte WasteNot2; 17 | public byte MuscleMemory; 18 | public byte Manipulation; 19 | public byte Expedience; 20 | public bool TrainedPerfection; 21 | public bool HeartAndSoul; 22 | 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | public void SetDuration(EffectType effect, byte duration) 25 | { 26 | switch (effect) 27 | { 28 | case EffectType.InnerQuiet: 29 | if (duration == 0) 30 | InnerQuiet = 0; 31 | break; 32 | case EffectType.WasteNot: 33 | WasteNot = duration; 34 | break; 35 | case EffectType.Veneration: 36 | Veneration = duration; 37 | break; 38 | case EffectType.GreatStrides: 39 | GreatStrides = duration; 40 | break; 41 | case EffectType.Innovation: 42 | Innovation = duration; 43 | break; 44 | case EffectType.FinalAppraisal: 45 | FinalAppraisal = duration; 46 | break; 47 | case EffectType.WasteNot2: 48 | WasteNot2 = duration; 49 | break; 50 | case EffectType.MuscleMemory: 51 | MuscleMemory = duration; 52 | break; 53 | case EffectType.Manipulation: 54 | Manipulation = duration; 55 | break; 56 | case EffectType.Expedience: 57 | Expedience = duration; 58 | break; 59 | case EffectType.TrainedPerfection: 60 | TrainedPerfection = duration != 0; 61 | break; 62 | case EffectType.HeartAndSoul: 63 | HeartAndSoul = duration != 0; 64 | break; 65 | } 66 | } 67 | 68 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 69 | public void Strengthen(EffectType effect) 70 | { 71 | if (effect == EffectType.InnerQuiet && InnerQuiet < 10) 72 | InnerQuiet++; 73 | } 74 | 75 | [Pure] 76 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 77 | public readonly byte GetDuration(EffectType effect) => 78 | effect switch 79 | { 80 | EffectType.InnerQuiet => (byte)(InnerQuiet != 0 ? 1 : 0), 81 | EffectType.WasteNot => WasteNot, 82 | EffectType.Veneration => Veneration, 83 | EffectType.GreatStrides => GreatStrides, 84 | EffectType.Innovation => Innovation, 85 | EffectType.FinalAppraisal => FinalAppraisal, 86 | EffectType.WasteNot2 => WasteNot2, 87 | EffectType.MuscleMemory => MuscleMemory, 88 | EffectType.Manipulation => Manipulation, 89 | EffectType.Expedience => Expedience, 90 | EffectType.TrainedPerfection => (byte)(TrainedPerfection ? 1 : 0), 91 | EffectType.HeartAndSoul => (byte)(HeartAndSoul ? 1 : 0), 92 | _ => 0 93 | }; 94 | 95 | [Pure] 96 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 97 | public readonly byte GetStrength(EffectType effect) => 98 | effect == EffectType.InnerQuiet ? InnerQuiet : 99 | (byte)(HasEffect(effect) ? 1 : 0); 100 | 101 | [Pure] 102 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 103 | public readonly bool HasEffect(EffectType effect) => 104 | GetDuration(effect) != 0; 105 | 106 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 107 | public void DecrementDuration() 108 | { 109 | if (WasteNot > 0) 110 | WasteNot--; 111 | if (WasteNot2 > 0) 112 | WasteNot2--; 113 | if (Veneration > 0) 114 | Veneration--; 115 | if (GreatStrides > 0) 116 | GreatStrides--; 117 | if (Innovation > 0) 118 | Innovation--; 119 | if (FinalAppraisal > 0) 120 | FinalAppraisal--; 121 | if (MuscleMemory > 0) 122 | MuscleMemory--; 123 | if (Manipulation > 0) 124 | Manipulation--; 125 | if (Expedience > 0) 126 | Expedience--; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Simulator/Recipe.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator; 2 | 3 | public sealed record RecipeInfo 4 | { 5 | public bool IsExpert { get; init; } 6 | public int ClassJobLevel { get; init; } 7 | public ushort ConditionsFlag { get; init; } 8 | public int MaxDurability { get; init; } 9 | public int MaxQuality { get; init; } 10 | public int MaxProgress { get; init; } 11 | 12 | public int QualityModifier { get; init; } 13 | public int QualityDivider { get; init; } 14 | public int ProgressModifier { get; init; } 15 | public int ProgressDivider { get; init; } 16 | } 17 | -------------------------------------------------------------------------------- /Simulator/SimulationInput.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator; 2 | 3 | public sealed class SimulationInput 4 | { 5 | public CharacterStats Stats { get; } 6 | public RecipeInfo Recipe { get; } 7 | public Random Random { get; } 8 | 9 | public int StartingQuality { get; } 10 | public int BaseProgressGain { get; } 11 | public int BaseQualityGain { get; } 12 | 13 | public SimulationInput(CharacterStats stats, RecipeInfo recipe, int startingQuality, Random random) 14 | { 15 | Stats = stats; 16 | Recipe = recipe; 17 | Random = random; 18 | StartingQuality = startingQuality; 19 | 20 | // https://github.com/NotRanged/NotRanged.github.io/blob/0f4aee074f969fb05aad34feaba605057c08ffd1/app/js/ffxivcraftmodel.js#L88 21 | { 22 | var baseIncrease = (Stats.Craftsmanship * 10f / Recipe.ProgressDivider) + 2; 23 | if (Stats.Level <= Recipe.ClassJobLevel) 24 | baseIncrease *= Recipe.ProgressModifier * 0.01f; 25 | BaseProgressGain = (int)baseIncrease; 26 | } 27 | { 28 | var baseIncrease = (Stats.Control * 10f / Recipe.QualityDivider) + 35; 29 | if (Stats.Level <= Recipe.ClassJobLevel) 30 | baseIncrease *= Recipe.QualityModifier * 0.01f; 31 | BaseQualityGain = (int)baseIncrease; 32 | } 33 | } 34 | 35 | public SimulationInput(CharacterStats stats, RecipeInfo recipe, int startingQuality = 0, int? seed = null) : this(stats, recipe, startingQuality, seed == null ? new Random() : new Random(seed.Value)) 36 | { 37 | 38 | } 39 | 40 | public Condition[] AvailableConditions => ConditionUtils.GetPossibleConditions(Recipe.ConditionsFlag); 41 | 42 | public override string ToString() => 43 | $"SimulationInput {{ Stats = {Stats}, Recipe = {Recipe}, Random = {Random}, StartingQuality = {StartingQuality}, BaseProgressGain = {BaseProgressGain}, BaseQualityGain = {BaseQualityGain} }}"; 44 | } 45 | -------------------------------------------------------------------------------- /Simulator/SimulationState.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Craftimizer.Simulator; 4 | 5 | [StructLayout(LayoutKind.Auto)] 6 | public record struct SimulationState 7 | { 8 | public readonly SimulationInput Input; 9 | 10 | public int ActionCount; 11 | public int StepCount; 12 | public int Progress; 13 | public int Quality; 14 | public int Durability; 15 | public int CP; 16 | public Condition Condition; 17 | public Effects ActiveEffects; 18 | public ActionStates ActionStates; 19 | 20 | // https://github.com/ffxiv-teamcraft/simulator/blob/0682dfa76043ff4ccb38832c184d046ceaff0733/src/model/tables.ts#L2 21 | private static ReadOnlySpan HQPercentTable => [ 22 | 1, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 23 | 9, 9, 9, 10, 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 17, 17, 24 | 17, 18, 18, 18, 19, 19, 20, 20, 21, 22, 23, 24, 26, 28, 31, 34, 38, 42, 47, 52, 58, 64, 68, 71, 25 | 74, 76, 78, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 94, 96, 98, 100 26 | ]; 27 | public readonly int HQPercent => HQPercentTable[(int)Math.Clamp((float)Quality / (Input?.Recipe.MaxQuality ?? 1) * 100, 0, 100)]; 28 | public readonly int Collectability => Math.Max(Quality / 10, 1); 29 | public readonly int MaxCollectability => Math.Max((Input?.Recipe.MaxQuality ?? 1) / 10, 1); 30 | 31 | public readonly bool IsFirstStep => StepCount == 0; 32 | 33 | public SimulationState(SimulationInput input) 34 | { 35 | Input = input; 36 | 37 | StepCount = 0; 38 | Progress = 0; 39 | Quality = input.StartingQuality; 40 | Durability = Input.Recipe.MaxDurability; 41 | CP = Input.Stats.CP; 42 | Condition = Condition.Normal; 43 | ActiveEffects = new(); 44 | ActionCount = 0; 45 | ActionStates = new(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Simulator/SimulatorNoRandom.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Simulator; 2 | 3 | public class SimulatorNoRandom : Simulator 4 | { 5 | public sealed override bool RollSuccessRaw(int successRate) => successRate == 100; 6 | public sealed override Condition GetNextRandomCondition() => Condition.Normal; 7 | } 8 | -------------------------------------------------------------------------------- /Solver/ActionSet.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Simulator.Actions; 2 | using System.Diagnostics.Contracts; 3 | using System.Numerics; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace Craftimizer.Solver; 7 | 8 | public struct ActionSet 9 | { 10 | private ulong bits; 11 | 12 | [Pure] 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | private static int FromAction(ActionType action) => (byte)action; 15 | 16 | [Pure] 17 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 18 | private static ActionType ToAction(int index) => (ActionType)index; 19 | 20 | [Pure] 21 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 22 | private static ulong ToMask(ActionType action) => 1ul << FromAction(action); 23 | 24 | // Return true if action was newly added and not there before. 25 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 26 | public bool AddAction(ActionType action) 27 | { 28 | var mask = ToMask(action); 29 | var old = bits; 30 | bits |= mask; 31 | return (old & mask) == 0; 32 | } 33 | 34 | // Return true if action was newly removed and not already gone. 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | public bool RemoveAction(ActionType action) 37 | { 38 | var mask = ToMask(action); 39 | var old = bits; 40 | bits &= ~mask; 41 | return (old & mask) != 0; 42 | } 43 | 44 | [Pure] 45 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 46 | public readonly bool HasAction(ActionType action) => (bits & ToMask(action)) != 0; 47 | [Pure] 48 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 49 | public readonly ActionType ElementAt(int index) => ToAction(Intrinsics.NthBitSet(bits, index)); 50 | 51 | [Pure] 52 | public readonly int Count => BitOperations.PopCount(bits); 53 | 54 | [Pure] 55 | public readonly bool IsEmpty => bits == 0; 56 | 57 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 58 | public readonly ActionType SelectRandom(Random random) 59 | { 60 | #if IS_DETERMINISTIC 61 | return First(); 62 | #else 63 | return ElementAt(random.Next(Count)); 64 | #endif 65 | } 66 | 67 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 68 | public ActionType PopRandom(Random random) 69 | { 70 | #if IS_DETERMINISTIC 71 | return PopFirst(); 72 | #else 73 | var action = ElementAt(random.Next(Count)); 74 | RemoveAction(action); 75 | return action; 76 | #endif 77 | } 78 | 79 | #if IS_DETERMINISTIC 80 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 81 | private ActionType PopFirst() 82 | { 83 | var action = First(); 84 | RemoveAction(action); 85 | return action; 86 | } 87 | 88 | [Pure] 89 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 90 | private readonly ActionType First() => ElementAt(0); 91 | #endif 92 | } 93 | -------------------------------------------------------------------------------- /Solver/ArenaBuffer.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Craftimizer.Solver; 5 | 6 | public struct ArenaBuffer 7 | { 8 | // Technically 25, but it's very unlikely to actually get to there. 9 | // The benchmark reaches 20 at most, but here we have a little leeway just in case. 10 | internal const int MaxSize = 32; 11 | 12 | internal const int BatchSize = 8; 13 | internal const int BatchSizeBits = 3; // int.Log2(BatchSize); 14 | internal const int BatchSizeMask = BatchSize - 1; 15 | 16 | internal const int BatchCount = MaxSize / BatchSize; 17 | } 18 | 19 | // Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs 20 | public struct ArenaBuffer where T : struct 21 | { 22 | public ArenaNode[][] Data; 23 | public int Count { get; private set; } 24 | 25 | public void Add(ArenaNode node) 26 | { 27 | Data ??= new ArenaNode[ArenaBuffer.BatchCount][]; 28 | 29 | var idx = Count++; 30 | 31 | var (arrayIdx, subIdx) = GetArrayIndex(idx); 32 | 33 | Data[arrayIdx] ??= new ArenaNode[ArenaBuffer.BatchSize]; 34 | 35 | node.ChildIdx = (arrayIdx, subIdx); 36 | Data[arrayIdx][subIdx] = node; 37 | } 38 | 39 | [Pure] 40 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 41 | private static (int arrayIdx, int subIdx) GetArrayIndex(int idx) => 42 | (idx >> ArenaBuffer.BatchSizeBits, idx & ArenaBuffer.BatchSizeMask); 43 | } 44 | -------------------------------------------------------------------------------- /Solver/ArenaNode.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Craftimizer.Solver; 4 | 5 | public sealed class ArenaNode(in T state, ArenaNode? parent = null) where T : struct 6 | { 7 | public T State = state; 8 | public ArenaBuffer Children; 9 | public NodeScoresBuffer ChildScores; 10 | public (int arrayIdx, int subIdx) ChildIdx; 11 | public readonly ArenaNode? Parent = parent; 12 | 13 | public NodeScoresBuffer? ParentScores => Parent?.ChildScores; 14 | 15 | public ArenaNode? ChildAt((int arrayIdx, int subIdx) at) => 16 | Children.Data?[at.arrayIdx]?[at.subIdx]; 17 | 18 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 19 | public ArenaNode Add(in T state) 20 | { 21 | var node = new ArenaNode(in state, this); 22 | ChildScores.Add(); 23 | Children.Add(node); 24 | return node; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Solver/Craftimizer.Solver.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | True 8 | x64 9 | Debug;Release;Deterministic 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | <_Parameter1>Craftimizer.Test 28 | 29 | 30 | 31 | 32 | $(DefineConstants);IS_DETERMINISTIC 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Solver/Intrinsics.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Contracts; 2 | using System.Numerics; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.Intrinsics; 5 | using System.Runtime.Intrinsics.X86; 6 | 7 | namespace Craftimizer.Solver; 8 | 9 | [SkipLocalsInit] 10 | [Pure] 11 | internal static class Intrinsics 12 | { 13 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 14 | // https://stackoverflow.com/a/73439472 15 | private static Vector128 HMax(Vector256 v1) 16 | { 17 | var v2 = Avx.Permute(v1, 0b10110001); 18 | var v3 = Avx.Max(v1, v2); 19 | var v4 = Avx.Permute(v3, 0b00001010); 20 | var v5 = Avx.Max(v3, v4); 21 | var v6 = Avx.ExtractVector128(v5, 1); 22 | var v7 = Sse.Max(v5.GetLower(), v6); 23 | return v7; 24 | } 25 | 26 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 27 | private static int HMaxIndexScalar(Vector256 v, int len) 28 | { 29 | var m = 0; 30 | for (var i = 1; i < len; ++i) 31 | { 32 | if (v[i] >= v[m]) 33 | m = i; 34 | } 35 | return m; 36 | } 37 | 38 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 39 | private static Vector256 ClearLastN(Vector256 data, int len) 40 | { 41 | var threshold = Vector256.Create(len); 42 | var index = Vector256.Create(0, 1, 2, 3, 4, 5, 6, 7); 43 | return Avx.And(Avx2.CompareGreaterThan(threshold, index).AsSingle(), data); 44 | } 45 | 46 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 47 | // https://stackoverflow.com/a/23592221 48 | private static int HMaxIndexAVX2(Vector256 v, int len) 49 | { 50 | // Remove NaNs 51 | var vfilt = ClearLastN(v, len); 52 | 53 | // Find max value and broadcast to all lanes 54 | var vmax128 = HMax(vfilt); 55 | var vmax = Vector256.Create(vmax128, vmax128); 56 | 57 | // Find the highest index with that value, respecting len 58 | var vcmp = Avx.CompareEqual(vfilt, vmax); 59 | var mask = unchecked((uint)Avx.MoveMask(vcmp)); 60 | 61 | return BitOperations.TrailingZeroCount(mask); 62 | } 63 | 64 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 65 | public static int HMaxIndex(Vector256 v, int len) => 66 | Avx2.IsSupported ? 67 | HMaxIndexAVX2(v, len) : 68 | HMaxIndexScalar(v, len); 69 | 70 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 71 | private static int NthBitSetScalar(ulong value, int n) 72 | { 73 | var mask = 0x00000000FFFFFFFFul; 74 | var size = 32; 75 | var _base = 0; 76 | 77 | if (n++ >= BitOperations.PopCount(value)) 78 | return 64; 79 | 80 | while (size > 0) 81 | { 82 | var count = BitOperations.PopCount(value & mask); 83 | if (n > count) 84 | { 85 | _base += size; 86 | size >>= 1; 87 | mask |= mask << size; 88 | } 89 | else 90 | { 91 | size >>= 1; 92 | mask >>= size; 93 | } 94 | } 95 | 96 | return _base; 97 | } 98 | 99 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 100 | private static int NthBitSetBMI2(ulong value, int n) => 101 | BitOperations.TrailingZeroCount(Bmi2.X64.ParallelBitDeposit(1ul << n, value)); 102 | 103 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 104 | public static int NthBitSet(ulong value, int n) 105 | { 106 | if (n >= BitOperations.PopCount(value)) 107 | return 64; 108 | 109 | return Bmi2.X64.IsSupported ? 110 | NthBitSetBMI2(value, n) : 111 | NthBitSetScalar(value, n); 112 | } 113 | 114 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 115 | public static Vector256 ReciprocalSqrt(Vector256 data) 116 | { 117 | #if !IS_DETERMINISTIC 118 | // Accurate to 14 bits 119 | if (Avx512F.VL.IsSupported) 120 | return Avx512F.VL.ReciprocalSqrt14(data); 121 | #endif 122 | 123 | // Accurate to 12 bits 124 | if (Avx.IsSupported) 125 | return Avx.ReciprocalSqrt(data); 126 | 127 | Unsafe.SkipInit(out Vector256 ret); 128 | ref var result = ref Unsafe.As, float>(ref Unsafe.AsRef(in ret)); 129 | for (var i = 0; i < Vector256.Count; ++i) 130 | Unsafe.Add(ref result, i) = MathF.ReciprocalSqrtEstimate(data[i]); 131 | return ret; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Solver/MCTSConfig.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Simulator.Actions; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Craftimizer.Solver; 5 | 6 | [StructLayout(LayoutKind.Auto)] 7 | public readonly record struct MCTSConfig 8 | { 9 | public int MaxThreadCount { get; init; } 10 | 11 | public int MaxStepCount { get; init; } 12 | public int MaxRolloutStepCount { get; init; } 13 | public bool StrictActions { get; init; } 14 | 15 | public float MaxScoreWeightingConstant { get; init; } 16 | public float ExplorationConstant { get; init; } 17 | 18 | public float ScoreProgress { get; init; } 19 | public float ScoreQuality { get; init; } 20 | public float ScoreDurability { get; init; } 21 | public float ScoreCP { get; init; } 22 | public float ScoreSteps { get; init; } 23 | 24 | public ActionType[] ActionPool { get; init; } 25 | 26 | public MCTSConfig(in SolverConfig config) 27 | { 28 | MaxStepCount = config.MaxStepCount; 29 | MaxRolloutStepCount = config.MaxRolloutStepCount; 30 | StrictActions = config.StrictActions; 31 | 32 | MaxScoreWeightingConstant = config.MaxScoreWeightingConstant; 33 | ExplorationConstant = config.ExplorationConstant; 34 | 35 | var total = config.ScoreProgress + 36 | config.ScoreQuality + 37 | config.ScoreDurability + 38 | config.ScoreCP + 39 | config.ScoreSteps; 40 | 41 | ScoreProgress = config.ScoreProgress / total; 42 | ScoreQuality = config.ScoreQuality / total; 43 | ScoreDurability = config.ScoreDurability / total; 44 | ScoreCP = config.ScoreCP / total; 45 | ScoreSteps = config.ScoreSteps / total; 46 | 47 | ActionPool = config.ActionPool; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Solver/NodeScoresBuffer.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using System.Runtime.InteropServices; 3 | using System.Runtime.Intrinsics; 4 | 5 | namespace Craftimizer.Solver; 6 | 7 | public struct NodeScoresBuffer 8 | { 9 | [StructLayout(LayoutKind.Auto)] 10 | public struct ScoresBatch 11 | { 12 | public Vector256 ScoreSum; 13 | public Vector256 MaxScore; 14 | public Vector256 Visits; 15 | } 16 | 17 | public ScoresBatch[]? Data; 18 | public int Count { get; private set; } 19 | 20 | public void Add() 21 | { 22 | Data ??= GC.AllocateUninitializedArray(ArenaBuffer.BatchCount); 23 | var count = Count++; 24 | if ((count & ArenaBuffer.BatchSizeMask) == 0) 25 | Data[count >> ArenaBuffer.BatchSizeBits] = new(); 26 | } 27 | 28 | public readonly void Visit((int arrayIdx, int subIdx) at, float score) 29 | { 30 | ref var batch = ref Data![at.arrayIdx]; 31 | batch.ScoreSum.At(at.subIdx) += score; 32 | ref var maxScore = ref batch.MaxScore.At(at.subIdx); 33 | maxScore = Math.Max(maxScore, score); 34 | batch.Visits.At(at.subIdx)++; 35 | } 36 | 37 | public readonly int GetVisits((int arrayIdx, int subIdx) at) => 38 | Data![at.arrayIdx].Visits[at.subIdx]; 39 | } 40 | 41 | internal static class VectorUtils 42 | { 43 | public static ref T At(this ref Vector256 me, int idx) => 44 | ref Unsafe.Add(ref Unsafe.As, T>(ref me), idx); 45 | } 46 | -------------------------------------------------------------------------------- /Solver/RaphaelUtils.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Simulator.Actions; 2 | using Action = Raphael.Action; 3 | 4 | namespace Craftimizer.Solver; 5 | 6 | internal static unsafe class RaphaelUtils 7 | { 8 | public static ActionType[] ConvertRawActions(IReadOnlyList actions) 9 | { 10 | var result = new ActionType[actions.Count]; 11 | for (var i = 0; i < actions.Count; i++) 12 | result[i] = ConvertRawAction(actions[i]); 13 | return result; 14 | } 15 | 16 | public static Action[] ConvertToRawActions(IReadOnlyList actions) 17 | { 18 | var result = new List(actions.Count); 19 | foreach(var action in actions) 20 | { 21 | if (ConvertToRawAction(action) is { } a) 22 | result.Add(a); 23 | } 24 | return [.. result]; 25 | } 26 | 27 | public static ActionType ConvertRawAction(Action action) 28 | { 29 | return action switch 30 | { 31 | Action.BasicSynthesis => ActionType.BasicSynthesis, 32 | Action.BasicTouch => ActionType.BasicTouch, 33 | Action.MasterMend => ActionType.MastersMend, 34 | Action.Observe => ActionType.Observe, 35 | Action.TricksOfTheTrade => ActionType.TricksOfTheTrade, 36 | Action.WasteNot => ActionType.WasteNot, 37 | Action.Veneration => ActionType.Veneration, 38 | Action.StandardTouch => ActionType.StandardTouch, 39 | Action.GreatStrides => ActionType.GreatStrides, 40 | Action.Innovation => ActionType.Innovation, 41 | Action.WasteNot2 => ActionType.WasteNot2, 42 | Action.ByregotsBlessing => ActionType.ByregotsBlessing, 43 | Action.PreciseTouch => ActionType.PreciseTouch, 44 | Action.MuscleMemory => ActionType.MuscleMemory, 45 | Action.CarefulSynthesis => ActionType.CarefulSynthesis, 46 | Action.Manipulation => ActionType.Manipulation, 47 | Action.PrudentTouch => ActionType.PrudentTouch, 48 | Action.AdvancedTouch => ActionType.AdvancedTouch, 49 | Action.Reflect => ActionType.Reflect, 50 | Action.PreparatoryTouch => ActionType.PreparatoryTouch, 51 | Action.Groundwork => ActionType.Groundwork, 52 | Action.DelicateSynthesis => ActionType.DelicateSynthesis, 53 | Action.IntensiveSynthesis => ActionType.IntensiveSynthesis, 54 | Action.TrainedEye => ActionType.TrainedEye, 55 | Action.HeartAndSoul => ActionType.HeartAndSoul, 56 | Action.PrudentSynthesis => ActionType.PrudentSynthesis, 57 | Action.TrainedFinesse => ActionType.TrainedFinesse, 58 | Action.RefinedTouch => ActionType.RefinedTouch, 59 | Action.QuickInnovation => ActionType.QuickInnovation, 60 | Action.ImmaculateMend => ActionType.ImmaculateMend, 61 | Action.TrainedPerfection => ActionType.TrainedPerfection, 62 | _ => throw new ArgumentOutOfRangeException(nameof(action), action, $"Invalid action value {action}"), 63 | }; 64 | } 65 | 66 | public static Action? ConvertToRawAction(ActionType action) 67 | { 68 | return action switch 69 | { 70 | ActionType.BasicSynthesis => Action.BasicSynthesis, 71 | ActionType.BasicTouch => Action.BasicTouch, 72 | ActionType.MastersMend => Action.MasterMend, 73 | ActionType.Observe => Action.Observe, 74 | ActionType.TricksOfTheTrade => Action.TricksOfTheTrade, 75 | ActionType.WasteNot => Action.WasteNot, 76 | ActionType.Veneration => Action.Veneration, 77 | ActionType.StandardTouch => Action.StandardTouch, 78 | ActionType.GreatStrides => Action.GreatStrides, 79 | ActionType.Innovation => Action.Innovation, 80 | ActionType.WasteNot2 => Action.WasteNot2, 81 | ActionType.ByregotsBlessing => Action.ByregotsBlessing, 82 | ActionType.PreciseTouch => Action.PreciseTouch, 83 | ActionType.MuscleMemory => Action.MuscleMemory, 84 | ActionType.CarefulSynthesis => Action.CarefulSynthesis, 85 | ActionType.Manipulation => Action.Manipulation, 86 | ActionType.PrudentTouch => Action.PrudentTouch, 87 | ActionType.AdvancedTouch => Action.AdvancedTouch, 88 | ActionType.Reflect => Action.Reflect, 89 | ActionType.PreparatoryTouch => Action.PreparatoryTouch, 90 | ActionType.Groundwork => Action.Groundwork, 91 | ActionType.DelicateSynthesis => Action.DelicateSynthesis, 92 | ActionType.IntensiveSynthesis => Action.IntensiveSynthesis, 93 | ActionType.TrainedEye => Action.TrainedEye, 94 | ActionType.HeartAndSoul => Action.HeartAndSoul, 95 | ActionType.PrudentSynthesis => Action.PrudentSynthesis, 96 | ActionType.TrainedFinesse => Action.TrainedFinesse, 97 | ActionType.RefinedTouch => Action.RefinedTouch, 98 | ActionType.QuickInnovation => Action.QuickInnovation, 99 | ActionType.ImmaculateMend => Action.ImmaculateMend, 100 | ActionType.TrainedPerfection => Action.TrainedPerfection, 101 | _ => null, 102 | }; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Solver/RootScores.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Craftimizer.Solver; 4 | 5 | [StructLayout(LayoutKind.Auto)] 6 | public sealed class RootScores 7 | { 8 | public float MaxScore; 9 | public int Visits; 10 | 11 | public void Visit(float score) 12 | { 13 | MaxScore = Math.Max(MaxScore, score); 14 | Visits++; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Solver/SimulationNode.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Simulator; 2 | using Craftimizer.Simulator.Actions; 3 | using System.Diagnostics.Contracts; 4 | using System.Runtime.CompilerServices; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace Craftimizer.Solver; 8 | 9 | [StructLayout(LayoutKind.Auto)] 10 | public struct SimulationNode(in SimulationState state, ActionType? action, CompletionState completionState, ActionSet actions) 11 | { 12 | public readonly SimulationState State = state; 13 | public readonly ActionType? Action = action; 14 | public readonly CompletionState SimulationCompletionState = completionState; 15 | 16 | public ActionSet AvailableActions = actions; 17 | 18 | public readonly CompletionState CompletionState => GetCompletionState(SimulationCompletionState, AvailableActions); 19 | 20 | public readonly bool IsComplete => CompletionState != CompletionState.Incomplete; 21 | 22 | public static CompletionState GetCompletionState(CompletionState simCompletionState, ActionSet actions) => 23 | actions.IsEmpty && simCompletionState == CompletionState.Incomplete ? 24 | CompletionState.NoMoreActions : 25 | simCompletionState; 26 | 27 | public readonly float? CalculateScore(in MCTSConfig config) => 28 | CalculateScoreForState(State, SimulationCompletionState, config); 29 | 30 | public static float? CalculateScoreForState(in SimulationState state, CompletionState completionState, in MCTSConfig config) 31 | { 32 | if (completionState != CompletionState.ProgressComplete) 33 | return null; 34 | 35 | var stepScore = 1f - ((float)(state.ActionCount + 1) / config.MaxStepCount); 36 | 37 | if (state.Input.Recipe.MaxQuality == 0) 38 | return stepScore; 39 | 40 | [Pure] 41 | [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] 42 | static float Apply(float multiplier, float value, float target) => 43 | multiplier * (target > 0 ? Math.Clamp(value / target, 0, 1) : 1); 44 | 45 | [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] 46 | static float ApplyNondominant(float multiplier, float dominance, float value, float target) => 47 | Apply(float.Lerp(multiplier, 0, dominance), value, target); 48 | 49 | [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] 50 | static float ApplyNondominant2(float multiplier, float dominance, float score) => 51 | float.Lerp(multiplier, 0, dominance) * score; 52 | 53 | [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] 54 | static float ApplyDominant(float multiplier, float dominance, float value, float target) => 55 | Apply(float.Lerp(multiplier, 1, dominance), value, target); 56 | 57 | var qualityDominance = state.ActionCount / config.MaxStepCount; 58 | 59 | var progressScore = ApplyNondominant( 60 | config.ScoreProgress, 61 | qualityDominance, 62 | state.Progress, 63 | state.Input.Recipe.MaxProgress 64 | ); 65 | 66 | var qualityScore = ApplyDominant( 67 | config.ScoreQuality, 68 | qualityDominance, 69 | state.Quality, 70 | state.Input.Recipe.MaxQuality 71 | ); 72 | 73 | var durabilityScore = ApplyNondominant( 74 | config.ScoreDurability, 75 | qualityDominance, 76 | state.Durability, 77 | state.Input.Recipe.MaxDurability 78 | ); 79 | 80 | var cpScore = ApplyNondominant( 81 | config.ScoreCP, 82 | qualityDominance, 83 | state.CP, 84 | state.Input.Stats.CP 85 | ); 86 | 87 | var fewerStepsScore = ApplyNondominant2(config.ScoreSteps, qualityDominance, stepScore); 88 | 89 | return progressScore + qualityScore + durabilityScore + cpScore + fewerStepsScore; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Solver/Simulator.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Simulator; 2 | using Craftimizer.Simulator.Actions; 3 | using System.Diagnostics.Contracts; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace Craftimizer.Solver; 7 | 8 | internal sealed class Simulator : SimulatorNoRandom 9 | { 10 | private readonly (BaseAction Data, ActionType Action)[] actionPoolObjects; 11 | private readonly int maxStepCount; 12 | 13 | public override CompletionState CompletionState 14 | { 15 | get 16 | { 17 | var b = base.CompletionState; 18 | if (b == CompletionState.Incomplete && (ActionCount + 1) >= maxStepCount) 19 | return CompletionState.MaxActionCountReached; 20 | return b; 21 | } 22 | } 23 | 24 | public Simulator(ActionType[] actionPool, int maxStepCount, SimulationState? filteringState = null) 25 | { 26 | var pool = actionPool.Select(x => (x.Base(), x)); 27 | if (filteringState is { } state) 28 | { 29 | State = state; 30 | pool = pool.Where(x => x.Item1.IsPossible(this)); 31 | } 32 | actionPoolObjects = [.. pool.OrderBy(x => x.x)]; 33 | this.maxStepCount = maxStepCount; 34 | } 35 | 36 | // https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/craft_state.rs#L146 37 | [Pure] 38 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 39 | private bool CouldUseAction(BaseAction baseAction) 40 | { 41 | if (CalculateSuccessRate(baseAction.SuccessRate(this)) != 100) 42 | return false; 43 | 44 | // don't allow quality moves at max quality 45 | if (Quality >= Input.Recipe.MaxQuality && baseAction.IncreasesQuality) 46 | return false; 47 | 48 | return baseAction.CouldUse(this); 49 | } 50 | 51 | // https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/craft_state.rs#L146 52 | [Pure] 53 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 54 | // It's just a bunch of if statements, I would assume this is actually quite simple to follow 55 | #pragma warning disable MA0051 // Method is too long 56 | private bool ShouldUseAction(ActionType action, BaseAction baseAction) 57 | #pragma warning restore MA0051 // Method is too long 58 | { 59 | if (CalculateSuccessRate(baseAction.SuccessRate(this)) != 100) 60 | return false; 61 | 62 | // don't allow quality moves at max quality 63 | if (Quality >= Input.Recipe.MaxQuality && baseAction.IncreasesQuality) 64 | return false; 65 | 66 | // always use Trained Eye if it's available 67 | if (action == ActionType.TrainedEye) 68 | return baseAction.CouldUse(this); 69 | 70 | var isDifficult = Input.Stats.Level - Input.Recipe.ClassJobLevel < 10 || Input.Recipe.IsExpert; 71 | 72 | // don't allow quality moves under Muscle Memory for difficult crafts 73 | if (isDifficult && 74 | HasEffect(EffectType.MuscleMemory) && 75 | baseAction.IncreasesQuality) 76 | return false; 77 | 78 | // use First Turn actions if it's available and the craft is difficult 79 | if (IsFirstStep && 80 | Input.Stats.Level >= 69 && 81 | isDifficult && 82 | baseAction.Category != ActionCategory.FirstTurn && 83 | CP >= 6) 84 | return false; 85 | 86 | // don't allow combo actions if the combo is already in progress 87 | if (ActionStates.Combo != ActionProc.None && 88 | (action is ActionType.StandardTouchCombo or ActionType.AdvancedTouchCombo or ActionType.RefinedTouchCombo)) 89 | return false; 90 | 91 | // when combo'd, only allow Advanced Touch 92 | if (ActionStates.Combo == ActionProc.AdvancedTouch && action is not ActionType.AdvancedTouch) 93 | return false; 94 | 95 | // don't allow pure quality moves under Veneration 96 | if (HasEffect(EffectType.Veneration) && 97 | !baseAction.IncreasesProgress && 98 | baseAction.IncreasesQuality) 99 | return false; 100 | 101 | var durability = CalculateDurabilityCost(baseAction.DurabilityCost); 102 | 103 | // don't allow pure quality moves when it won't be able to finish the craft 104 | if (!baseAction.IncreasesProgress && durability >= Durability) 105 | return false; 106 | 107 | if (baseAction.IncreasesProgress) 108 | { 109 | var progressIncrease = CalculateProgressGain(baseAction.Efficiency(this)); 110 | var wouldFinish = Progress + progressIncrease >= Input.Recipe.MaxProgress; 111 | 112 | if (wouldFinish) 113 | { 114 | // don't allow finishing the craft if there is significant quality remaining 115 | if (Quality * 5 < Input.Recipe.MaxQuality) 116 | return false; 117 | } 118 | else 119 | { 120 | // don't allow pure progress moves under Innovation, if it wouldn't finish the craft 121 | if (HasEffect(EffectType.Innovation) && 122 | !baseAction.IncreasesQuality && 123 | baseAction.IncreasesProgress) 124 | return false; 125 | } 126 | } 127 | 128 | if (action is ActionType.ByregotsBlessing && 129 | GetEffectStrength(EffectType.InnerQuiet) <= 1) 130 | return false; 131 | 132 | // use of Waste Not should be efficient 133 | if ((action is ActionType.WasteNot or ActionType.WasteNot2) && 134 | (HasEffect(EffectType.WasteNot) || HasEffect(EffectType.WasteNot2))) 135 | return false; 136 | 137 | // don't Observe when Advanced Touch is impossible (7 + 18) 138 | if (action is ActionType.Observe && CP < 25) 139 | return false; 140 | 141 | // don't allow Refined Touch without a combo 142 | if (action is ActionType.RefinedTouch && 143 | ActionStates.Combo != ActionProc.UsedBasicTouch) 144 | return false; 145 | 146 | // don't allow Immaculate Mends that are too inefficient 147 | if (action is ActionType.ImmaculateMend && 148 | (Input.Recipe.MaxDurability - durability <= 45 || HasEffect(EffectType.Manipulation))) 149 | return false; 150 | 151 | // don't allow buffs too early 152 | if (action is ActionType.MastersMend && 153 | Input.Recipe.MaxDurability - durability < 25) 154 | return false; 155 | 156 | if (action is ActionType.Manipulation && 157 | HasEffect(EffectType.Manipulation)) 158 | return false; 159 | 160 | if (action is ActionType.GreatStrides && 161 | HasEffect(EffectType.GreatStrides)) 162 | return false; 163 | 164 | if ((action is ActionType.Veneration or ActionType.Innovation) && 165 | (GetEffectDuration(EffectType.Veneration) > 1 || GetEffectDuration(EffectType.Innovation) > 1)) 166 | return false; 167 | 168 | if (action is ActionType.QuickInnovation && 169 | Quality * 3 <= Input.Recipe.MaxQuality) 170 | return false; 171 | 172 | return baseAction.CouldUse(this); 173 | } 174 | 175 | // https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/craft_state.rs#L137 176 | public ActionSet AvailableActionsHeuristic(bool strict) 177 | { 178 | if (IsComplete) 179 | return new(); 180 | 181 | var ret = new ActionSet(); 182 | if (strict) 183 | { 184 | foreach (var (data, action) in actionPoolObjects) 185 | { 186 | if (ShouldUseAction(action, data)) 187 | ret.AddAction(action); 188 | } 189 | 190 | // If Trained Eye is possible, *always* use Trained Eye 191 | if (ret.HasAction(ActionType.TrainedEye)) 192 | { 193 | ret = new(); 194 | ret.AddAction(ActionType.TrainedEye); 195 | } 196 | } 197 | else 198 | { 199 | foreach (var (data, action) in actionPoolObjects) 200 | if (CouldUseAction(data)) 201 | ret.AddAction(action); 202 | } 203 | 204 | return ret; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Solver/SolverConfig.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Simulator.Actions; 2 | using System.Collections.Frozen; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Craftimizer.Solver; 6 | 7 | public enum SolverAlgorithm 8 | { 9 | Oneshot, 10 | OneshotForked, 11 | Stepwise, 12 | StepwiseForked, 13 | StepwiseGenetic, 14 | Raphael, 15 | } 16 | 17 | [StructLayout(LayoutKind.Auto)] 18 | public readonly record struct SolverConfig 19 | { 20 | // MCTS configuration 21 | public int Iterations { get; init; } 22 | public int MaxIterations { get; init; } 23 | public float MaxScoreWeightingConstant { get; init; } 24 | public float ExplorationConstant { get; init; } 25 | public int MaxStepCount { get; init; } 26 | public int MaxRolloutStepCount { get; init; } 27 | public int ForkCount { get; init; } 28 | public int FurcatedActionCount { get; init; } 29 | public bool StrictActions { get; init; } 30 | 31 | // MCTS score weights 32 | public float ScoreProgress { get; init; } 33 | public float ScoreQuality { get; init; } 34 | public float ScoreDurability { get; init; } 35 | public float ScoreCP { get; init; } 36 | public float ScoreSteps { get; init; } 37 | 38 | // Raphael/A* configuration 39 | public bool Adversarial { get; init; } 40 | public bool BackloadProgress { get; init; } 41 | public bool MinimizeSteps { get; init; } 42 | 43 | public int MaxThreadCount { get; init; } 44 | public ActionType[] ActionPool { get; init; } 45 | public SolverAlgorithm Algorithm { get; init; } 46 | 47 | public SolverConfig() 48 | { 49 | Iterations = 100_000; 50 | MaxIterations = 1_500_000; 51 | MaxScoreWeightingConstant = 0.1f; 52 | ExplorationConstant = 4; 53 | MaxStepCount = 30; 54 | MaxRolloutStepCount = 99; 55 | // Use 75% of all cores if less than 12 cores are available, otherwise use all but 4 cores. Keep at least 1 core. 56 | MaxThreadCount = Math.Max(1, Math.Max(Environment.ProcessorCount - 4, (int)MathF.Floor(Environment.ProcessorCount * 0.75f))); 57 | // Use 32 forks at minimum, or the number of cores, whichever is higher. 58 | ForkCount = Math.Max(Environment.ProcessorCount, 32); 59 | FurcatedActionCount = ForkCount / 2; 60 | StrictActions = true; 61 | 62 | ScoreProgress = 10; 63 | ScoreQuality = 80; 64 | ScoreDurability = 2; 65 | ScoreCP = 3; 66 | ScoreSteps = 5; 67 | 68 | ActionPool = DeterministicActionPool; 69 | Algorithm = SolverAlgorithm.StepwiseGenetic; 70 | } 71 | 72 | public static ActionType[] OptimizeActionPool(IEnumerable actions) => 73 | [.. actions.Order()]; 74 | 75 | public SolverConfig FilterSpecialistActions() => 76 | this with { ActionPool = ActionPool.Where(action => !SpecialistActions.Contains(action)).ToArray() }; 77 | 78 | public static readonly ActionType[] DeterministicActionPool = OptimizeActionPool(new[] 79 | { 80 | ActionType.MuscleMemory, 81 | ActionType.Reflect, 82 | ActionType.TrainedEye, 83 | 84 | ActionType.BasicSynthesis, 85 | ActionType.CarefulSynthesis, 86 | ActionType.Groundwork, 87 | ActionType.DelicateSynthesis, 88 | ActionType.PrudentSynthesis, 89 | 90 | ActionType.BasicTouch, 91 | ActionType.StandardTouch, 92 | ActionType.ByregotsBlessing, 93 | ActionType.PrudentTouch, 94 | ActionType.AdvancedTouch, 95 | ActionType.PreparatoryTouch, 96 | ActionType.TrainedFinesse, 97 | ActionType.RefinedTouch, 98 | 99 | ActionType.MastersMend, 100 | ActionType.WasteNot, 101 | ActionType.WasteNot2, 102 | ActionType.Manipulation, 103 | ActionType.ImmaculateMend, 104 | ActionType.TrainedPerfection, 105 | 106 | ActionType.Veneration, 107 | ActionType.GreatStrides, 108 | ActionType.Innovation, 109 | ActionType.QuickInnovation, 110 | 111 | ActionType.Observe, 112 | ActionType.HeartAndSoul, 113 | 114 | ActionType.StandardTouchCombo, 115 | ActionType.AdvancedTouchCombo, 116 | ActionType.ObservedAdvancedTouchCombo, 117 | ActionType.RefinedTouchCombo, 118 | }); 119 | 120 | // Same as deterministic, but with condition-specific actions added 121 | public static readonly ActionType[] RandomizedActionPool = OptimizeActionPool(new[] 122 | { 123 | ActionType.MuscleMemory, 124 | ActionType.Reflect, 125 | ActionType.TrainedEye, 126 | 127 | ActionType.BasicSynthesis, 128 | ActionType.CarefulSynthesis, 129 | ActionType.Groundwork, 130 | ActionType.DelicateSynthesis, 131 | ActionType.IntensiveSynthesis, 132 | ActionType.PrudentSynthesis, 133 | 134 | ActionType.BasicTouch, 135 | ActionType.StandardTouch, 136 | ActionType.ByregotsBlessing, 137 | ActionType.PreciseTouch, 138 | ActionType.PrudentTouch, 139 | ActionType.AdvancedTouch, 140 | ActionType.PreparatoryTouch, 141 | ActionType.TrainedFinesse, 142 | ActionType.RefinedTouch, 143 | 144 | ActionType.MastersMend, 145 | ActionType.WasteNot, 146 | ActionType.WasteNot2, 147 | ActionType.Manipulation, 148 | ActionType.ImmaculateMend, 149 | ActionType.TrainedPerfection, 150 | 151 | ActionType.Veneration, 152 | ActionType.GreatStrides, 153 | ActionType.Innovation, 154 | ActionType.QuickInnovation, 155 | 156 | ActionType.Observe, 157 | ActionType.HeartAndSoul, 158 | ActionType.TricksOfTheTrade, 159 | 160 | ActionType.StandardTouchCombo, 161 | ActionType.AdvancedTouchCombo, 162 | ActionType.ObservedAdvancedTouchCombo, 163 | ActionType.RefinedTouchCombo, 164 | }); 165 | 166 | public static readonly FrozenSet InefficientActions = 167 | new[] 168 | { 169 | ActionType.CarefulObservation, 170 | ActionType.FinalAppraisal 171 | }.ToFrozenSet(); 172 | 173 | public static readonly FrozenSet RiskyActions = 174 | new[] 175 | { 176 | ActionType.RapidSynthesis, 177 | ActionType.HastyTouch, 178 | ActionType.DaringTouch, 179 | }.ToFrozenSet(); 180 | 181 | public static readonly FrozenSet SpecialistActions = 182 | new[] 183 | { 184 | ActionType.CarefulObservation, 185 | ActionType.HeartAndSoul, 186 | ActionType.QuickInnovation, 187 | }.ToFrozenSet(); 188 | 189 | public static readonly SolverConfig RecipeNoteDefault = new SolverConfig() with 190 | { 191 | 192 | }; 193 | 194 | public static readonly SolverConfig EditorDefault = new SolverConfig() with 195 | { 196 | Algorithm = SolverAlgorithm.Raphael, 197 | Adversarial = true 198 | }; 199 | 200 | public static readonly SolverConfig SynthHelperDefault = new SolverConfig() with 201 | { 202 | ActionPool = RandomizedActionPool 203 | }; 204 | } 205 | -------------------------------------------------------------------------------- /Solver/SolverSolution.cs: -------------------------------------------------------------------------------- 1 | using Craftimizer.Simulator; 2 | using Craftimizer.Simulator.Actions; 3 | 4 | namespace Craftimizer.Solver; 5 | 6 | public readonly record struct SolverSolution { 7 | private readonly List actions = null!; 8 | public readonly IReadOnlyList Actions { get => actions; init => ActionEnumerable = value; } 9 | public readonly IEnumerable ActionEnumerable { init => actions = value.ToList(); } 10 | public readonly SimulationState State { get; init; } 11 | 12 | public SolverSolution(IEnumerable actions, in SimulationState state) 13 | { 14 | ActionEnumerable = actions; 15 | State = state; 16 | } 17 | 18 | public void Deconstruct(out IReadOnlyList actions, out SimulationState state) 19 | { 20 | actions = Actions; 21 | state = State; 22 | } 23 | 24 | internal static IEnumerable SanitizeCombo(ActionType action) 25 | { 26 | if (action.Base() is BaseComboAction combo) 27 | { 28 | foreach (var a in SanitizeCombo(combo.ActionTypeA)) 29 | yield return a; 30 | foreach (var b in SanitizeCombo(combo.ActionTypeB)) 31 | yield return b; 32 | } 33 | else 34 | yield return action; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Test/Craftimizer.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | false 8 | true 9 | x64 10 | Debug;Release;Deterministic 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | $(DefineConstants);IS_DETERMINISTIC 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Test/Solver/ActionSet.cs: -------------------------------------------------------------------------------- 1 | namespace Craftimizer.Test.Solver; 2 | 3 | [TestClass] 4 | public class ActionSetTests 5 | { 6 | [TestMethod] 7 | public void TestSize() 8 | { 9 | var set = new ActionSet(); 10 | Assert.IsTrue(set.IsEmpty); 11 | Assert.AreEqual(0, set.Count); 12 | 13 | set.AddAction(ActionType.BasicSynthesis); 14 | set.AddAction(ActionType.WasteNot2); 15 | 16 | Assert.AreEqual(2, set.Count); 17 | Assert.IsFalse(set.IsEmpty); 18 | 19 | set.RemoveAction(ActionType.BasicSynthesis); 20 | set.RemoveAction(ActionType.WasteNot2); 21 | 22 | Assert.IsTrue(set.IsEmpty); 23 | Assert.AreEqual(0, set.Count); 24 | } 25 | 26 | [TestMethod] 27 | public void TestAddRemove() 28 | { 29 | var set = new ActionSet(); 30 | 31 | Assert.IsTrue(set.AddAction(ActionType.BasicSynthesis)); 32 | Assert.IsFalse(set.AddAction(ActionType.BasicSynthesis)); 33 | 34 | Assert.IsTrue(set.RemoveAction(ActionType.BasicSynthesis)); 35 | Assert.IsFalse(set.RemoveAction(ActionType.BasicSynthesis)); 36 | 37 | Assert.IsTrue(set.AddAction(ActionType.BasicSynthesis)); 38 | Assert.IsTrue(set.AddAction(ActionType.WasteNot2)); 39 | 40 | Assert.IsTrue(set.RemoveAction(ActionType.BasicSynthesis)); 41 | Assert.IsTrue(set.RemoveAction(ActionType.WasteNot2)); 42 | } 43 | 44 | [TestMethod] 45 | public void TestHasAction() 46 | { 47 | var set = new ActionSet(); 48 | 49 | set.AddAction(ActionType.BasicSynthesis); 50 | 51 | Assert.IsTrue(set.HasAction(ActionType.BasicSynthesis)); 52 | Assert.IsFalse(set.HasAction(ActionType.WasteNot2)); 53 | 54 | set.AddAction(ActionType.WasteNot2); 55 | Assert.IsTrue(set.HasAction(ActionType.BasicSynthesis)); 56 | Assert.IsTrue(set.HasAction(ActionType.WasteNot2)); 57 | 58 | set.RemoveAction(ActionType.BasicSynthesis); 59 | Assert.IsFalse(set.HasAction(ActionType.BasicSynthesis)); 60 | Assert.IsTrue(set.HasAction(ActionType.WasteNot2)); 61 | } 62 | 63 | [TestMethod] 64 | public void TestElementAt() 65 | { 66 | var set = new ActionSet(); 67 | 68 | set.AddAction(ActionType.BasicSynthesis); 69 | set.AddAction(ActionType.ByregotsBlessing); 70 | set.AddAction(ActionType.DelicateSynthesis); 71 | set.AddAction(ActionType.Reflect); 72 | 73 | Assert.AreEqual(4, set.Count); 74 | 75 | Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(0)); 76 | Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(1)); 77 | Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(2)); 78 | Assert.AreEqual(ActionType.Reflect, set.ElementAt(3)); 79 | 80 | set.RemoveAction(ActionType.Reflect); 81 | 82 | Assert.AreEqual(3, set.Count); 83 | 84 | Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(0)); 85 | Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(1)); 86 | Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(2)); 87 | } 88 | 89 | [TestMethod] 90 | public void TestRandomIndex() 91 | { 92 | #if IS_DETERMINISTIC 93 | Assert.Inconclusive("Craftimizer is currently built for determinism; all random actions are not actually random."); 94 | #endif 95 | 96 | var actions = new[] 97 | { 98 | ActionType.BasicTouch, 99 | ActionType.BasicSynthesis, 100 | ActionType.GreatStrides, 101 | ActionType.TrainedFinesse, 102 | }; 103 | 104 | var set = new ActionSet(); 105 | foreach(var action in actions) 106 | set.AddAction(action); 107 | 108 | var counts = new Dictionary(); 109 | var rng = new Random(0); 110 | for (var i = 0; i < 100; i++) 111 | { 112 | var action = set.SelectRandom(rng); 113 | 114 | CollectionAssert.Contains(actions, action); 115 | 116 | counts[action] = counts.GetValueOrDefault(action) + 1; 117 | } 118 | 119 | foreach (var action in actions) 120 | Assert.IsTrue(counts.GetValueOrDefault(action) > 0); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Test/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | global using Craftimizer.Solver; 3 | global using Craftimizer.Simulator; 4 | global using Craftimizer.Simulator.Actions; 5 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorkingRobot/Craftimizer/fd1651cef90acaa0636316851a81b0137a7c8f86/icon.png --------------------------------------------------------------------------------