From 3648cb585d3c4690923139486eb72fe8e862f7b3 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 26 Sep 2023 16:05:58 -0700 Subject: [PATCH 01/58] Use exp array idx for job levels --- Craftimizer/SimulatorUtils.cs | 3 +++ Craftimizer/Utils/RecipeNote.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Craftimizer/SimulatorUtils.cs b/Craftimizer/SimulatorUtils.cs index 1ca6aea..27eeb87 100644 --- a/Craftimizer/SimulatorUtils.cs +++ b/Craftimizer/SimulatorUtils.cs @@ -142,6 +142,9 @@ public static byte GetClassJobIndex(this ClassJob me) => _ => null }; + public static sbyte GetExpArrayIdx(this ClassJob me) => + LuminaSheets.ClassJobSheet.GetRow(me.GetClassJobIndex())!.ExpArrayIndex; + public static string GetName(this ClassJob classJob) { var job = LuminaSheets.ClassJobSheet.GetRow(classJob.GetClassJobIndex())!; diff --git a/Craftimizer/Utils/RecipeNote.cs b/Craftimizer/Utils/RecipeNote.cs index 315bbcd..9178682 100644 --- a/Craftimizer/Utils/RecipeNote.cs +++ b/Craftimizer/Utils/RecipeNote.cs @@ -122,7 +122,7 @@ private void CalculateStats() Table = Recipe.RecipeLevelTable.Value!; Info = CreateInfo(); ClassJob = (ClassJob)Recipe.CraftType.Row; - CharacterLevel = PlayerState.Instance()->ClassJobLevelArray[ClassJob.GetClassJobIndex()]; + CharacterLevel = PlayerState.Instance()->ClassJobLevelArray[ClassJob.GetExpArrayIdx()]; CanUseManipulation = ActionManager.CanUseActionOnTarget(ActionType.Manipulation.GetId(ClassJob), (GameObject*)Service.ClientState.LocalPlayer!.Address); HQIngredientCount = Recipe.UnkData5 .Where(i => From 9dc83be1553601f263a094a8c146aa39200692ae Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 26 Sep 2023 16:06:53 -0700 Subject: [PATCH 02/58] Fix crash when rendering macros and unable to craft recipe --- Craftimizer/Windows/CraftingLog.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Craftimizer/Windows/CraftingLog.cs b/Craftimizer/Windows/CraftingLog.cs index 04691e9..1654dd2 100644 --- a/Craftimizer/Windows/CraftingLog.cs +++ b/Craftimizer/Windows/CraftingLog.cs @@ -145,8 +145,7 @@ private void CalculateCharacterStats() }; CharacterCannotCraftReason = Config.OverrideUncraftability ? CannotCraftReason.OK : CanCraftRecipe(CharacterEquipment, CharacterStatsConsumable); - if (CharacterCannotCraftReason == CannotCraftReason.OK) - CharacterSimulationInput = new(CharacterStatsConsumable, RecipeUtils.Info, StartingQuality, Random); + CharacterSimulationInput = new(CharacterStatsConsumable, RecipeUtils.Info, StartingQuality, Random); } public override void Draw() From 36b9e4fb6de05fc25675c2ce304f710227763166 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Wed, 27 Sep 2023 23:36:27 -0700 Subject: [PATCH 03/58] Add HQ consumables to crafting log --- Craftimizer/Windows/CraftingLog.cs | 40 +++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/Craftimizer/Windows/CraftingLog.cs b/Craftimizer/Windows/CraftingLog.cs index 1654dd2..d4d01b5 100644 --- a/Craftimizer/Windows/CraftingLog.cs +++ b/Craftimizer/Windows/CraftingLog.cs @@ -38,7 +38,7 @@ public unsafe class CraftingLog : Window // If not relative, increase stat by Value, and ignore Max [StructLayout(LayoutKind.Auto)] private record struct FoodStat(bool IsRelative, sbyte Value, short Max, sbyte ValueHQ, short MaxHQ); - private sealed record Food(Item Item, string Name, FoodStat? Craftsmanship, FoodStat? Control, FoodStat? CP); + private sealed record Food(Item Item, string Name, string NameHQ, FoodStat? Craftsmanship, FoodStat? Control, FoodStat? CP); private static Food[] FoodItems { get; } private static Food[] MedicineItems { get; } @@ -106,7 +106,8 @@ static CraftingLog() if (craftsmanship != null || control != null || cp != null) { - var food = new Food(item, item.Name.ToDalamudString().TextValue ?? $"Unknown ({item.RowId})", craftsmanship, control, cp); + var name = item.Name.ToDalamudString().TextValue ?? $"Unknown ({item.RowId})"; + var food = new Food(item, name, $"{name} (HQ)", craftsmanship, control, cp); if (isFood) foods.Add(food); if (isMedicine) @@ -215,26 +216,53 @@ private void DrawCraftParameters() ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, LeftSideWidth - 120); ImGui.TableNextColumn(); - if (ImGui.BeginCombo("Food", SelectedFood?.Name ?? "None")) + if (ImGui.BeginCombo("Food", SelectedFood != null ? (SelectedFoodHQ ? SelectedFood.NameHQ : SelectedFood.Name) : "None")) { if (ImGui.Selectable("None", SelectedFood == null)) + { SelectedFood = null; + SelectedFoodHQ = false; + } foreach (var food in FoodItems) - if (ImGui.Selectable(food.Name, food == SelectedFood)) + { + if (ImGui.Selectable(food.Name, food == SelectedFood && !SelectedFoodHQ)) + { + SelectedFood = food; + SelectedFoodHQ = false; + } + else if (ImGui.Selectable($"{food.Name} (HQ)", food == SelectedFood && SelectedFoodHQ)) + { SelectedFood = food; + SelectedFoodHQ = true; + } + } ImGui.EndCombo(); } - if (ImGui.BeginCombo("Medicine", SelectedMedicine?.Name ?? "None")) + if (ImGui.BeginCombo("Medicine", SelectedMedicine != null ? (SelectedMedicineHQ ? SelectedMedicine.NameHQ : SelectedMedicine.Name) : "None")) { if (ImGui.Selectable("None", SelectedMedicine == null)) + { SelectedMedicine = null; + SelectedMedicineHQ = false; + } + foreach (var food in MedicineItems) - if (ImGui.Selectable(food.Name, food == SelectedMedicine)) + { + if (ImGui.Selectable(food.Name, food == SelectedMedicine && !SelectedMedicineHQ)) + { SelectedMedicine = food; + SelectedMedicineHQ = false; + } + else if (ImGui.Selectable($"{food.Name} (HQ)", food == SelectedMedicine && SelectedMedicineHQ)) + { + SelectedMedicine = food; + SelectedMedicineHQ = true; + } + } ImGui.EndCombo(); } From d9e78166d974eccf59506da9b9a939226493cd85 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sun, 1 Oct 2023 03:43:11 -0700 Subject: [PATCH 04/58] Updates for v9, begin ui rework --- Craftimizer/Craftimizer.csproj | 10 + Craftimizer/Graphics/collectible_badge.png | Bin 0 -> 816 bytes Craftimizer/Graphics/expert.png | Bin 0 -> 1963 bytes Craftimizer/Graphics/expert_badge.png | Bin 0 -> 896 bytes Craftimizer/Graphics/no_manip.png | Bin 0 -> 9422 bytes Craftimizer/Graphics/specialist.png | Bin 0 -> 1992 bytes Craftimizer/Graphics/splendorous.png | Bin 0 -> 1270 bytes Craftimizer/Icons.cs | 19 - Craftimizer/ImGuiUtils.cs | 21 +- Craftimizer/LuminaSheets.cs | 3 + Craftimizer/Plugin.cs | 34 +- Craftimizer/Service.cs | 31 +- Craftimizer/SimulatorUtils.cs | 29 +- Craftimizer/Utils/Gearsets.cs | 9 +- Craftimizer/Utils/IconManager.cs | 66 +++ Craftimizer/Utils/RecipeNote.cs | 46 ++ Craftimizer/Utils/SqText.cs | 33 ++ Craftimizer/Windows/RecipeNote.cs | 542 +++++++++++++++++++++ Craftimizer/Windows/Settings.cs | 2 +- Craftimizer/Windows/SimulatorDrawer.cs | 2 +- 20 files changed, 785 insertions(+), 62 deletions(-) create mode 100644 Craftimizer/Graphics/collectible_badge.png create mode 100644 Craftimizer/Graphics/expert.png create mode 100644 Craftimizer/Graphics/expert_badge.png create mode 100644 Craftimizer/Graphics/no_manip.png create mode 100644 Craftimizer/Graphics/specialist.png create mode 100644 Craftimizer/Graphics/splendorous.png delete mode 100644 Craftimizer/Icons.cs create mode 100644 Craftimizer/Utils/IconManager.cs create mode 100644 Craftimizer/Utils/SqText.cs create mode 100644 Craftimizer/Windows/RecipeNote.cs diff --git a/Craftimizer/Craftimizer.csproj b/Craftimizer/Craftimizer.csproj index 1b2e168..c021083 100644 --- a/Craftimizer/Craftimizer.csproj +++ b/Craftimizer/Craftimizer.csproj @@ -28,6 +28,12 @@ + + + + + + @@ -50,6 +56,10 @@ $(DalamudLibPath)Dalamud.dll false + + $(DalamudLibPath)Dalamud.Interface.dll + false + $(DalamudLibPath)ImGui.NET.dll false diff --git a/Craftimizer/Graphics/collectible_badge.png b/Craftimizer/Graphics/collectible_badge.png new file mode 100644 index 0000000000000000000000000000000000000000..5b3b890f6fd6b5b3425a6708cc80bfd0dac358a0 GIT binary patch literal 816 zcmV-01JC@4P) z20%M#AF_cxP~2biN~sT8>CajEjS+}#kALlV{H?QRue}xDKixSZa$-#gac~?1JbnHY z6BumUqDqSJO$dNe3avC+>2VB$_x_{r`V%7BTD&U|^I1D=HCqI00f7lnB}Hw%20*b; z#8`u|25T+TN2e)Q%2X=O8UBKoqpndR*#fv*LWPn5ZM_2QoLVxgGa8*6AjYZ9~q z%9V0v5%2v+v9ope9dLNqMxChB3nnm_z!0v7NF}LWs{X9dyEx~1H~HR0&d8onv509K zj4@baky0XOE2S3<8i70IO}kM!@YC^+e#^^&)1V@(>4UH0OuT1N~#yC zlNVT6SirU|b64ij<=qd`Sw~~JF;pLOwK;UD*S6E?;9fWy4;#M;4kM6DQ+s=R8{4+P zZVJUnC6P-0u@9wH5Ckx8`0Y@W99Vu`$ui79PUKFB)3dO;dVhAu?<@+$G17_*5QuTk uL4Nl*2?Lhm^a5Mn`!Ju&Uq5r^%99^&QD#T%#d%YDB9 literal 0 HcmV?d00001 diff --git a/Craftimizer/Graphics/expert.png b/Craftimizer/Graphics/expert.png new file mode 100644 index 0000000000000000000000000000000000000000..db26b79292ac4e7246b98389524528065f9fdc18 GIT binary patch literal 1963 zcmZ`(YgAKL7Cu*mBq%sSTOJ~T+z>(%G~k1WBI()fr0`OAdm=($T)bYq!3Cd1O*i*LlIb&lPu)!KtcpkSn|a6@mUZhCnIoMLM2RB zinaM{86?Np5Csju93x@i6cnCF0)t)vkY zG(7APkU^2hDxHj@q~H}4ypl?gkpK*J7%E2*I2Yzc=;~tB5~{++q2NL+9it@yRVXS& z0gnpg9smMi!2bUPkb1&Sh((|tsFUjit@^JRSi-6lAftlmYOxe16rkRR_U6Vy223rA znI@+qB@6$J2%KZEFh{j*nR4(SV_Zd6hlsT z?Bb0f6o`ekK(&xoO2j-=(D8Ctq))VeuA!)efrr+?5)=tTwZ|Q9e^VDhdmFIk%ZKRy2DOLUVwt;NE-AO@?!#sNK-gfNhiS0e8>#F!uYYT z0(L?Ji%?Lv1ike4Q+zPr{l}NxHJbp;|brFOLP? zxGRhqKgI9jheKOhq8WGjF{XwS`hw)UqL?Sj)Rnf1H_gR5dBV!dN>|8(C1|%s!j6px zt8ML8t#P6GZzmI+y*O#f;qO@B(crAImQ&n6Z*r7&k3Q_I8Z|BCXjN9Eq@&dX<0kW{ zzP`O{mI@%4zFUGa-~?6~{3+*xy*f z7=AzufSm+;kmp>Y3?AYn5iy~-32Vf{%5wcHds_f3Y$8JTaQwCPkDesk#kur~j=04! zYP`W=TlYz&}Fi^x;HWyq^D|V05vb5pUJIn{+KE-k4dKiI4Kx z9OyUka&GzMnu(n!?-?8KPAvA2@W(tSBM6^7N08>OCbzwe$vu{f>*s9G4@{o<_~T3Gf3Uu6FKH*U{@Gmi>6MDo4VFVX>G^S% z4{Q5V*A7RUiBZpl%JiAqt7E8&f9(}-GPLZyB)gPl8)ho9%JDrow5xc1 zB=U>oktaF&dJ5`c5*luT^+4F-i#lZb+oc|TiHs;nNRSUTzr?YA{YVdwk zUE|brGRQb|tBI3c!TpI;FY+0#S$kvk>2s~osNP1zcQ|`$;D}Jc%y53PYsbO&)Rh~) z+WPDY_!O>c;TWPid5)%>9F#?B;h8$_4UhrJhhGIbkRrSwo+A}nphRl zn!H$-I+#lxaom@)cVn=>U+$bU+3&q~`TZ+BvP3K6OkxRrh~@MJFXuIjd+o1fZT?AhY|=}|xNI*y z@p#(6=VM>+UZ=CTs*C4iIZ2UsMpk<5*Ol!c-0On zg^{H-4$pEH*DXZ1w{beQ9?dE)C+&KF)B1vufUmW}#21}Ev)aGw5U|FY`ZVuMcXp?o z({i{pt)C5!fxUH#@kJeJByYEH0>8Qd+V|Z@|2VygKpUQn-=2%P5tD? zzqCAXp4Pl;w7%G%PWtR|Qu6Te-@dKBe)#GKPsiF-=$S#U>9+v^EAy0{9{buek~PZKtyP4Nax-|$NmkV<_&`Y literal 0 HcmV?d00001 diff --git a/Craftimizer/Graphics/expert_badge.png b/Craftimizer/Graphics/expert_badge.png new file mode 100644 index 0000000000000000000000000000000000000000..5186387b33864c011989f8bbb953926245ba66db GIT binary patch literal 896 zcmV-`1AqL9P)UTtz`Zvk~Nksp0kHZP+3fe+9WB z1STLg4B4D+H|&T0s}S3--zA^I=a0dP@GoG+UFPTE`3rcJgGImsI@_ST8wf*ZH#Ai1 z@S#!okW;y0E&mSz9?az6(S&X%+H}I&=?M-kj3)Lk;e}WQo|#;7h>1Dn;5xZb5x@8+G+t$H$K9`b^f{ zmuUCHM8lVWT|43O6^J?iCXXRg-2V2{&{(bg?JPZUs6PzW5=_2^cc0anu@^WVg_dIw z+4M#hkN`sJk+|R%k$R>@ob7^WwL7dMt}Wxup@t}Qo%6}sfo=ar%N5}wpIv`$R6-s= z{Jg;zpr+ga`@U~u=niDR>2^>$Q4pMv@URjgfIcUBwTM`)+L@prC3y)`(x5Fqz{Eeu WfGCcxKIPyA07*naRCr#keSe4?>6zX$guqw-RXM@ezf7dAuETnIAAt`BVKS|HwZ^7kQ0l&Lnn)~kT^D|kF4(+bgWz0tb2UyF=g0| z3om%9eUPjAufG2#Jn#E`Rn@K8A2Ugt+tKt?S64mX`{Q|^_xcX+|8)WEp4-9-ka=EUFVC^q%RcTt<$YeqDD9)4#+dKr zc$8(R1t+4%@3HMl`7?F?)GW3G_b~AIv5y6iz{$;9D}a33ix=FO*t(?&ng+d4t*j_2 zRJLqR*;uiF7|1b6k|2rW#wvNRT!*_DYc2M&4D8Gx9>~T5NB!|ZdxwX7!wN|5m*$6oZA@_02%Wh-z8+$YhrMl( zrU^FtDZ(gnAhkB3`EDPM8&atejjzzA1ta}xgfB6B< z|J{e5W4EG)8mSdaPe{9LMNy z^$|zWs4Y77rG z_Ol$19?UfKJmxqSgEYl>G=#Bgx%X#t1yTOfe&F)|{%n@F4$j87>$+=UrM>MAP#%oO zF=;GKzGMwFA2I_EpvRX$yTILKmXt{I}~k|Nj6UFTerVULH?R4(2%;0#A}%q;w#0 zHAkZ%dU4nUcuhu~#L{uLRp974D|XggKxHcw`98{g>E824dZSe9B8c%|*ibw@xP~p3 z$fq-PP9E9;k?ubpkHvL=^X7MO^8Q<&VrP&4nQnXIA0-X9Yydnw-gPSsayCc|+8%GA zm$+Eutki!M=?u?R)wDJ)g~mhvtR-lB^87eel7;gYT9wGNJ@**}XN3z}3qzhc*b?v1 zv#GzAS%&3&u0Z=mbU^mUBmCRx3{T#_e@yVwmCtnm>~g!n(Gvwtu7XT|&=;%It1_U) zRaJ$&EKyb@%NBKAipQzDc0sZT;+_!aFbWZckyyDL(f+5oI1flsEK%lp6Yv{LbEl5t z7-5pGeG^bjXRw{JP8O!NxU~O1CO>`ec;NlI1Fv(v0${`NB!uZ|lCvprMiT1o&KOA& zqlOXCSaY((5;n*~1q}Z`ZwrcKV;$YhEo%@Uib8~Oj3Cs6bJ(l46_$*Ta-v$n>?D^U zPUIXCDqWT@%L>csRN|>-T#HLT&Ml{f$8#Dme(8Ow>#93;)2RFAHO|P?Xo3)NF99v8eRUYB zwJ7qv_I%wWT?0|F2?XsThzIOvs8|!*8khfi59f~q-iedA@x&?RjH?bMm$?dH!|~Mm zqA0@47sgVi%@&y`BwS%{q2BT;Sd?yc6gn?2_#E4Qw|u$_ppP6b-0~8_I7JjPQ0vPd zF7zi~h#`1rTZ;$~rhWZY?v>#LSk7j6>3{t-&b@ndxBbymr}3XZe*&z-8*=GEbU!yH z{R;;)aIt)GTLq7V_vM{4C~S@CoGTBBdFpw3jME-8S$Oj{F4)Vlhb-9&wWQ^>C^i3d zp(Bp_FouFv3nRa$%;zrMxIYPC0ZiC~iDDO0bt%vO(N&y%`<+8W!zaCYKS^zJurQ z9D}NyOmX#OU*hJtKSY)n$d~z{AgTp=0Nu({#h_r67wWPtIaccv@EBuAC3+#^Bt{g) zFrms}x;UL`z-Zv-OIV$)1Wa4!0A((IMW7U>rIC6TeJ?~DjfAdfL^Y_&g=Btx!*NlG zEinTkzI62_p1pMps&euaZk#%$LC8W9=O{^VbIKapyRzq677oBjUC9`s7c9xLYN;;j ztWvJqfyY0@gH43YoDC0DOH|<6WKnmLNWePE+f73;89@u6UI0NQfYE7JwQ3i4%~$~9 zSxA|RV#c))W*}B4U>bbskKeZx)za$sRSxO1cQW- z*%_>zp*B=rE=d^hnxDlSQFRk0+=fMr0woHYBQN^_$1*hPHxr#XB!1t2!*3(l>iq;1~CG}CPCk@lF)N8rn2PAs|vQRu~=&COrua1 zpcnBo28@2AFodx=GJ74lEl||J)8Bj#PrZ5cY34 zBMqZLWD$ln;z-?-dWtbSC~b^oX~6X22@uzzDlEz@XP|BF#ETWAS7|>%8XF`MXZ|Dp z!8fKjapUMD`OewraqX*LmYe3{f)z|f3e@txBn~m^4@7`?l}`vi!S z5TnsZk^?_O%j<&2-b9}5qbhPq4%!M)>dZ(W48&NFjE5nA0tpQICPq01g3(Qtu-oElbzKN&aeDla|d+X^ZarNK5C@W05B)7ho z!4~WcDxH=00)z3^Knncn>`|lk@b4Rw@yiEIz1tB>H`7gQjrvWJ=z>?U{$1>&WNPmc zV<}?PjR_uejJLcfh(Ll7Vpl4f93hrjg_6Meq(hu4p};K|VFXkvl&-%_7-0APzr^IN zV}SSUlel{N86Zg=|0NeTz}^BuK{L_#8`KIQHGr@NX0w@SnIf4HzzgG-4}8t5a5Hr$ z7>!9AlstRu%a==J1g@ggn5UVrg2$}66X9mqGRQoEV zHrNm-|6A!K`jwKy@k`hBd+EpD$GP|3KCJWo%Iy6&UB4EbWNwjqJiS&55vBK1nJ1ESC( zj4Yy_K^nzK%>~ivvZ^Q6NB(Gw5>=!55jQW#>jf(tT5qtr<`iZ4_kV!%?>{^;c=yjf zi|haTw@{b0T7H~Jgpl0F7_-`EuVJ)V!IjUmm!1LEEHApLWF$U6YDRjm?(G61!(KiN5TQLtKFRhe@1hL91 z*uDM6;%Z42&K50i&K0$XKy>AUcX08;qf?dXxzo7$!tZGXRXW}p_h4P3GC>5pzksP2 zVzp4iyz`E zAN};mmiO?*G5-7SkHy-gp7yed(YNLu)K6EanRhJrynR z!Yc+1R8lfs#zQkfr!4v4jYfEuaZ=Et#kftI*ITj=VH)8392qEpx2ACBk zvfRo_Tk@!)&^DxiqNLTU3TzH3FYlC14zA;Z_tUR^4G+G#g}k8f%V?q2N3USqlThI~ zRF=EyH9+KIt>OKoiI8al(2EOdfv+A!vB9pmu7bBm@aA*azzA*WbR|@0CU8lDFyXw5 zuUGXa1yX*hvP(QB;~3P6qy ztUILKkj6ved0g=Bo}S>wH(teLpq#P>#=?Y(d8B!= zD3R~E7)U|&(xulADA&{axH`Al<#)mY3wC8?$|Ztch-7oq$>#dL3IqnFUBH@L#xO!m zkQ5bi7zK}?&gi;J)O2B=EC`@bmqT6-z{@{7Ud+4u>;yM{YZo=ekpYL7BE%*~R&s2v zmZV<>aT62elDNRy7tsH)i6#Kj7y?Nm3LxX86}4vLjW_Nc42LAl!0qWH6lEz>0F0>u zpeySb=aMv8<+JiPi>jcKQotk;Qj;JkLWD@LS5zniNTq?iK+V|K$+lu+otUT6mE&Hd zDj)n1U-|Ip3FGeRv$*klUj>Q^b=*S`1{e){U>!*-lxeYw;VN`GrOWuaF9jWdFo?qd zW;4~Qgz_CNn2E+sHf+ovEDlT@N|Wf(B10Xh@*+ink4=e!*93P*aTv#z1kY5$L0L1# zW(WPE#Eh;>Pvk-@3eso9=YmFpGQZn&s{wFJ>OXy{{js5x0WjgC7 zi)aT7&9!l~C`8g{5$5$iCpJi@jNmbmQ7B-~I7MPpL@arhT7{5DR?`eM-NPk~rt*-v znAI|kH}v&mTHdV_PvPnpe_Q&+BFmXb0b~hD?-+)NOoZGrp#&%$-xP|(@e)VieV39f zgUKVNTIB?UI?JU95J`?MyubMQ!JO8|qTLnjL|!B-1EkKqHBfq;Xb)FqulUC#F;voX%|M|h~ITZ#4OeG_= zoMf0va*=E(W~0p`&ajA+`yQ0IF994OVVXyPY+0h9yXGZox`)I+g6ncR%K_J8@fr6@ z_7E5Iqr2^`6Hnsm6VEA+k{m!A3V=97z#a{)F^sS^?8$XzD3O;O1R!=yZX}03v;^+f zm_b4WjQu{4lolmJ5!tc@qPoV$g9nR)DJ2}&knp-75_bICUW2a+q_xRd=J}sfT{hz$ zMrH^$!KZoMTp$Rb<9>3HZUk*5xLJGbm?xI^#50}6aEXYVls#rY0U>I7j8Q+qtg2Ad zrA|@^DP)hvS&*)P*2cX=LP6pdre^smMF1U7S>(p+Z#+22W#(8%HMC(4#9OJ%0Eopn z*Ddl~n&(;w(p5ASg`vSH9$`BSagX6($$o`{oTQlmmX08kOs~DJy!!wbkCAO@dEa{C z87G2wdU;}j5bGg;vf8mhZW6*QTbe$DiH!~Y^ zfhZ=t^7`!qY2}Fcpxh@NB(NbYcY z;UZxZK%PLW5S^BCAX?zqZks%XtLd{HEzDh)6E~?92Aou&3-iShl75WAK!0B_<66?0 zQX23+#%mrma{s(H0gyIDm3m*bOND?*L;$>ZAa#N7DMkjPtxeF=RjpG1bM}W_Vrc=? zX{{vcp&!Nq;Oo;Va>No}ByM&AqziZE|E|3Em$>-;(N&dO9=xt4`_%43kq3|@m@me1 zx^SwLWLf4C|0s?sfIiSWT~JI|WqtuLuTYbK*Bbw5Ly582c=^@a2ezaAk?*COF)|rX z(|}I_krji|oQ8xb_v`glFb!i2jKR%EGvsyT;xa9-Ru`t_@f;37vApc)69b2W=aYv< zmR6#dRn1r9>v0Q8n+ z47P@SL~($;rUg3d<0cnzaSxsbie8e+3bE);L+XFPbZ?HVjGXmCI1^o0^ggLNEH2Og z9OwUkf#7$Jv^fZyJT64t&$Z1H%}Nk%=+O_OMs7Hg<~mQX582 zN5bW|@8bM>M^};$wY;ti3;haUyfhYsA^?zzlyaTGFb$E$TwH3C8J3OvIuH%hg}#Ia z(Ku|0*zEw2ipB9^8n*)Y%6Bye20%|Xxaa`4fXA%?B#!M6GIY{_Iv@*dS8%gtj0z0R z5c>toBvw2Ymfyk^W*uBrxcoo=1m}Nr^d811g4ZP9h6KV;nh%x3QB2@EZ zGHJ*c692pfT3TyMV4SE5L{$ZwLrOVTr-R`p;-n`lNdOo5 z63g!9J#x&tmJ8!RqkN&7=;7DdQsGvWtFTx)n~f#i?_aw^AC?cZt=0;`J~sh z0i5N~>X<#l5Ot*PDnSn zCFLvvsP(~cJOYIU9>FX+<5*iW=i;YaEF!8Ce)kd2R^?JLrE!W)lj8Pl3Tsl7k&&eF z!hd}Y=caeM4)>Y2c{FM-(Ca7I6bh4zmn^GNnU&CC z2VZA&(UkfZQX1N(a7M|#HIiN;iIei22&&Q!Fa~F~$Ee*j*3w$pkLH)9QKhL+P!sGd znJwlrt-qPX=$jF;B7?Od7KO#mjc;Lc_ts|utb5R{lTYL7seh?%<6Ea48(BZ08l>yX zvIL_W`{^hqyiqJxlUvGJ1(T|R=O~H4AyycsW-CJ9iJhTX*37&V4z^QZ;Zp2pZBvPE zyuGs{Iv{CH&KYR#bnFm(o|W!v*vayD!1Pot#HEv7gf!}7)5N%$6*&8?*KqdkF`C{U zJXcnIEc$Y7ZL3pOJEHr>5m{>IM%=_WHY4Wwcp2 zjaIMH#tw1Ez^>e6^6~+Xq)s;88LP->fH_AP z+^}RbK*W+V-EzW%lk%5)6z&m)u?!CNdNIEI?MFEI?Kh4L-km3(!?l0@yJ`Tw;dIEh z*v2VVX1ZB+sRSkBBwv+9GEPvEV^PGab5u*VDqR=O2?(?%0kLz%v`6+tyfs3=Gl*D; zP>jfP6%!^CPG?bj46(hl-ArH2?oAzecFi%lFoTG*B%V@}rDSudi5YWxp~3I}>0jWv zH~;E*;63xN+y%Rak~>RdalH5yi}RLe+L$Nef$K(5!k7LR%eT3(0pHRTzbq?+zNOh* zv9n0VdZ&-snA_MmPG?0dE@j&+EjGZ$c)Y9QZIQvw1r9ILo-AhfA4vHlLaYll9IP}P zh42xO0PYd!P^KdO@ZLK(d;jPp`Oed0Tsu8!CtLl^GzsL8^>D;lC=35A)0&aAB&0N> z)x}oYuN=B84 zyAxPS-c^a&bgDDYBJaB?)6GqpgjWFFM!{?*7ypgvJ2*Ezx-f9}xe2cR?oK0_`>n6d zn6Ssri?0Hfb5#!O{vj_U_IWO7YJ-i@NT>Aqr8_$lNz+u5A3T_fm?*P>ZW=!r3}AXaC;jNSht1}= z@`E4Y;=`jW$@eE0aee2C>vESmljj@m>qfV-Pg!Lq=g)GaOisu@WL}6&7m2wk!=B}m z!=HhxXWh72Q`Wor>n7u?03oeRx5G*L!E6C=mdAw=A1?(!l>-Br%A(1$moa-V?F^Oc zWNI)N%2d9}-wNO#Tzw4}-#L2g@%~p{#Ldh9zMVN1Ro%}YyKyP^01SDAN@IyST-ov{ zhlHEKOjGu8sny*?QP)y}S2{_8y2-ws&wC&xAyYr(whGT7wf9T#WDv^D=XeWbHr*I) zUC`ko18nsNV2?PfET#mIFWD2~w!m`ReXrL5_~Ol*IM4lPM|{0Kncz=eeOYuJ6S=ppj?uP^Ti|(2V9-mbq*x5%8EQ89Ym1^XE8x)zoD}T-;kRWl1juL^!hjog zltW4eU_2#wZgKrnA7n|{l^bnMWJ+J;sW`^YWGphz&D;0YO+=J%?vf6~HK;(oaQ!;Y z9b?PrM_+pmzk2O8+$MeGsITLDZOJGzbxmw)rXm`bQ;kt{rWy!0Ln$K>Wr%Q(QYnhk zz*d}!P)idXk3PQQwrWQ@nCy36=}vq93OMAsw@@$}>uU!qIsm$laEH=lTk7HKH^0|5 zY6v2AGRB3$06SN&VsiKH5$z86>5Hea|Ju{&@kYWS_UsUgqC`L|G@;a{4Uq2aa_^mO zu|g%7U?URz3Q@u>60eG>$^bmwRpd0aBApxJb_M=Qq!l#5vMHz3%c$aTN4q2rDo9R+ z_p{QHV4BenJ@>{|e{vx4+0Da@`a|@GDQ-`vShC&cDiKkLk_0Nmz~&>l`8b)4ZvumxyJSu9A@a;M%KffRU4hL50{XV6c>3Pb^j0 zdl8mmUG*gUM>SBSh%F`i8OKA)&x+WndN0Gj<)ghQ;PdVQ@- z#fs#={GS4+9v+?N-FYg-cYk{bv)OM(E4&C0&xaa3DsFG+>@{(>Di5@9Tm4t* z44c`sUOyVV79Ts7)n*QNOoys?J9eW-x*MU>RQHz7$Mz+0Pe?vzfkbSO<2Xa(&+B$7 zi3Q4;eQgQ6^o#d!?xXiVJ8JI8JEul^!SgQg-Q(|;C(`cNl zOm`AiuUM^*+YH>cL^oowg59A&w&}=^?K0NuF8Yq8%5!LRj|;cpa)^kke6!9DIqe}U zczme^c0$Zczshj#qoY4d>aEjH<2(QCpSqc9hbRH$jLD{MX_daMGeuKz2+sRyYc1}c zU_)-KZg0MQR-gxxeG9pp$7L`%#2JL6EBxL@Z}6J~TzsSw%x=T5YybcRd`Uz>RIM4@ z<3izpn?85tE#+XcDLSGK z-nj={(AGE=@9s)Ra`jXGvlTbNG&6*q?G{~26QZk#G=M6`8rmIj+VS8yv|+9^CX#t@ zDG8;@;M0^)8*o6c|$GR zIz^~jPN}KUHd}IT)M*}o)KPR;YYs;% zJWdz7R1n1^)@u;&+WJ#0lDeJmeYiO3fs=~HJ%LWElg`5U6iRt!$-zn;c51DzYh7y_ z9@h!EJMqQ=&^~;2eN{i^_Xa%ocgo+R@fmR)*=IMO$yNZL1ANay#w5tC`fe}s%K0mH zsx^*zPU`%MV4>U^zB<78b!ne4(r_ae?z$>&XG=#X*mg!E8^b;lfC}Cd zkK3iwDoF+O84Ix}C6DG0^x-(})fkpM+#*7_wIxG8F7CI-F$;>cMSE@-_+#JesG+b_J6pW!E8&>|& zcinSKE4Pq+CRVn5e$2~};>P}(jOyq|*9)2;=OF9;zDAxOk9rFDY;PvmeJX%_TJ6m7>S+`*mmZqC|rXOT8L*6M1< z?;^%bHrYE*1?hmO9^1*fs}<#H`aWdM9s|%H)3wAE@VpB;Bl00<)!sdEZNHI+N1UNf zQyB@K?=MjBVG|879dq;HPB!`JQz`YymMe|T=H$&MWxrjADpP*)+cMT~ehoCm%>n6t ztmgaG6L{eKyN~^UbMOHA=Opt$Sz}f_&4T$F)@{T&%&R(Cv-vY!yO-!@Zkf^R^emO@ ztenaJ#tcvQ=I;TOo#rTa`+Rby%&@Op+bS^fu~r9I)w~Y5ho-pf80w=;UDv<Px+f=NU{RCr$Pon2@XSro@l;zvKy`kAUt)oR@;Vw)(@D#fLhwI8DBgHYMkBJ1MA zf>0mqgD=G*zNiI-XdiT4WTC|hg6Kn2U8t^#C|Z+7E%pPUZP@w|thF^(ZI=J+j)ZQe zb0;%NZqhjrD9pVx_x|SGd(S=h%&CENQANO@seZ_`0|NsK#>U1JwZ&*O z2IkM7|M!j^J9;b@%ZSRRgk4=xqr-kmP(=}1e}BKl>-FYt+_=%<_xtJd=g*36KXvL< znm&Cx-M)SMKz4R^XI560Hx@9X1`yzynwq}zcs$=178VN7OePcM<>k?mB}>R)P}Y>O zu`%lD>7i%Op3(5|@Hd!tyZx_{)A4Qj%-Unl*x{5^FO>4wBq;Kx}Jk z!=ge%LqoRe>T0)Ko#5+=0AlE#I(6!ol9H0s+qZ8QoB>*i&*#IwnO?tsP4C{l6Sifd zhi2NeX~MH+&6-8?=FKCE#X_@Z&(`YE^#1*Ox^m?TIi1d`!-o(5ro_Bq1~`7`u3EK9 zll?3l0G*wk^ybYQML^7EGv(&y3TBg9!LG&Sa?!0@w@B$wW(Iiv{P{ei(b&Fv^=eIq z?u!>M=*g2O^x?w?RsBy(OCz)iTShg4p^RaSHu?Sj{Hm%d56k_S0UkYibhFuPzOif9 zE>*(^?xuV9?$OJaFIn2hTn0Wx?1JFD2v;zDG&D3&UteFz$&)8&t5|x&gQbtCG;>U0caAahJu3ft(tgO)xtk~POZBuL|a0?Kc z*WceC4R+t2k(HH2Wo2cGp2;OZ_~ITsc%Y~wS`{uRC=d>Cz>|N0zJ| zmMaGX999F4KUsu3fY>787fZpG6F(N=4q#JLlWIemrQ;KofuUVdQNeT@t^i_R+uYpD z)bL0b+OlN}WoBl|K87oRj~+b|_Ln?}&1NIJ-7eb{t^mfubde&&YSM58kcatC1j;%O zas}|>#f$Xy>sQ%f6O96J`tIGkSN2g{0mP=9C)u}epKNou0;p@?xc8;hou$u#DgnX< zge}N}#N{f?lO2fD!?kPI%Dzjk=0KiyBIV`f!bzT-a0L*Jg(oIjPE;v4tTrI(a|clO z#ss}qrK>Qi;`{@vvC>Z%t`nR;S=;y*}A`@`9j>rT=Lq$RmnNVC@Ob9#* zN4Oor!JWa%j7WJNV`Jh1pa>JuS#bFe4Go2(EZ^#+r>7HK_G+WExHP0$$RIutF!A=4 z2m*)zO^f(I@wsuOf#YGZMyMia2;LPfBUBRtpc3tiRN1)Jz(_$+a$g;w5?Ue3>Hu|s zDha5pu4|wUP$dDC)pZTj0jeaRvbwH;IzW{KR94qDPzR`zfXeE+2J#HBrlzJWBO{}^ zva*t9&YY>TseD!s(!lb%x;iIIE15nv#K+b(H8npM6%{#l?%YYpOq7U%)5FHbMx5Im zIC0{{?=1Nfm;fSALU(t!Ytf=bf+G_jCnusH6!_JvSJd0vON$pTX3C!+3sB6HkeQiz z8X4FU6<)AHJbn5Uv5idm6D9;G=7&j1NvTJE0vsDAIzVV4=EAP7E_`Jrn;uFMpqOML zIXU^}Fn5_#T}n#%zZ(G-{umn~Z+nS??T zAd*b@e7+m``S}zG1QK!0knIz`b3rDLw6rwIBovYWVfPIV4mKmD2=b~-MnS(HK72^& z>FJVoVMu_Y-DftN9XPm7^o2u8Ignl0?RH}*OWK7Y0Z#V$FL*J5UD(&xCpiy>1Xx>J z`@`ty=*^s*9N}J*bqGTP(RYNN7>&lw$B!TXbG){O1Xy2RpXc-W+Od?YP0bYVb0<=$ z!2UIvO!?K-)&ENeB{>IPzI-{Sx3_meAP|t47=(X%uzUCJkpJ{xQp3^W a#s32eTPmv$_2|?90000Px(ut`KgR7gv$mVZoJRTRh1d8M7YQgr+pEp%BNVZ#V351}0j3=@CEWLb3>H+rgx7*M%MnaPRZF{H2u0J8w7 z2^o6cI&J;MpGcXYWC_A9N>GJJSD+NSk)eYGM1!|yZV5uefQm%r%SfOU5MHWQ)PT;} z2YQ#Tjse;^>6kS5U;vc>@B{#;R8rQ*8<;mZ0dN43v|DGa?LsO;fV?`wKlO6iM< zr8{z^`M$WbQ>W_;uKrFAfJFe70%dL0nKx=O?E|aAutHb(r1Nc5G zMgWLd<*x-jdJeq&=D1qzXY>Y_Aww1&D{m@Tr&8KJ#cfoeYHtMS<8jx>m zmNe(hV}De*03hR#yehUZt|aQXdFXP8y=Cg-A9eG+??eSElD1|8$7KUzwgh9I&y#7= zkEhng+{_+tpmFtdCzgIOzQVmzFrUx}+yRq@96ZDSs{|#)!+l++n6FtpoS;Xq0S>m^<|ZEG4A0h$5mxogwcz8BoXV1urBq!9q>#V8g8 z^5>?9cn%v~M#z=rYI#TAzjv;Ohk8YT7hT-lHrrp8?^Y*?c$VslMyBmE#|Fds&uXlmm9YMrztuvOBkxrVB-1Q#6$N|-0d9Jbyr0vNKDJu* z5BZmpw`|-p%m9-ZY~$PBXnBvjI@i;UNY7hlET0Dkf33srkLxMY@0N)k>0Axu*Z)FK zPC9NitQ(L|WGcDID>&$f`Y&XD{rg;Z`%y!=&nDQ?wWv0gn2i8b#m5E%DJ5oASH`33 z4!ly>zjd=jaTNfP2@PEGFvrbm)t9r5@OQ$xD&!Shf7<#SAl6e#iug69q~qa@l0EXD g Cache = new(); - - public static TextureWrap GetIconFromPath(string path) - { - if (!Cache.TryGetValue(path, out var ret)) - Cache.Add(path, ret = Service.DataManager.GetImGuiTexture(path)!); - return ret; - } - - public static TextureWrap GetIconFromId(ushort id) => - GetIconFromPath($"ui/icon/{id / 1000 * 1000:000000}/{id:000000}_hr1.tex"); -} diff --git a/Craftimizer/ImGuiUtils.cs b/Craftimizer/ImGuiUtils.cs index c895934..0e34dc6 100644 --- a/Craftimizer/ImGuiUtils.cs +++ b/Craftimizer/ImGuiUtils.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Numerics; - namespace Craftimizer.Plugin; internal static class ImGuiUtils @@ -158,4 +157,24 @@ public static unsafe void Hyperlink(string text, string url) ImGui.SetTooltip("Open in Browser"); } } + + public static void AlignCentered(float width) + { + var availWidth = ImGui.GetContentRegionAvail().X; + if (availWidth > width) + ImGui.SetCursorPosX(ImGui.GetCursorPos().X + (availWidth - width) / 2); + } + + // https://stackoverflow.com/a/67855985 + public static void TextCentered(string text) + { + AlignCentered(ImGui.CalcTextSize(text).X); + ImGui.TextUnformatted(text); + } + + public static bool ButtonCentered(string text) + { + AlignCentered(ImGui.CalcTextSize(text).X + ImGui.GetStyle().FramePadding.Y * 2); + return ImGui.Button(text); + } } diff --git a/Craftimizer/LuminaSheets.cs b/Craftimizer/LuminaSheets.cs index 67ba2e9..18301f2 100644 --- a/Craftimizer/LuminaSheets.cs +++ b/Craftimizer/LuminaSheets.cs @@ -16,6 +16,9 @@ public static class LuminaSheets public static readonly ExcelSheet ClassJobSheet = Service.DataManager.GetExcelSheet()!; public static readonly ExcelSheet ItemSheet = Service.DataManager.GetExcelSheet()!; public static readonly ExcelSheet ItemSheetEnglish = Service.DataManager.GetExcelSheet(ClientLanguage.English)!; + public static readonly ExcelSheet ENpcResidentSheet = Service.DataManager.GetExcelSheet()!; + public static readonly ExcelSheet LevelSheet = Service.DataManager.GetExcelSheet()!; + public static readonly ExcelSheet QuestSheet = Service.DataManager.GetExcelSheet()!; public static readonly ExcelSheet MateriaSheet = Service.DataManager.GetExcelSheet()!; public static readonly ExcelSheet BaseParamSheet = Service.DataManager.GetExcelSheet()!; public static readonly ExcelSheet ItemFoodSheet = Service.DataManager.GetExcelSheet()!; diff --git a/Craftimizer/Plugin.cs b/Craftimizer/Plugin.cs index cb31ff4..c62db6e 100644 --- a/Craftimizer/Plugin.cs +++ b/Craftimizer/Plugin.cs @@ -1,6 +1,7 @@ using Craftimizer.Plugin.Windows; using Craftimizer.Simulator; using Craftimizer.Utils; +using Craftimizer.Windows; using Dalamud.Interface.Windowing; using Dalamud.IoC; using Dalamud.Plugin; @@ -16,40 +17,36 @@ public sealed class Plugin : IDalamudPlugin public string Name => "Craftimizer"; public string Version { get; } public string Author { get; } - public string Configuration { get; } + public string BuildConfiguration { get; } public TextureWrap Icon { get; } public WindowSystem WindowSystem { get; } public Settings SettingsWindow { get; } - public CraftingLog RecipeNoteWindow { get; } + public Craftimizer.Windows.RecipeNote RecipeNoteWindow { get; } public Craft SynthesisWindow { get; } public Windows.Simulator? SimulatorWindow { get; set; } + public Configuration Configuration { get; } public Hooks Hooks { get; } - public RecipeNote RecipeNote { get; } + public Craftimizer.Utils.RecipeNote RecipeNote { get; } + public IconManager IconManager { get; } public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface) { - Service.Plugin = this; - pluginInterface.Create(); - Service.Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); - - var assembly = Assembly.GetExecutingAssembly(); - Version = assembly.GetCustomAttribute()!.InformationalVersion; - Author = assembly.GetCustomAttribute()!.Company; - Configuration = assembly.GetCustomAttribute()!.Configuration; - byte[] iconData; - using (var stream = assembly.GetManifestResourceStream("Craftimizer.icon.png")!) - { - iconData = new byte[stream.Length]; - _ = stream.Read(iconData); - } - Icon = Service.PluginInterface.UiBuilder.LoadImage(iconData); + Service.Initialize(this, pluginInterface); + Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new(); Hooks = new(); RecipeNote = new(); + IconManager = new(); WindowSystem = new(Name); + var assembly = Assembly.GetExecutingAssembly(); + Version = assembly.GetCustomAttribute()!.InformationalVersion; + Author = assembly.GetCustomAttribute()!.Company; + BuildConfiguration = assembly.GetCustomAttribute()!.Configuration; + Icon = IconManager.GetAssemblyTexture("icon.png"); + SettingsWindow = new(); RecipeNoteWindow = new(); SynthesisWindow = new(); @@ -86,5 +83,6 @@ public void Dispose() SynthesisWindow.Dispose(); RecipeNote.Dispose(); Hooks.Dispose(); + IconManager.Dispose(); } } diff --git a/Craftimizer/Service.cs b/Craftimizer/Service.cs index 0c02ad0..e09304e 100644 --- a/Craftimizer/Service.cs +++ b/Craftimizer/Service.cs @@ -1,13 +1,11 @@ -using Dalamud.Data; +using Craftimizer.Utils; using Dalamud.Game; -using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.Command; -using Dalamud.Game.Gui; using Dalamud.Interface.Windowing; using Dalamud.IoC; using Dalamud.Plugin; +using Dalamud.Plugin.Services; namespace Craftimizer.Plugin; @@ -15,19 +13,26 @@ public sealed class Service { #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. [PluginService] public static DalamudPluginInterface PluginInterface { get; private set; } - [PluginService] public static CommandManager CommandManager { get; private set; } - [PluginService] public static ObjectTable Objects { get; private set; } - [PluginService] public static SigScanner SigScanner { get; private set; } - [PluginService] public static GameGui GameGui { get; private set; } - [PluginService] public static ClientState ClientState { get; private set; } - [PluginService] public static DataManager DataManager { get; private set; } - [PluginService] public static TargetManager TargetManager { get; private set; } + [PluginService] public static ICommandManager CommandManager { get; private set; } + [PluginService] public static IObjectTable Objects { get; private set; } + [PluginService] public static ISigScanner SigScanner { get; private set; } + [PluginService] public static IGameGui GameGui { get; private set; } + [PluginService] public static IClientState ClientState { get; private set; } + [PluginService] public static IDataManager DataManager { get; private set; } + [PluginService] public static ITextureProvider TextureProvider { get; private set; } + [PluginService] public static ITargetManager TargetManager { get; private set; } [PluginService] public static Condition Condition { get; private set; } [PluginService] public static Framework Framework { get; private set; } - public static Plugin Plugin { get; internal set; } - public static Configuration Configuration { get; internal set; } + public static Plugin Plugin { get; private set; } + public static Configuration Configuration => Plugin.Configuration; public static WindowSystem WindowSystem => Plugin.WindowSystem; + public static IconManager IconManager => Plugin.IconManager; #pragma warning restore CS8618 + internal static void Initialize(Plugin plugin, DalamudPluginInterface iface) + { + Plugin = plugin; + iface.Create(); + } } diff --git a/Craftimizer/SimulatorUtils.cs b/Craftimizer/SimulatorUtils.cs index 27eeb87..4d30def 100644 --- a/Craftimizer/SimulatorUtils.cs +++ b/Craftimizer/SimulatorUtils.cs @@ -2,6 +2,9 @@ using Craftimizer.Simulator.Actions; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using FFXIVClientStructs.FFXIV.Client.Game.UI; using ImGuiScene; using Lumina.Excel.GeneratedSheets; using System; @@ -10,8 +13,10 @@ using System.Numerics; using System.Text; using Action = Lumina.Excel.GeneratedSheets.Action; +using ActionType = Craftimizer.Simulator.Actions.ActionType; using ClassJob = Craftimizer.Simulator.ClassJob; using Condition = Craftimizer.Simulator.Condition; +using Status = Lumina.Excel.GeneratedSheets.Status; namespace Craftimizer.Plugin; @@ -85,11 +90,11 @@ public static TextureWrap GetIcon(this ActionType me, ClassJob classJob) { var (craftAction, action) = GetActionRow(me, classJob); if (craftAction != null) - return Icons.GetIconFromId(craftAction.Icon); + return Service.IconManager.GetIcon(craftAction.Icon); if (action != null) - return Icons.GetIconFromId(action.Icon); + return Service.IconManager.GetIcon(action.Icon); // Old "Steady Hand" action icon - return Icons.GetIconFromId(1953); + return Service.IconManager.GetIcon(1953); } public static ActionType? GetActionTypeFromId(uint actionId, ClassJob classJob, bool isCraftAction) @@ -145,12 +150,24 @@ public static byte GetClassJobIndex(this ClassJob me) => public static sbyte GetExpArrayIdx(this ClassJob me) => LuminaSheets.ClassJobSheet.GetRow(me.GetClassJobIndex())!.ExpArrayIndex; - public static string GetName(this ClassJob classJob) + public static unsafe short GetPlayerLevel(this ClassJob me) => + PlayerState.Instance()->ClassJobLevelArray[me.GetExpArrayIdx()]; + + public static unsafe bool CanPlayerUseManipulation(this ClassJob me) => + ActionManager.CanUseActionOnTarget(ActionType.Manipulation.GetId(me), (GameObject*)Service.ClientState.LocalPlayer!.Address); + + public static string GetName(this ClassJob me) { - var job = LuminaSheets.ClassJobSheet.GetRow(classJob.GetClassJobIndex())!; + var job = LuminaSheets.ClassJobSheet.GetRow(me.GetClassJobIndex())!; return CultureInfo.InvariantCulture.TextInfo.ToTitleCase(job.Name.ToDalamudString().TextValue); } + public static Quest GetUnlockQuest(this ClassJob me) => + LuminaSheets.QuestSheet.GetRow(65720 + (uint)me) ?? throw new ArgumentException($"Could not get unlock quest for {me}", nameof(me)); + + public static ushort GetIconId(this ClassJob me) => + (ushort)(62100 + me.GetClassJobIndex()); + public static bool IsClassJob(this ClassJobCategory me, ClassJob classJob) => classJob switch { @@ -303,7 +320,7 @@ public static ushort GetIconId(this EffectType me, int strength) } public static TextureWrap GetIcon(this EffectType me, int strength) => - Icons.GetIconFromId(me.GetIconId(strength)); + Service.IconManager.GetIcon(me.GetIconId(strength)); public static string GetTooltip(this EffectType me, int strength, int duration) { diff --git a/Craftimizer/Utils/Gearsets.cs b/Craftimizer/Utils/Gearsets.cs index 86e16c5..0a6dd19 100644 --- a/Craftimizer/Utils/Gearsets.cs +++ b/Craftimizer/Utils/Gearsets.cs @@ -8,7 +8,7 @@ using System.Linq; namespace Craftimizer.Plugin.Utils; -internal static unsafe class Gearsets +public static unsafe class Gearsets { public record struct GearsetStats(int CP, int Craftsmanship, int Control); public record struct GearsetMateria(ushort Type, ushort Grade); @@ -117,9 +117,12 @@ public static bool IsItem(GearsetItem item, uint itemId) => public static bool IsSpecialistSoulCrystal(GearsetItem item) { + if (item.itemId == 0) + return false; + var luminaItem = LuminaSheets.ItemSheet.GetRow(item.itemId)!; - // Soul Crystal ItemUICategory DoH Category - return luminaItem.ItemUICategory.Row != 62 && luminaItem.ClassJobUse.Value!.ClassJobCategory.Row == 33; + // Soul Crystal ItemUICategory DoH Category + return luminaItem.ItemUICategory.Row == 62 && luminaItem.ClassJobUse.Value!.ClassJobCategory.Row == 33; } public static bool IsSplendorousTool(GearsetItem item) => diff --git a/Craftimizer/Utils/IconManager.cs b/Craftimizer/Utils/IconManager.cs new file mode 100644 index 0000000..abefac2 --- /dev/null +++ b/Craftimizer/Utils/IconManager.cs @@ -0,0 +1,66 @@ +using Craftimizer.Plugin; +using Dalamud.Interface.Internal; +using ImGuiScene; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace Craftimizer.Utils; + +public sealed class IconManager : IDisposable +{ + private readonly Dictionary iconCache = new(); + private readonly Dictionary textureCache = new(); + private readonly Dictionary assemblyCache = new(); + + public IDalamudTextureWrap GetIcon(uint id) + { + if (!iconCache.TryGetValue(id, out var ret)) + iconCache.Add(id, ret = Service.TextureProvider.GetIcon(id) ?? + throw new ArgumentException($"Invalid icon id {id}", nameof(id))); + return ret; + } + + public IDalamudTextureWrap GetTexture(string path) + { + if (!textureCache.TryGetValue(path, out var ret)) + textureCache.Add(path, ret = Service.TextureProvider.GetTextureFromGame(path) ?? + throw new ArgumentException($"Invalid texture {path}", nameof(path))); + return ret; + } + + public TextureWrap GetAssemblyTexture(string filename) + { + if (!assemblyCache.TryGetValue(filename, out var ret)) + assemblyCache.Add(filename, ret = GetAssemblyTextureInternal(filename)); + return ret; + } + + private static TextureWrap GetAssemblyTextureInternal(string filename) + { + var assembly = Assembly.GetExecutingAssembly(); + byte[] iconData; + using (var stream = assembly.GetManifestResourceStream($"Craftimizer.{filename}") ?? throw new InvalidDataException($"Could not load resource {filename}")) + { + iconData = new byte[stream.Length]; + _ = stream.Read(iconData); + } + return Service.PluginInterface.UiBuilder.LoadImage(iconData); + } + + public void Dispose() + { + foreach (var image in iconCache.Values) + image.Dispose(); + iconCache.Clear(); + + foreach (var image in textureCache.Values) + image.Dispose(); + textureCache.Clear(); + + foreach (var image in assemblyCache.Values) + image.Dispose(); + assemblyCache.Clear(); + } +} diff --git a/Craftimizer/Utils/RecipeNote.cs b/Craftimizer/Utils/RecipeNote.cs index 9178682..d27154b 100644 --- a/Craftimizer/Utils/RecipeNote.cs +++ b/Craftimizer/Utils/RecipeNote.cs @@ -16,6 +16,52 @@ namespace Craftimizer.Utils; +public record RecipeData +{ + public ushort RecipeId { get; } + + public Recipe Recipe { get; } + public RecipeLevelTable Table { get; } + + public ClassJob ClassJob { get; } + public RecipeInfo RecipeInfo { get; } + public int HQIngredientCount { get; } + public int MaxStartingQuality { get; } + + public RecipeData(ushort recipeId) + { + RecipeId = recipeId; + + Recipe = LuminaSheets.RecipeSheet.GetRow(recipeId) ?? + throw new ArgumentException($"Invalid recipe id {recipeId}", nameof(recipeId)); + + Table = Recipe.RecipeLevelTable.Value!; + ClassJob = (ClassJob)Recipe.CraftType.Row; + RecipeInfo = new() + { + IsExpert = Recipe.IsExpert, + ClassJobLevel = Table.ClassJobLevel, + RLvl = (int)Table.RowId, + ConditionsFlag = Table.ConditionsFlag, + MaxDurability = Table.Durability * Recipe.DurabilityFactor / 100, + MaxQuality = (int)Table.Quality * Recipe.QualityFactor / 100, + MaxProgress = Table.Difficulty * Recipe.DifficultyFactor / 100, + QualityModifier = Table.QualityModifier, + QualityDivider = Table.QualityDivider, + ProgressModifier = Table.ProgressModifier, + ProgressDivider = Table.ProgressDivider, + }; + + HQIngredientCount = Recipe.UnkData5 + .Where(i => + i != null && + i.ItemIngredient != 0 && + (LuminaSheets.ItemSheet.GetRow((uint)i.ItemIngredient)?.CanBeHq ?? false) + ).Sum(i => i.AmountIngredient); + MaxStartingQuality = (int)Math.Floor(Recipe.MaterialQualityFactor * RecipeInfo.MaxQuality / 100f); + } +} + public sealed unsafe class RecipeNote : IDisposable { public AddonRecipeNote* AddonRecipe { get; private set; } diff --git a/Craftimizer/Utils/SqText.cs b/Craftimizer/Utils/SqText.cs new file mode 100644 index 0000000..b8ab740 --- /dev/null +++ b/Craftimizer/Utils/SqText.cs @@ -0,0 +1,33 @@ +using Dalamud.Game.Text; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Craftimizer.Utils; + +public static class SqText +{ + private static ReadOnlyDictionary levelNumReplacements = new(new Dictionary + { + ['0'] = SeIconChar.Number0, + ['1'] = SeIconChar.Number1, + ['2'] = SeIconChar.Number2, + ['3'] = SeIconChar.Number3, + ['4'] = SeIconChar.Number4, + ['5'] = SeIconChar.Number5, + ['6'] = SeIconChar.Number6, + ['7'] = SeIconChar.Number7, + ['8'] = SeIconChar.Number8, + ['9'] = SeIconChar.Number9, + }); + + public static string ToLevelString(T value) where T : IBinaryInteger + { + var str = value.ToString() ?? throw new FormatException("Failed to format value"); + foreach(var (k, v) in levelNumReplacements) + str = str.Replace(k, v.ToIconChar()); + return SeIconChar.LevelEn.ToIconChar() + str; + } +} diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs new file mode 100644 index 0000000..4685ded --- /dev/null +++ b/Craftimizer/Windows/RecipeNote.cs @@ -0,0 +1,542 @@ +using Craftimizer.Plugin; +using Craftimizer.Plugin.Utils; +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; +using Craftimizer.Utils; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Raii; +using Dalamud.Interface.Windowing; +using Dalamud.Logging; +using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; +using ImGuiScene; +using System; +using System.Linq; +using System.Numerics; +using ActionType = Craftimizer.Simulator.Actions.ActionType; +using ClassJob = Craftimizer.Simulator.ClassJob; +using CSRecipeNote = FFXIVClientStructs.FFXIV.Client.Game.UI.RecipeNote; + +namespace Craftimizer.Windows; + +public sealed unsafe class RecipeNote : Window, IDisposable +{ + private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration + | ImGuiWindowFlags.AlwaysAutoResize + | ImGuiWindowFlags.NoSavedSettings + | ImGuiWindowFlags.NoFocusOnAppearing + | ImGuiWindowFlags.NoNavFocus; + + public enum CraftableStatus + { + OK, + LockedClassJob, + WrongClassJob, + SpecialistRequired, + RequiredItem, + RequiredStatus, + CraftsmanshipTooLow, + ControlTooLow, + } + + public AddonRecipeNote* Addon { get; private set; } + public RecipeData? RecipeData { get; private set; } + public CharacterStats? CharacterStats { get; private set; } + public CraftableStatus CraftStatus { get; private set; } + + private TextureWrap ExpertBadge { get; } + private TextureWrap CollectibleBadge { get; } + private TextureWrap SplendorousBadge { get; } + private TextureWrap SpecialistBadge { get; } + private TextureWrap NoManipulationBadge { get; } + private GameFontHandle AxisFont { get; } + + public RecipeNote() : base("Craftimizer RecipeNode", WindowFlags, false) + { + ExpertBadge = Service.IconManager.GetAssemblyTexture("Graphics.expert_badge.png"); + CollectibleBadge = Service.IconManager.GetAssemblyTexture("Graphics.collectible_badge.png"); + SplendorousBadge = Service.IconManager.GetAssemblyTexture("Graphics.splendorous.png"); + SpecialistBadge = Service.IconManager.GetAssemblyTexture("Graphics.specialist.png"); + NoManipulationBadge = Service.IconManager.GetAssemblyTexture("Graphics.no_manip.png"); + AxisFont = Service.PluginInterface.UiBuilder.GetGameFontHandle(new(GameFontFamilyAndSize.Axis14)); + + Service.WindowSystem.AddWindow(this); + + IsOpen = true; + } + + public override bool DrawConditions() + { + if (Service.ClientState.LocalPlayer == null) + return false; + + { + Addon = (AddonRecipeNote*)Service.GameGui.GetAddonByName("RecipeNote"); + if (Addon == null) + return false; + + // Check if RecipeNote addon is visible + if (Addon->AtkUnitBase.WindowNode == null) + return false; + + // Check if RecipeNote has a visible selected recipe + if (!Addon->Unk258->IsVisible) + return false; + } + + { + var instance = CSRecipeNote.Instance(); + + var list = instance->RecipeList; + if (list == null) + return false; + + var recipeEntry = list->SelectedRecipe; + if (recipeEntry == null) + return false; + + var recipeId = recipeEntry->RecipeId; + RecipeData = new(recipeId); + } + + Gearsets.GearsetItem[] gearItems; + { + var gearStats = Gearsets.CalculateGearsetCurrentStats(); + + var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems); + if (container == null) + return false; + + gearItems = Gearsets.GetGearsetItems(container); + + CharacterStats = Gearsets.CalculateCharacterStats(gearStats, gearItems, RecipeData.ClassJob.GetPlayerLevel(), RecipeData.ClassJob.CanPlayerUseManipulation()); + } + + CraftStatus = CalculateCraftStatus(gearItems); + + return true; + } + + public override void PreDraw() + { + ref var unit = ref Addon->AtkUnitBase; + var scale = unit.Scale; + var pos = new Vector2(unit.X, unit.Y); + var size = new Vector2(unit.WindowNode->AtkResNode.Width, unit.WindowNode->AtkResNode.Height) * scale; + + var node = (AtkResNode*)Addon->Unk458; // unit.GetNodeById(59); + var nodeParent = Addon->Unk258; // unit.GetNodeById(57); + + Position = pos + new Vector2(size.X, (nodeParent->Y + node->Y) * scale); + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new(-1), + MaximumSize = new(10000, 10000) + }; + } + + public override void Draw() + { + using var table = ImRaii.Table("stats", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame); + if (table) + { + ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableNextColumn(); + DrawCharacterStats(); + ImGui.TableNextColumn(); + DrawRecipeStats(); + } + } + + private void DrawCharacterStats() + { + ImGuiUtils.TextCentered("Crafter"); + + var level = RecipeData!.ClassJob.GetPlayerLevel(); + { + var className = RecipeData.ClassJob.GetName(); + var levelText = string.Empty; + if (level != 0) + levelText = SqText.ToLevelString(level); + var imageSize = ImGuiUtils.ButtonHeight; + bool hasSplendorous = false, hasSpecialist = false, shouldHaveManip = false; + if (CraftStatus is not (CraftableStatus.LockedClassJob or CraftableStatus.WrongClassJob)) + { + hasSplendorous = CharacterStats!.HasSplendorousBuff; + hasSpecialist = CharacterStats!.IsSpecialist; + shouldHaveManip = !CharacterStats.CanUseManipulation && CharacterStats.Level >= ActionType.Manipulation.Level(); + } + + ImGuiUtils.AlignCentered( + imageSize + 5 + + ImGui.CalcTextSize(className).X + + (level == 0 ? 0 : (3 + ImGui.CalcTextSize(levelText).X)) + + (hasSplendorous ? (3 + imageSize) : 0) + + (hasSpecialist ? (3 + imageSize) : 0) + + (shouldHaveManip ? (3 + imageSize) : 0) + ); + ImGui.AlignTextToFramePadding(); + + ImGui.Image(Service.IconManager.GetIcon(RecipeData.ClassJob.GetIconId()).ImGuiHandle, new Vector2(imageSize)); + ImGui.SameLine(0, 5); + + if (level != 0) + { + ImGui.Text(levelText); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"CLvl {Gearsets.CalculateCLvl(level)}"); + ImGui.SameLine(0, 3); + } + + ImGui.Text(className); + + if (hasSplendorous) + { + ImGui.SameLine(0, 3); + ImGui.Image(SplendorousBadge.ImGuiHandle, new Vector2(imageSize)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Splendorous Tool"); + } + + if (hasSpecialist) + { + ImGui.SameLine(0, 3); + ImGui.Image(SpecialistBadge.ImGuiHandle, new Vector2(imageSize), Vector2.Zero, Vector2.One, new(0.99f, 0.97f, 0.62f, 1f)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Specialist"); + } + + if (shouldHaveManip) + { + ImGui.SameLine(0, 3); + ImGui.Image(NoManipulationBadge.ImGuiHandle, new Vector2(imageSize)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"No Manipulation (Missing Job Quest)"); + } + } + + ImGui.Separator(); + + switch (CraftStatus) + { + case CraftableStatus.LockedClassJob: + { + ImGuiUtils.TextCentered($"You do not have {RecipeData.ClassJob.GetName().ToLowerInvariant()} unlocked."); + ImGui.Separator(); + var unlockQuest = RecipeData.ClassJob.GetUnlockQuest(); + var (questGiver, questTerritory, questLocation, mapPayload) = ResolveLevelData(unlockQuest.IssuerLocation.Row); + + var unlockText = $"Unlock it from {questGiver}"; + ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGuiUtils.ButtonHeight); + ImGui.AlignTextToFramePadding(); + ImGui.Text(unlockText); + ImGui.SameLine(0, 5); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Flag)) + Service.GameGui.OpenMapWithMapLink(mapPayload); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Open in map"); + + ImGuiUtils.TextCentered($"{questTerritory} ({questLocation.X:0.0}, {questLocation.Y:0.0})"); + } + break; + case CraftableStatus.WrongClassJob: + { + ImGuiUtils.TextCentered($"You are not a {RecipeData.ClassJob.GetName().ToLowerInvariant()}."); + var gearsetId = GetGearsetForJob(RecipeData.ClassJob); + if (gearsetId.HasValue) + { + if (ImGuiUtils.ButtonCentered("Switch Job")) + Chat.SendMessage($"/gearset change {gearsetId + 1}"); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Swap to gearset {gearsetId + 1}"); + } + else + ImGuiUtils.TextCentered($"You do not have any {RecipeData.ClassJob.GetName().ToLowerInvariant()} gearsets."); + } + break; + case CraftableStatus.SpecialistRequired: + { + ImGuiUtils.TextCentered($"You need to be a specialist to craft this recipe."); + + var (vendorName, vendorTerritory, vendorLoation, mapPayload) = ResolveLevelData(5891399); + + var unlockText = $"Trade a Soul of the Crafter to {vendorName}"; + ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGuiUtils.ButtonHeight); + ImGui.AlignTextToFramePadding(); + ImGui.Text(unlockText); + ImGui.SameLine(0, 5); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Flag)) + Service.GameGui.OpenMapWithMapLink(mapPayload); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Open in map"); + + ImGuiUtils.TextCentered($"{vendorTerritory} ({vendorLoation.X:0.0}, {vendorLoation.Y:0.0})"); + } + break; + case CraftableStatus.RequiredItem: + { + var item = RecipeData.Recipe.ItemRequired.Value!; + var itemName = item.Name.ToDalamudString().ToString(); + var imageSize = ImGuiUtils.ButtonHeight; + + ImGuiUtils.TextCentered($"You are missing the required equipment."); + ImGuiUtils.AlignCentered(imageSize + 5 + ImGui.CalcTextSize(itemName).X); + ImGui.AlignTextToFramePadding(); + ImGui.Image(Service.IconManager.GetIcon(item.Icon).ImGuiHandle, new(imageSize)); + ImGui.SameLine(0, 5); + ImGui.Text(itemName); + } + break; + case CraftableStatus.RequiredStatus: + { + var status = RecipeData.Recipe.StatusRequired.Value!; + var statusName = status.Name.ToDalamudString().ToString(); + var statusIcon = Service.IconManager.GetIcon(status.Icon); + var imageSize = new Vector2(ImGuiUtils.ButtonHeight * statusIcon.Width / statusIcon.Height, ImGuiUtils.ButtonHeight); + + ImGuiUtils.TextCentered($"You are missing the required status effect."); + ImGuiUtils.AlignCentered(imageSize.X + 5 + ImGui.CalcTextSize(statusName).X); + ImGui.AlignTextToFramePadding(); + ImGui.Image(statusIcon.ImGuiHandle, imageSize); + ImGui.SameLine(0, 5); + ImGui.Text(statusName); + } + break; + case CraftableStatus.CraftsmanshipTooLow: + { + ImGuiUtils.TextCentered("Your Craftsmanship is too low."); + + DrawRequiredStatsTable(CharacterStats!.Craftsmanship, RecipeData.Recipe.RequiredCraftsmanship); + } + break; + case CraftableStatus.ControlTooLow: + { + ImGuiUtils.TextCentered("Your Control is too low."); + + DrawRequiredStatsTable(CharacterStats!.Control, RecipeData.Recipe.RequiredControl); + } + break; + case CraftableStatus.OK: + { + using var table = ImRaii.Table("characterStats", 2, ImGuiTableFlags.NoHostExtendX); + if (table) + { + ImGui.TableSetupColumn("ccol1", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("ccol2", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + ImGui.Text("Craftsmanship"); + ImGui.TableNextColumn(); + ImGui.Text($"{CharacterStats!.Craftsmanship}"); + + ImGui.TableNextColumn(); + ImGui.Text("Control"); + ImGui.TableNextColumn(); + ImGui.Text($"{CharacterStats.Control}"); + + ImGui.TableNextColumn(); + ImGui.Text("CP"); + ImGui.TableNextColumn(); + ImGui.Text($"{CharacterStats.CP}"); + } + } + break; + } + } + + private void DrawRecipeStats() + { + ImGuiUtils.TextCentered("Recipe"); + + { + var textStars = new string('★', RecipeData!.Table.Stars); + var textStarsSize = Vector2.Zero; + if (!string.IsNullOrEmpty(textStars)) { + var layout = AxisFont.LayoutBuilder(textStars).Build(); + textStarsSize = new(layout.Width + 3, layout.Height); + } + var textLevel = SqText.ToLevelString(RecipeData.RecipeInfo.ClassJobLevel); + var isExpert = RecipeData.RecipeInfo.IsExpert; + var isCollectable = RecipeData.Recipe.ItemResult.Value!.IsCollectable; + var imageSize = ImGuiUtils.ButtonHeight; + var textSize = ImGui.CalcTextSize("A").Y; + var badgeSize = new Vector2(textSize * ExpertBadge.Width / ExpertBadge.Height, textSize); + var badgeOffset = (imageSize - badgeSize.Y) / 2; + + ImGuiUtils.AlignCentered( + imageSize + 5 + + ImGui.CalcTextSize(textLevel).X + + textStarsSize.X + + (isCollectable ? badgeSize.X + 3 : 0) + + (isExpert ? badgeSize.X + 3 : 0) + ); + ImGui.AlignTextToFramePadding(); + + ImGui.Image(Service.IconManager.GetIcon(RecipeData.Recipe.ItemResult.Value!.Icon).ImGuiHandle, new Vector2(imageSize)); + + ImGui.SameLine(0, 5); + ImGui.Text(textLevel); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"RLvl {RecipeData.RecipeInfo.RLvl}"); + + if (textStarsSize != Vector2.Zero) + { + ImGui.SameLine(0, 3); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (textStarsSize.Y - textSize) / 2); + AxisFont.Text(textStars); + } + + if (isCollectable) + { + ImGui.SameLine(0, 3); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + badgeOffset); + ImGui.Image(CollectibleBadge.ImGuiHandle, badgeSize); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Collectible"); + } + + if (isExpert) + { + ImGui.SameLine(0, 3); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + badgeOffset); + ImGui.Image(ExpertBadge.ImGuiHandle, badgeSize); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Expert Recipe"); + } + } + + using var table = ImRaii.Table("recipeStats", 2); + if (table) + { + ImGui.TableSetupColumn("rcol1", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("rcol2", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + ImGui.Text("Progress"); + ImGui.TableNextColumn(); + ImGui.Text($"{RecipeData.RecipeInfo.MaxProgress}"); + + ImGui.TableNextColumn(); + ImGui.Text("Quality"); + ImGui.TableNextColumn(); + ImGui.Text($"{RecipeData.RecipeInfo.MaxQuality}"); + + ImGui.TableNextColumn(); + ImGui.Text("Durability"); + ImGui.TableNextColumn(); + ImGui.Text($"{RecipeData.RecipeInfo.MaxDurability}"); + } + } + + private static void DrawRequiredStatsTable(int current, int required) + { + if (current >= required) + throw new ArgumentOutOfRangeException(nameof(current)); + + using var table = ImRaii.Table("requiredStats", 2, ImGuiTableFlags.NoHostExtendX); + if (table) + { + ImGui.TableSetupColumn("ccol1", ImGuiTableColumnFlags.WidthFixed, 100); + ImGui.TableSetupColumn("ccol2", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + ImGui.Text("Current"); + ImGui.TableNextColumn(); + ImGui.TextColored(new(0, 1, 0, 1), $"{current}"); + + ImGui.TableNextColumn(); + ImGui.Text("Required"); + ImGui.TableNextColumn(); + ImGui.TextColored(new(1, 0, 0, 1), $"{required}"); + + ImGui.TableNextColumn(); + ImGui.Text("You need"); + ImGui.TableNextColumn(); + ImGui.Text($"{required - current}"); + } + } + + private CraftableStatus CalculateCraftStatus(Gearsets.GearsetItem[] gearItems) + { + if (RecipeData!.ClassJob.GetPlayerLevel() == 0) + return CraftableStatus.LockedClassJob; + + if (PlayerState.Instance()->CurrentClassJobId != RecipeData.ClassJob.GetClassJobIndex()) + return CraftableStatus.WrongClassJob; + + if (RecipeData.Recipe.IsSpecializationRequired && !(CharacterStats!.IsSpecialist)) + return CraftableStatus.SpecialistRequired; + + var itemRequired = RecipeData.Recipe.ItemRequired; + if (itemRequired.Row != 0 && itemRequired.Value != null) + { + if (!gearItems.Any(i => Gearsets.IsItem(i, itemRequired.Row))) + return CraftableStatus.RequiredItem; + } + + var statusRequired = RecipeData.Recipe.StatusRequired; + if (statusRequired.Row != 0 && statusRequired.Value != null) + { + if (!Service.ClientState.LocalPlayer!.StatusList.Any(s => s.StatusId == statusRequired.Row)) + return CraftableStatus.RequiredStatus; + } + + if (RecipeData.Recipe.RequiredCraftsmanship > CharacterStats!.Craftsmanship) + return CraftableStatus.CraftsmanshipTooLow; + + if (RecipeData.Recipe.RequiredControl > CharacterStats.Control) + return CraftableStatus.ControlTooLow; + + return CraftableStatus.OK; + } + + private static (string NpcName, string Territory, Vector2 MapLocation, MapLinkPayload Payload) ResolveLevelData(uint levelRowId) + { + var level = LuminaSheets.LevelSheet.GetRow(levelRowId) ?? + throw new ArgumentNullException(nameof(levelRowId), $"Invalid level row {levelRowId}"); + var territory = level.Territory.Value!.PlaceName.Value!.Name.ToDalamudString().ToString(); + var location = MapUtil.WorldToMap(new(level.X, level.Z), level.Map.Value!); + + return (ResolveNpcResidentName(level.Object), territory, location, new(level.Territory.Row, level.Map.Row, location.X, location.Y)); + } + + private static string ResolveNpcResidentName(uint npcRowId) + { + var resident = LuminaSheets.ENpcResidentSheet.GetRow(npcRowId) ?? + throw new ArgumentNullException(nameof(npcRowId), $"Invalid npc row {npcRowId}"); + return resident.Singular.ToDalamudString().ToString(); + } + + private static int? GetGearsetForJob(ClassJob job) + { + var gearsetModule = RaptureGearsetModule.Instance(); + for (var i = 0; i < 100; i++) + { + var gearset = gearsetModule->Gearset[i]; + if (gearset == null) + continue; + if (!gearset->Flags.HasFlag(RaptureGearsetModule.GearsetFlag.Exists)) + continue; + if (gearset->ID != i) + continue; + if (gearset->ClassJob != job.GetClassJobIndex()) + continue; + return i; + } + return null; + } + + public void Dispose() + { + AxisFont?.Dispose(); + } +} diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index e55ada3..1088b3a 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -473,7 +473,7 @@ private void DrawTabAbout() ImGui.Image(icon.ImGuiHandle, new(icon.Width, icon.Height)); ImGui.TableNextColumn(); - ImGui.Text($"{plugin.Name} v{plugin.Version} {plugin.Configuration}"); + ImGui.Text($"{plugin.Name} v{plugin.Version} {plugin.BuildConfiguration}"); ImGui.Text($"By {plugin.Author} ("); ImGui.SameLine(0, 0); ImGuiUtils.Hyperlink("WorkingRobot", "https://github.com/WorkingRobot"); diff --git a/Craftimizer/Windows/SimulatorDrawer.cs b/Craftimizer/Windows/SimulatorDrawer.cs index 9ad426e..acc0d57 100644 --- a/Craftimizer/Windows/SimulatorDrawer.cs +++ b/Craftimizer/Windows/SimulatorDrawer.cs @@ -133,7 +133,7 @@ private void DrawSimulationHeader() { var imageSize = new Vector2(ImGui.GetFontSize() * 2.25f); - ImGui.Image(Icons.GetIconFromId(Item.Icon).ImGuiHandle, imageSize); + ImGui.Image(Service.IconManager.GetIcon(Item.Icon).ImGuiHandle, imageSize); ImGui.SameLine(); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize.Y - ImGui.GetFontSize()) / 2f); ImGui.TextUnformatted(Item.Name.ToDalamudString().ToString()); From cfb5c53f3bb82995afa4d1f700ee8c4e4be3f94e Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Mon, 2 Oct 2023 12:16:01 -0700 Subject: [PATCH 05/58] Make crafting log ui nicer, remove redundant code --- Benchmark/Program.cs | 4 +- Craftimizer/SimulatorUtils.cs | 8 ++- Craftimizer/Windows/RecipeNote.cs | 81 ++++++++++++++++++++++++------- Solver/Crafty/Solver.cs | 29 ++--------- 4 files changed, 78 insertions(+), 44 deletions(-) diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index 832c8cd..377620b 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -77,11 +77,11 @@ private static void Main() Console.WriteLine($"{state.Quality} {state.CP} {state.Progress} {state.Durability}"); //return; - var (_, s) = Solver.Crafty.Solver.SearchStepwiseFurcated(config, state, a => Console.WriteLine(a)); + var (_, s) = Solver.Crafty.Solver.SearchStepwiseFurcated(config, state, a => Console.WriteLine(a), default); Console.WriteLine($"Qual: {s.Quality}/{s.Input.Recipe.MaxQuality}"); return; - Solver.Crafty.Solver.SearchStepwiseFurcated(config, input); + Solver.Crafty.Solver.SearchStepwiseFurcated(config, new(input), null, default); //Benchmark(() => ); } diff --git a/Craftimizer/SimulatorUtils.cs b/Craftimizer/SimulatorUtils.cs index 4d30def..806f40a 100644 --- a/Craftimizer/SimulatorUtils.cs +++ b/Craftimizer/SimulatorUtils.cs @@ -162,11 +162,17 @@ public static string GetName(this ClassJob me) return CultureInfo.InvariantCulture.TextInfo.ToTitleCase(job.Name.ToDalamudString().TextValue); } + public static string GetAbbreviation(this ClassJob me) + { + var job = LuminaSheets.ClassJobSheet.GetRow(me.GetClassJobIndex())!; + return job.Abbreviation.ToDalamudString().TextValue; + } + public static Quest GetUnlockQuest(this ClassJob me) => LuminaSheets.QuestSheet.GetRow(65720 + (uint)me) ?? throw new ArgumentException($"Could not get unlock quest for {me}", nameof(me)); public static ushort GetIconId(this ClassJob me) => - (ushort)(62100 + me.GetClassJobIndex()); + (ushort)(62000 + me.GetClassJobIndex()); public static bool IsClassJob(this ClassJobCategory me, ClassJob classJob) => classJob switch diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index 4685ded..ee348fe 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -9,7 +9,6 @@ using Dalamud.Interface.GameFonts; using Dalamud.Interface.Raii; using Dalamud.Interface.Windowing; -using Dalamud.Logging; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.UI; @@ -92,6 +91,7 @@ public override bool DrawConditions() return false; } + var statsChanged = false; { var instance = CSRecipeNote.Instance(); @@ -104,7 +104,11 @@ public override bool DrawConditions() return false; var recipeId = recipeEntry->RecipeId; - RecipeData = new(recipeId); + if (recipeId != RecipeData?.RecipeId) + { + RecipeData = new(recipeId); + statsChanged = true; + } } Gearsets.GearsetItem[] gearItems; @@ -117,10 +121,23 @@ public override bool DrawConditions() gearItems = Gearsets.GetGearsetItems(container); - CharacterStats = Gearsets.CalculateCharacterStats(gearStats, gearItems, RecipeData.ClassJob.GetPlayerLevel(), RecipeData.ClassJob.CanPlayerUseManipulation()); + var characterStats = Gearsets.CalculateCharacterStats(gearStats, gearItems, RecipeData.ClassJob.GetPlayerLevel(), RecipeData.ClassJob.CanPlayerUseManipulation()); + if (characterStats != CharacterStats) + { + CharacterStats = characterStats; + statsChanged = true; + } + } + + var craftStatus = CalculateCraftStatus(gearItems); + if (craftStatus != CraftStatus) + { + CraftStatus = craftStatus; + statsChanged = true; } - CraftStatus = CalculateCraftStatus(gearItems); + if (statsChanged && CraftStatus == CraftableStatus.OK) + CalculateBestMacros(); return true; } @@ -145,16 +162,28 @@ public override void PreDraw() public override void Draw() { - using var table = ImRaii.Table("stats", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame); - if (table) { - ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableNextColumn(); - DrawCharacterStats(); - ImGui.TableNextColumn(); - DrawRecipeStats(); + using var table = ImRaii.Table("stats", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame); + if (table) + { + ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableNextColumn(); + DrawCharacterStats(); + ImGui.TableNextColumn(); + DrawRecipeStats(); + } } + + if (CraftStatus != CraftableStatus.OK) + return; + + ImGui.Separator(); + + ImGuiUtils.TextCentered("Best Saved Macro"); + ImGuiUtils.ButtonCentered("View Saved Macros"); + ImGuiUtils.TextCentered("Suggested Macro"); + ImGuiUtils.ButtonCentered("Open Simulator"); } private void DrawCharacterStats() @@ -163,7 +192,12 @@ private void DrawCharacterStats() var level = RecipeData!.ClassJob.GetPlayerLevel(); { - var className = RecipeData.ClassJob.GetName(); + var textClassName = RecipeData.ClassJob.GetAbbreviation(); + Vector2 textClassSize; + { + var layout = AxisFont.LayoutBuilder(textClassName).Build(); + textClassSize = new(layout.Width, layout.Height); + } var levelText = string.Empty; if (level != 0) levelText = SqText.ToLevelString(level); @@ -178,7 +212,7 @@ private void DrawCharacterStats() ImGuiUtils.AlignCentered( imageSize + 5 + - ImGui.CalcTextSize(className).X + + textClassSize.X + (level == 0 ? 0 : (3 + ImGui.CalcTextSize(levelText).X)) + (hasSplendorous ? (3 + imageSize) : 0) + (hasSpecialist ? (3 + imageSize) : 0) + @@ -186,7 +220,12 @@ private void DrawCharacterStats() ); ImGui.AlignTextToFramePadding(); - ImGui.Image(Service.IconManager.GetIcon(RecipeData.ClassJob.GetIconId()).ImGuiHandle, new Vector2(imageSize)); + var uv0 = new Vector2(6, 3); + var uv1 = uv0 + new Vector2(44); + uv0 /= new Vector2(56); + uv1 /= new Vector2(56); + + ImGui.Image(Service.IconManager.GetIcon(RecipeData.ClassJob.GetIconId()).ImGuiHandle, new Vector2(imageSize), uv0, uv1); ImGui.SameLine(0, 5); if (level != 0) @@ -197,7 +236,8 @@ private void DrawCharacterStats() ImGui.SameLine(0, 3); } - ImGui.Text(className); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize - textClassSize.Y) / 2); + AxisFont.Text(textClassName); if (hasSplendorous) { @@ -391,7 +431,7 @@ private void DrawRecipeStats() if (textStarsSize != Vector2.Zero) { ImGui.SameLine(0, 3); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (textStarsSize.Y - textSize) / 2); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize - textStarsSize.Y) / 2); AxisFont.Text(textStars); } @@ -414,6 +454,8 @@ private void DrawRecipeStats() } } + ImGui.Separator(); + using var table = ImRaii.Table("recipeStats", 2); if (table) { @@ -535,6 +577,11 @@ private static string ResolveNpcResidentName(uint npcRowId) return null; } + private void CalculateBestMacros() + { + throw new NotImplementedException(); + } + public void Dispose() { AxisFont?.Dispose(); diff --git a/Solver/Crafty/Solver.cs b/Solver/Crafty/Solver.cs index f5192b3..0ed5d35 100644 --- a/Solver/Crafty/Solver.cs +++ b/Solver/Crafty/Solver.cs @@ -332,10 +332,7 @@ private void Search(int iterations, CancellationToken token) } } - public static SolverSolution SearchStepwiseFurcated(SolverConfig config, SimulationInput input, Action? actionCallback = null, CancellationToken token = default) => - SearchStepwiseFurcated(config, new SimulationState(input), actionCallback, token); - - public static SolverSolution SearchStepwiseFurcated(SolverConfig config, SimulationState state, Action? actionCallback = null, CancellationToken token = default) + public static SolverSolution SearchStepwiseFurcated(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) { var definiteActionCount = 0; var bestSims = new List<(float Score, SolverSolution Result)>(); @@ -445,10 +442,7 @@ public static SolverSolution SearchStepwiseFurcated(SolverConfig config, Simulat return result; } - public static SolverSolution SearchStepwiseForked(SolverConfig config, SimulationInput input, Action? actionCallback = null, CancellationToken token = default) => - SearchStepwiseForked(config, new SimulationState(input), actionCallback, token); - - public static SolverSolution SearchStepwiseForked(SolverConfig config, SimulationState state, Action? actionCallback = null, CancellationToken token = default) + public static SolverSolution SearchStepwiseForked(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) { var actions = new List(); var sim = new Simulator(state, config.MaxStepCount); @@ -495,10 +489,7 @@ public static SolverSolution SearchStepwiseForked(SolverConfig config, Simulatio return new(actions, state); } - public static SolverSolution SearchStepwise(SolverConfig config, SimulationInput input, Action? actionCallback = null, CancellationToken token = default) => - SearchStepwise(config, new SimulationState(input), actionCallback, token); - - public static SolverSolution SearchStepwise(SolverConfig config, SimulationState state, Action? actionCallback = null, CancellationToken token = default) + public static SolverSolution SearchStepwise(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) { var actions = new List(); var sim = new Simulator(state, config.MaxStepCount); @@ -535,10 +526,7 @@ public static SolverSolution SearchStepwise(SolverConfig config, SimulationState return new(actions, state); } - public static SolverSolution SearchOneshotForked(SolverConfig config, SimulationInput input, Action? actionCallback = null, CancellationToken token = default) => - SearchOneshotForked(config, new SimulationState(input), actionCallback, token); - - public static SolverSolution SearchOneshotForked(SolverConfig config, SimulationState state, Action? actionCallback = null, CancellationToken token = default) + public static SolverSolution SearchOneshotForked(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) { var tasks = new Task<(float MaxScore, SolverSolution Solution)>[config.ForkCount]; for (var i = 0; i < config.ForkCount; ++i) @@ -557,10 +545,7 @@ public static SolverSolution SearchOneshotForked(SolverConfig config, Simulation return solution; } - public static SolverSolution SearchOneshot(SolverConfig config, SimulationInput input, Action? actionCallback = null, CancellationToken token = default) => - SearchOneshot(config, new SimulationState(input), actionCallback, token); - - public static SolverSolution SearchOneshot(SolverConfig config, SimulationState state, Action? actionCallback = null, CancellationToken token = default) + public static SolverSolution SearchOneshot(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) { var solver = new Solver(config, state); solver.Search(config.Iterations, token); @@ -571,9 +556,6 @@ public static SolverSolution SearchOneshot(SolverConfig config, SimulationState return solution; } - public static SolverSolution Search(SolverConfig config, SimulationInput input, Action? actionCallback = null, CancellationToken token = default) => - Search(config, new SimulationState(input), actionCallback, token); - public static SolverSolution Search(SolverConfig config, SimulationState state, Action? actionCallback = null, CancellationToken token = default) { Func?, CancellationToken, SolverSolution> func = config.Algorithm switch @@ -586,5 +568,4 @@ public static SolverSolution Search(SolverConfig config, SimulationState state, }; return func(config, state, actionCallback, token); } - } From e6081f5e606a8fb6bb0b4f0390ffc7e89071aa69 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Mon, 2 Oct 2023 12:16:43 -0700 Subject: [PATCH 06/58] Remove crafty namespace from solver --- Benchmark/Program.cs | 6 +++--- Craftimizer/Configuration.cs | 4 ++-- Craftimizer/Windows/Settings.cs | 2 +- Solver/{Crafty => }/ActionSet.cs | 2 +- Solver/{Crafty => }/ArenaBuffer.cs | 2 +- Solver/{Crafty => }/ArenaNode.cs | 2 +- Solver/{Crafty => }/CompletionState.cs | 2 +- Solver/{Crafty => }/Intrinsics.cs | 4 ++-- Solver/{Crafty => }/NodeScoresBuffer.cs | 2 +- Solver/{Crafty => }/RootScores.cs | 2 +- Solver/{Crafty => }/SimulationNode.cs | 4 ++-- Solver/{Crafty => }/Simulator.cs | 6 +++--- Solver/{Crafty => }/Solver.cs | 6 +++--- Solver/{Crafty => }/SolverConfig.cs | 2 +- Solver/{Crafty => }/SolverSolution.cs | 2 +- 15 files changed, 24 insertions(+), 24 deletions(-) rename Solver/{Crafty => }/ActionSet.cs (98%) rename Solver/{Crafty => }/ArenaBuffer.cs (97%) rename Solver/{Crafty => }/ArenaNode.cs (95%) rename Solver/{Crafty => }/CompletionState.cs (91%) rename Solver/{Crafty => }/Intrinsics.cs (98%) rename Solver/{Crafty => }/NodeScoresBuffer.cs (98%) rename Solver/{Crafty => }/RootScores.cs (89%) rename Solver/{Crafty => }/SimulationNode.cs (94%) rename Solver/{Crafty => }/Simulator.cs (97%) rename Solver/{Crafty => }/Solver.cs (99%) rename Solver/{Crafty => }/SolverConfig.cs (98%) rename Solver/{Crafty => }/SolverSolution.cs (81%) diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index 377620b..8b4b06f 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -1,6 +1,6 @@ using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; -using Craftimizer.Solver.Crafty; +using Craftimizer.Solver; using System.Diagnostics; namespace Craftimizer.Benchmark; @@ -77,11 +77,11 @@ private static void Main() Console.WriteLine($"{state.Quality} {state.CP} {state.Progress} {state.Durability}"); //return; - var (_, s) = Solver.Crafty.Solver.SearchStepwiseFurcated(config, state, a => Console.WriteLine(a), default); + var (_, s) = Solver.Solver.SearchStepwiseFurcated(config, state, a => Console.WriteLine(a), default); Console.WriteLine($"Qual: {s.Quality}/{s.Input.Recipe.MaxQuality}"); return; - Solver.Crafty.Solver.SearchStepwiseFurcated(config, new(input), null, default); + Solver.Solver.SearchStepwiseFurcated(config, new(input), null, default); //Benchmark(() => ); } diff --git a/Craftimizer/Configuration.cs b/Craftimizer/Configuration.cs index 1b895e2..f90d4f4 100644 --- a/Craftimizer/Configuration.cs +++ b/Craftimizer/Configuration.cs @@ -1,6 +1,6 @@ using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; -using Craftimizer.Solver.Crafty; +using Craftimizer.Solver; using Dalamud.Configuration; using System; using System.Collections.Generic; @@ -21,7 +21,7 @@ public static void Invoke(this SolverConfig me, SimulationState state, Action where T : struct diff --git a/Solver/Crafty/ArenaNode.cs b/Solver/ArenaNode.cs similarity index 95% rename from Solver/Crafty/ArenaNode.cs rename to Solver/ArenaNode.cs index 5ac88ea..5d65e9f 100644 --- a/Solver/Crafty/ArenaNode.cs +++ b/Solver/ArenaNode.cs @@ -1,6 +1,6 @@ using System.Runtime.CompilerServices; -namespace Craftimizer.Solver.Crafty; +namespace Craftimizer.Solver; public sealed class ArenaNode where T : struct { diff --git a/Solver/Crafty/CompletionState.cs b/Solver/CompletionState.cs similarity index 91% rename from Solver/Crafty/CompletionState.cs rename to Solver/CompletionState.cs index b7aabab..47d3297 100644 --- a/Solver/Crafty/CompletionState.cs +++ b/Solver/CompletionState.cs @@ -1,6 +1,6 @@ using CompState = Craftimizer.Simulator.CompletionState; -namespace Craftimizer.Solver.Crafty; +namespace Craftimizer.Solver; public enum CompletionState : byte { diff --git a/Solver/Crafty/Intrinsics.cs b/Solver/Intrinsics.cs similarity index 98% rename from Solver/Crafty/Intrinsics.cs rename to Solver/Intrinsics.cs index 1380ae9..df946a7 100644 --- a/Solver/Crafty/Intrinsics.cs +++ b/Solver/Intrinsics.cs @@ -4,7 +4,7 @@ using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; -namespace Craftimizer.Solver.Crafty; +namespace Craftimizer.Solver; internal static class Intrinsics { [Pure] @@ -50,7 +50,7 @@ private static int HMaxIndexAVX2(Vector v, int len) var vcmp = Avx.CompareEqual(vfilt, vmax); var mask = unchecked((uint)Avx2.MoveMask(vcmp.AsByte())); - var inverseIdx = BitOperations.LeadingZeroCount(mask << ((8 - len) << 2)) >> 2; + var inverseIdx = BitOperations.LeadingZeroCount(mask << (8 - len << 2)) >> 2; return len - 1 - inverseIdx; } diff --git a/Solver/Crafty/NodeScoresBuffer.cs b/Solver/NodeScoresBuffer.cs similarity index 98% rename from Solver/Crafty/NodeScoresBuffer.cs rename to Solver/NodeScoresBuffer.cs index fa394bb..fecfb40 100644 --- a/Solver/Crafty/NodeScoresBuffer.cs +++ b/Solver/NodeScoresBuffer.cs @@ -2,7 +2,7 @@ using System.Numerics; using System.Runtime.CompilerServices; -namespace Craftimizer.Solver.Crafty; +namespace Craftimizer.Solver; // Adapted from https://github.com/dtao/ConcurrentList/blob/4fcf1c76e93021a41af5abb2d61a63caeba2adad/ConcurrentList/ConcurrentList.cs public struct NodeScoresBuffer diff --git a/Solver/Crafty/RootScores.cs b/Solver/RootScores.cs similarity index 89% rename from Solver/Crafty/RootScores.cs rename to Solver/RootScores.cs index 2ac7c2f..43b8420 100644 --- a/Solver/Crafty/RootScores.cs +++ b/Solver/RootScores.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Craftimizer.Solver.Crafty; +namespace Craftimizer.Solver; [StructLayout(LayoutKind.Auto)] public sealed class RootScores diff --git a/Solver/Crafty/SimulationNode.cs b/Solver/SimulationNode.cs similarity index 94% rename from Solver/Crafty/SimulationNode.cs rename to Solver/SimulationNode.cs index 672ce13..af37ac5 100644 --- a/Solver/Crafty/SimulationNode.cs +++ b/Solver/SimulationNode.cs @@ -2,7 +2,7 @@ using Craftimizer.Simulator.Actions; using System.Runtime.InteropServices; -namespace Craftimizer.Solver.Crafty; +namespace Craftimizer.Solver; [StructLayout(LayoutKind.Auto)] public struct SimulationNode @@ -75,7 +75,7 @@ static float Apply(float bonus, float value, float target) => ); var fewerStepsScore = - config.ScoreFewerStepsBonus * (1f - ((float)(state.ActionCount + 1) / config.MaxStepCount)); + config.ScoreFewerStepsBonus * (1f - (float)(state.ActionCount + 1) / config.MaxStepCount); return progressScore + qualityScore + durabilityScore + cpScore + fewerStepsScore; } diff --git a/Solver/Crafty/Simulator.cs b/Solver/Simulator.cs similarity index 97% rename from Solver/Crafty/Simulator.cs rename to Solver/Simulator.cs index 1b532e5..9b610e5 100644 --- a/Solver/Crafty/Simulator.cs +++ b/Solver/Simulator.cs @@ -3,7 +3,7 @@ using System.Diagnostics.Contracts; using System.Runtime.CompilerServices; -namespace Craftimizer.Solver.Crafty; +namespace Craftimizer.Solver; public sealed class Simulator : SimulatorNoRandom { @@ -129,7 +129,7 @@ private bool CanUseAction(ActionType action, bool strict) if (wouldFinish) { // don't allow finishing the craft if there is significant quality remaining - if (Quality < (Input.Recipe.MaxQuality / 5)) + if (Quality < Input.Recipe.MaxQuality / 5) return false; } else @@ -188,7 +188,7 @@ public ActionSet AvailableActionsHeuristic(bool strict) } public static CompletionState CalculateCompletionState(SimulationState state, int maxStepCount) => - (state.ActionCount + 1) >= maxStepCount ? + state.ActionCount + 1 >= maxStepCount ? CompletionState.MaxActionCountReached : (CompletionState)CalculateCompletionState(state); } diff --git a/Solver/Crafty/Solver.cs b/Solver/Solver.cs similarity index 99% rename from Solver/Crafty/Solver.cs rename to Solver/Solver.cs index 0ed5d35..9f19d67 100644 --- a/Solver/Crafty/Solver.cs +++ b/Solver/Solver.cs @@ -5,9 +5,9 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Text; -using Node = Craftimizer.Solver.Crafty.ArenaNode; +using Node = Craftimizer.Solver.ArenaNode; -namespace Craftimizer.Solver.Crafty; +namespace Craftimizer.Solver; // https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/simulator.rs public sealed class Solver @@ -155,7 +155,7 @@ private static (int arrayIdx, int subIdx) ChildMaxScore(ref NodeScoresBuffer sco vInt = Vector.Max(vInt, Vector.One); var v = Vector.ConvertToSingle(vInt); - var exploitation = (W * (s / v)) + (w * m); + var exploitation = W * (s / v) + w * m; var exploration = CVector * Intrinsics.ReciprocalSqrt(v); var evalScores = exploitation + exploration; diff --git a/Solver/Crafty/SolverConfig.cs b/Solver/SolverConfig.cs similarity index 98% rename from Solver/Crafty/SolverConfig.cs rename to Solver/SolverConfig.cs index 259946b..15be1db 100644 --- a/Solver/Crafty/SolverConfig.cs +++ b/Solver/SolverConfig.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace Craftimizer.Solver.Crafty; +namespace Craftimizer.Solver; public enum SolverAlgorithm { diff --git a/Solver/Crafty/SolverSolution.cs b/Solver/SolverSolution.cs similarity index 81% rename from Solver/Crafty/SolverSolution.cs rename to Solver/SolverSolution.cs index 3fa7898..8a5ac54 100644 --- a/Solver/Crafty/SolverSolution.cs +++ b/Solver/SolverSolution.cs @@ -1,6 +1,6 @@ using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; -namespace Craftimizer.Solver.Crafty; +namespace Craftimizer.Solver; public readonly record struct SolverSolution(List Actions, SimulationState State); From c0f579f23be4d475bf6b2527f966a80919cbe168 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Mon, 2 Oct 2023 13:07:06 -0700 Subject: [PATCH 07/58] Split solver into MCTS and algorithms --- .editorconfig | 1 + Benchmark/Program.cs | 4 +- Craftimizer/Configuration.cs | 19 -- Simulator/CompletionState.cs | 4 +- Simulator/Simulator.cs | 22 +-- Solver/CompletionState.cs | 20 -- Solver/MCTS.cs | 338 +++++++++++++++++++++++++++++++++ Solver/MCTSConfig.cs | 38 ++++ Solver/SimulationNode.cs | 14 +- Solver/Simulator.cs | 16 +- Solver/Solver.cs | 353 ++--------------------------------- Solver/SolverConfig.cs | 21 ++- 12 files changed, 444 insertions(+), 406 deletions(-) delete mode 100644 Solver/CompletionState.cs create mode 100644 Solver/MCTS.cs create mode 100644 Solver/MCTSConfig.cs diff --git a/.editorconfig b/.editorconfig index 5af9930..0d8e909 100644 --- a/.editorconfig +++ b/.editorconfig @@ -158,6 +158,7 @@ dotnet_diagnostic.CA1841.severity = suggestion dotnet_diagnostic.CA1845.severity = suggestion dotnet_diagnostic.MA0011.severity = silent dotnet_diagnostic.MA0076.severity = silent +dotnet_diagnostic.MA0046.severity = silent dotnet_diagnostic.MA0002.severity = silent csharp_style_prefer_switch_expression = true:suggestion csharp_style_prefer_pattern_matching = true:silent diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index 8b4b06f..f5acf7b 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -77,11 +77,11 @@ private static void Main() Console.WriteLine($"{state.Quality} {state.CP} {state.Progress} {state.Durability}"); //return; - var (_, s) = Solver.Solver.SearchStepwiseFurcated(config, state, a => Console.WriteLine(a), default); + var (_, s) = config.Invoke(state, a => Console.WriteLine(a))!.Value; Console.WriteLine($"Qual: {s.Quality}/{s.Input.Recipe.MaxQuality}"); return; - Solver.Solver.SearchStepwiseFurcated(config, new(input), null, default); + config.Invoke(new(input)); //Benchmark(() => ); } diff --git a/Craftimizer/Configuration.cs b/Craftimizer/Configuration.cs index f90d4f4..efddaa8 100644 --- a/Craftimizer/Configuration.cs +++ b/Craftimizer/Configuration.cs @@ -15,25 +15,6 @@ public class Macro public List Actions { get; set; } = new(); } -public static class AlgorithmUtils -{ - public static void Invoke(this SolverConfig me, SimulationState state, Action? actionCallback = null, CancellationToken token = default) - { - try - { - Solver.Solver.Search(me, state, actionCallback, token); - } - catch (AggregateException e) - { - e.Handle(ex => ex is OperationCanceledException); - } - catch (OperationCanceledException) - { - - } - } -} - [Serializable] public class Configuration : IPluginConfiguration { diff --git a/Simulator/CompletionState.cs b/Simulator/CompletionState.cs index 88856d0..1cb429f 100644 --- a/Simulator/CompletionState.cs +++ b/Simulator/CompletionState.cs @@ -6,5 +6,7 @@ public enum CompletionState : byte ProgressComplete, NoMoreDurability, - Other + InvalidAction, + MaxActionCountReached, + NoMoreActions } diff --git a/Simulator/Simulator.cs b/Simulator/Simulator.cs index 2d8beb0..b049eb2 100644 --- a/Simulator/Simulator.cs +++ b/Simulator/Simulator.cs @@ -21,8 +21,17 @@ public class Simulator public bool IsFirstStep => State.StepCount == 0; - public CompletionState CompletionState => CalculateCompletionState(State); - public virtual bool IsComplete => CompletionState != CompletionState.Incomplete; + public virtual CompletionState CompletionState { + get + { + if (Progress >= Input.Recipe.MaxProgress) + return CompletionState.ProgressComplete; + if (Durability <= 0) + return CompletionState.NoMoreDurability; + return CompletionState.Incomplete; + } + } + public bool IsComplete => CompletionState != CompletionState.Incomplete; public IEnumerable AvailableActions => ActionUtils.AvailableActions(this); @@ -278,13 +287,4 @@ public void IncreaseProgress(float efficiency) => [MethodImpl(MethodImplOptions.AggressiveInlining)] public void IncreaseQuality(float efficiency) => IncreaseQualityRaw(CalculateQualityGain(efficiency, false)); - - public static CompletionState CalculateCompletionState(SimulationState state) - { - if (state.Progress >= state.Input.Recipe.MaxProgress) - return CompletionState.ProgressComplete; - if (state.Durability <= 0) - return CompletionState.NoMoreDurability; - return CompletionState.Incomplete; - } } diff --git a/Solver/CompletionState.cs b/Solver/CompletionState.cs deleted file mode 100644 index 47d3297..0000000 --- a/Solver/CompletionState.cs +++ /dev/null @@ -1,20 +0,0 @@ -using CompState = Craftimizer.Simulator.CompletionState; - -namespace Craftimizer.Solver; - -public enum CompletionState : byte -{ - Incomplete, - ProgressComplete, - NoMoreDurability, - - InvalidAction, - MaxActionCountReached, - NoMoreActions -} - -internal static class CompletionStateUtils -{ - public static CompState IntoBase(this CompletionState me) => - (CompState)me >= CompState.Other ? CompState.Other : (CompState)me; -} diff --git a/Solver/MCTS.cs b/Solver/MCTS.cs new file mode 100644 index 0000000..4f976fb --- /dev/null +++ b/Solver/MCTS.cs @@ -0,0 +1,338 @@ +using Craftimizer.Simulator.Actions; +using Craftimizer.Simulator; +using System.Diagnostics.Contracts; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text; +using Node = Craftimizer.Solver.ArenaNode; + +namespace Craftimizer.Solver; + +// https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/simulator.rs +public sealed class MCTS +{ + private readonly MCTSConfig config; + private readonly Node rootNode; + private readonly RootScores rootScores; + + public float MaxScore => rootScores.MaxScore; + + public MCTS(MCTSConfig config, SimulationState state) + { + this.config = config; + var sim = new Simulator(state, config.MaxStepCount); + rootNode = new(new( + state, + null, + sim.CompletionState, + sim.AvailableActionsHeuristic(config.StrictActions) + )); + rootScores = new(); + } + + private static SimulationNode Execute(Simulator simulator, SimulationState state, ActionType action, bool strict) + { + (_, var newState) = simulator.Execute(state, action); + return new( + newState, + action, + simulator.CompletionState, + simulator.AvailableActionsHeuristic(strict) + ); + } + + private static Node ExecuteActions(Simulator simulator, Node startNode, ReadOnlySpan actions, bool strict) + { + foreach (var action in actions) + { + var state = startNode.State; + if (state.IsComplete) + return startNode; + + if (!state.AvailableActions.HasAction(action)) + return startNode; + state.AvailableActions.RemoveAction(action); + + startNode = startNode.Add(Execute(simulator, state.State, action, strict)); + } + + return startNode; + } + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static (int arrayIdx, int subIdx) ChildMaxScore(ref NodeScoresBuffer scores) + { + var length = scores.Count; + var vecLength = Vector.Count; + + var max = (0, 0); + var maxScore = 0f; + for (var i = 0; length > 0; ++i) + { + var iterCount = Math.Min(vecLength, length); + + ref var chunk = ref scores.Data[i]; + var m = new Vector(chunk.MaxScore.Span); + + var idx = Intrinsics.HMaxIndex(m, iterCount); + + if (m[idx] >= maxScore) + { + max = (i, idx); + maxScore = m[idx]; + } + + length -= iterCount; + } + + return max; + } + + // Calculates the best child node to explore next + // Exploitation: ((1 - w) * (s / v)) + (w * m) + // Exploration: sqrt(c * ln(V) / v) + // w = maxScoreWeightingConstant + // s = score sum + // m = max score + // v = visits + // V = parentVisits + // c = explorationConstant + + // Somewhat based off of https://en.wikipedia.org/wiki/Monte_Carlo_tree_search#Exploration_and_exploitation + // Here, w_i = (1-w)*score sum + // n_i = visits + // max score is tacked onto it + // N_i = parent visits + // c = exploration constant (but crafty places it inside the sqrt..?) + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (int arrayIdx, int subIdx) EvalBestChild( + float explorationConstant, + float maxScoreWeightingConstant, + int parentVisits, + ref NodeScoresBuffer scores) + { + var length = scores.Count; + var vecLength = Vector.Count; + + var C = MathF.Sqrt(explorationConstant * MathF.Log(parentVisits)); + var w = maxScoreWeightingConstant; + var W = 1f - w; + var CVector = new Vector(C); + + var max = (0, 0); + var maxScore = 0f; + for (var i = 0; length > 0; ++i) + { + var iterCount = Math.Min(vecLength, length); + + ref var chunk = ref scores.Data[i]; + var s = new Vector(chunk.ScoreSum.Span); + var vInt = new Vector(chunk.Visits.Span); + var m = new Vector(chunk.MaxScore.Span); + + vInt = Vector.Max(vInt, Vector.One); + var v = Vector.ConvertToSingle(vInt); + + var exploitation = W * (s / v) + w * m; + var exploration = CVector * Intrinsics.ReciprocalSqrt(v); + var evalScores = exploitation + exploration; + + var idx = Intrinsics.HMaxIndex(evalScores, iterCount); + + if (evalScores[idx] >= maxScore) + { + max = (i, idx); + maxScore = evalScores[idx]; + } + + length -= iterCount; + } + + return max; + } + + [Pure] + private Node Select() + { + var node = rootNode; + var nodeVisits = rootScores.Visits; + + float explorationConstant = config.ExplorationConstant, maxScoreWeightingConstant = config.MaxScoreWeightingConstant; + while (true) + { + var expandable = !node.State.AvailableActions.IsEmpty; + var likelyTerminal = node.Children.Count == 0; + if (expandable || likelyTerminal) + return node; + + // select the node with the highest score + var at = EvalBestChild(explorationConstant, maxScoreWeightingConstant, nodeVisits, ref node.ChildScores); + nodeVisits = node.ChildScores.GetVisits(at); + node = node.ChildAt(at)!; + } + } + + private (Node ExpandedNode, float Score) ExpandAndRollout(Random random, Simulator simulator, Node initialNode) + { + ref var initialState = ref initialNode.State; + // expand once + if (initialState.IsComplete) + return (initialNode, initialState.CalculateScore(config) ?? 0); + + var poppedAction = initialState.AvailableActions.PopRandom(random); + var expandedNode = initialNode.Add(Execute(simulator, initialState.State, poppedAction, true)); + + // playout to a terminal state + var currentState = expandedNode.State.State; + var currentCompletionState = expandedNode.State.SimulationCompletionState; + var currentActions = expandedNode.State.AvailableActions; + + + byte actionCount = 0; + Span actions = stackalloc ActionType[Math.Min(config.MaxStepCount - currentState.ActionCount, config.MaxRolloutStepCount)]; + while (SimulationNode.GetCompletionState(currentCompletionState, currentActions) == CompletionState.Incomplete && + actionCount < actions.Length) + { + var nextAction = currentActions.SelectRandom(random); + actions[actionCount++] = nextAction; + (_, currentState) = simulator.Execute(currentState, nextAction); + currentCompletionState = simulator.CompletionState; + if (currentCompletionState != CompletionState.Incomplete) + break; + currentActions = simulator.AvailableActionsHeuristic(true); + } + + // store the result if a max score was reached + var score = SimulationNode.CalculateScoreForState(currentState, currentCompletionState, config) ?? 0; + if (currentCompletionState == CompletionState.ProgressComplete) + { + if (score >= config.ScoreStorageThreshold && score >= MaxScore) + { + var terminalNode = ExecuteActions(simulator, expandedNode, actions[..actionCount], true); + return (terminalNode, score); + } + } + return (expandedNode, score); + } + + private void Backpropagate(Node startNode, float score) + { + while (true) + { + if (startNode == rootNode) + { + rootScores.Visit(score); + break; + } + startNode.ParentScores!.Value.Visit(startNode.ChildIdx, score); + + startNode = startNode.Parent!; + } + } + + private void ShowAllNodes() + { + static void ShowNodes(StringBuilder b, Node node, Stack path) + { + path.Push(node); + b.AppendLine($"{new string(' ', path.Count)}{node.State.Action}"); + { + for (var i = 0; i < node.Children.Count; ++i) + { + var n = node.ChildAt((i >> 3, i & 7))!; + ShowNodes(b, n, path); + } + path.Pop(); + } + } + var b = new StringBuilder(); + ShowNodes(b, rootNode, new()); + Console.WriteLine(b.ToString()); + } + + private bool AllNodesComplete() + { + static bool NodesIncomplete(Node node, Stack path) + { + path.Push(node); + if (node.Children.Count == 0) + { + if (!node.State.AvailableActions.IsEmpty) + return true; + } + else + { + for (var i = 0; i < node.Children.Count; ++i) + { + var n = node.ChildAt((i >> 3, i & 7))!; + if (NodesIncomplete(n, path)) + return true; + } + path.Pop(); + } + return false; + } + return !NodesIncomplete(rootNode, new()); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Search(int iterations, CancellationToken token) + { + Simulator simulator = new(rootNode.State.State, config.MaxStepCount); + var random = rootNode.State.State.Input.Random; + var n = 0; + for (var i = 0; i < iterations || MaxScore == 0; i++) + { + if (token.IsCancellationRequested) + break; + + var selectedNode = Select(); + var (endNode, score) = ExpandAndRollout(random, simulator, selectedNode); + if (MaxScore == 0) + { + if (endNode == selectedNode) + { + if (n++ > 5000) + { + n = 0; + if (AllNodesComplete()) + { + //Console.WriteLine("All nodes solved for. Can't find a valid solution."); + //ShowAllNodes(); + return; + } + } + } + else + n = 0; + } + + Backpropagate(endNode, score); + } + } + + [Pure] + public SolverSolution Solution() + { + var actions = new List(); + var node = rootNode; + + while (node.Children.Count != 0) + { + node = node.ChildAt(ChildMaxScore(ref node.ChildScores))!; + + if (node.State.Action != null) + actions.Add(node.State.Action.Value); + } + + //var at = node.ChildIdx; + //ref var sum = ref node.ParentScores!.Value.Data[at.arrayIdx].ScoreSum.Span[at.subIdx]; + //ref var max = ref node.ParentScores!.Value.Data[at.arrayIdx].MaxScore.Span[at.subIdx]; + //ref var visits = ref node.ParentScores!.Value.Data[at.arrayIdx].Visits.Span[at.subIdx]; + //Console.WriteLine($"{sum} {max} {visits}"); + + return new(actions, node.State.State); + } +} diff --git a/Solver/MCTSConfig.cs b/Solver/MCTSConfig.cs new file mode 100644 index 0000000..71ae268 --- /dev/null +++ b/Solver/MCTSConfig.cs @@ -0,0 +1,38 @@ +using System.Runtime.InteropServices; + +namespace Craftimizer.Solver; + +[StructLayout(LayoutKind.Auto)] +public readonly record struct MCTSConfig +{ + public int MaxStepCount { get; init; } + public int MaxRolloutStepCount { get; init; } + public bool StrictActions { get; init; } + + public float MaxScoreWeightingConstant { get; init; } + public float ExplorationConstant { get; init; } + public float ScoreStorageThreshold { get; init; } + + public float ScoreProgress { get; init; } + public float ScoreQuality { get; init; } + public float ScoreDurability { get; init; } + public float ScoreCP { get; init; } + public float ScoreSteps { get; init; } + + public MCTSConfig(SolverConfig config) + { + MaxStepCount= config.MaxStepCount; + MaxRolloutStepCount = config.MaxRolloutStepCount; + StrictActions = config.StrictActions; + + MaxScoreWeightingConstant = config.MaxScoreWeightingConstant; + ExplorationConstant = config.ExplorationConstant; + ScoreStorageThreshold = config.ScoreStorageThreshold; + + ScoreProgress = config.ScoreProgressBonus; + ScoreQuality = config.ScoreQualityBonus; + ScoreDurability = config.ScoreDurabilityBonus; + ScoreCP = config.ScoreCPBonus; + ScoreSteps = config.ScoreFewerStepsBonus; + } +} diff --git a/Solver/SimulationNode.cs b/Solver/SimulationNode.cs index af37ac5..e17c35f 100644 --- a/Solver/SimulationNode.cs +++ b/Solver/SimulationNode.cs @@ -30,7 +30,7 @@ public static CompletionState GetCompletionState(CompletionState simCompletionSt CompletionState.NoMoreActions : simCompletionState; - public readonly float? CalculateScore(SolverConfig config) => + public readonly float? CalculateScore(MCTSConfig config) => CalculateScoreForState(State, SimulationCompletionState, config); private static bool CanByregot(SimulationState state) @@ -41,7 +41,7 @@ private static bool CanByregot(SimulationState state) return BaseComboAction.VerifyDurability2(state, 10); } - public static float? CalculateScoreForState(SimulationState state, CompletionState completionState, SolverConfig config) + public static float? CalculateScoreForState(SimulationState state, CompletionState completionState, MCTSConfig config) { if (completionState != CompletionState.ProgressComplete) return null; @@ -50,32 +50,32 @@ static float Apply(float bonus, float value, float target) => bonus * Math.Min(1f, value / target); var progressScore = Apply( - config.ScoreProgressBonus, + config.ScoreProgress, state.Progress, state.Input.Recipe.MaxProgress ); var byregotBonus = CanByregot(state) ? (state.ActiveEffects.InnerQuiet * .2f + 1) * state.Input.BaseQualityGain : 0; var qualityScore = Apply( - config.ScoreQualityBonus, + config.ScoreQuality, state.Quality + byregotBonus, state.Input.Recipe.MaxQuality ); var durabilityScore = Apply( - config.ScoreDurabilityBonus, + config.ScoreDurability, state.Durability, state.Input.Recipe.MaxDurability ); var cpScore = Apply( - config.ScoreCPBonus, + config.ScoreCP, state.CP, state.Input.Stats.CP ); var fewerStepsScore = - config.ScoreFewerStepsBonus * (1f - (float)(state.ActionCount + 1) / config.MaxStepCount); + config.ScoreSteps * (1f - (float)(state.ActionCount + 1) / config.MaxStepCount); return progressScore + qualityScore + durabilityScore + cpScore + fewerStepsScore; } diff --git a/Solver/Simulator.cs b/Solver/Simulator.cs index 9b610e5..b3f6f60 100644 --- a/Solver/Simulator.cs +++ b/Solver/Simulator.cs @@ -9,8 +9,16 @@ public sealed class Simulator : SimulatorNoRandom { private readonly int maxStepCount; - public new CompletionState CompletionState => CalculateCompletionState(State, maxStepCount); - public override bool IsComplete => CompletionState != CompletionState.Incomplete; + public override CompletionState CompletionState + { + get + { + var b = base.CompletionState; + if (b == CompletionState.Incomplete && (ActionCount + 1) >= maxStepCount) + return CompletionState.MaxActionCountReached; + return b; + } + } public Simulator(SimulationState state, int maxStepCount) : base(state) { @@ -187,8 +195,4 @@ public ActionSet AvailableActionsHeuristic(bool strict) return ret; } - public static CompletionState CalculateCompletionState(SimulationState state, int maxStepCount) => - state.ActionCount + 1 >= maxStepCount ? - CompletionState.MaxActionCountReached : - (CompletionState)CalculateCompletionState(state); } diff --git a/Solver/Solver.cs b/Solver/Solver.cs index 9f19d67..d4b324f 100644 --- a/Solver/Solver.cs +++ b/Solver/Solver.cs @@ -1,338 +1,12 @@ using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; using System.Diagnostics; -using System.Diagnostics.Contracts; -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Text; -using Node = Craftimizer.Solver.ArenaNode; namespace Craftimizer.Solver; -// https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/simulator.rs -public sealed class Solver +public static class Solver { - private SolverConfig config; - private Node rootNode; - private RootScores rootScores; - - public float MaxScore => rootScores.MaxScore; - - public Solver(SolverConfig config, SimulationState state) - { - this.config = config; - var sim = new Simulator(state, config.MaxStepCount); - rootNode = new(new( - state, - null, - sim.CompletionState, - sim.AvailableActionsHeuristic(config.StrictActions) - )); - rootScores = new(); - } - - private static SimulationNode Execute(Simulator simulator, SimulationState state, ActionType action, bool strict) - { - (_, var newState) = simulator.Execute(state, action); - return new( - newState, - action, - simulator.CompletionState, - simulator.AvailableActionsHeuristic(strict) - ); - } - - private static Node ExecuteActions(Simulator simulator, Node startNode, ReadOnlySpan actions, bool strict) - { - foreach (var action in actions) - { - var state = startNode.State; - if (state.IsComplete) - return startNode; - - if (!state.AvailableActions.HasAction(action)) - return startNode; - state.AvailableActions.RemoveAction(action); - - startNode = startNode.Add(Execute(simulator, state.State, action, strict)); - } - - return startNode; - } - - [Pure] - private SolverSolution Solution() - { - var actions = new List(); - var node = rootNode; - - while (node.Children.Count != 0) - { - node = node.ChildAt(ChildMaxScore(ref node.ChildScores))!; - - if (node.State.Action != null) - actions.Add(node.State.Action.Value); - } - - //var at = node.ChildIdx; - //ref var sum = ref node.ParentScores!.Value.Data[at.arrayIdx].ScoreSum.Span[at.subIdx]; - //ref var max = ref node.ParentScores!.Value.Data[at.arrayIdx].MaxScore.Span[at.subIdx]; - //ref var visits = ref node.ParentScores!.Value.Data[at.arrayIdx].Visits.Span[at.subIdx]; - //Console.WriteLine($"{sum} {max} {visits}"); - - return new(actions, node.State.State); - } - - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static (int arrayIdx, int subIdx) ChildMaxScore(ref NodeScoresBuffer scores) - { - var length = scores.Count; - var vecLength = Vector.Count; - - var max = (0, 0); - var maxScore = 0f; - for (var i = 0; length > 0; ++i) - { - var iterCount = Math.Min(vecLength, length); - - ref var chunk = ref scores.Data[i]; - var m = new Vector(chunk.MaxScore.Span); - - var idx = Intrinsics.HMaxIndex(m, iterCount); - - if (m[idx] >= maxScore) - { - max = (i, idx); - maxScore = m[idx]; - } - - length -= iterCount; - } - - return max; - } - - // Calculates the best child node to explore next - // Exploitation: ((1 - w) * (s / v)) + (w * m) - // Exploration: sqrt(c * ln(V) / v) - // w = maxScoreWeightingConstant - // s = score sum - // m = max score - // v = visits - // V = parentVisits - // c = explorationConstant - - // Somewhat based off of https://en.wikipedia.org/wiki/Monte_Carlo_tree_search#Exploration_and_exploitation - // Here, w_i = (1-w)*score sum - // n_i = visits - // max score is tacked onto it - // N_i = parent visits - // c = exploration constant (but crafty places it inside the sqrt..?) - [Pure] - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private (int arrayIdx, int subIdx) EvalBestChild(int parentVisits, ref NodeScoresBuffer scores) - { - var length = scores.Count; - var vecLength = Vector.Count; - - var C = MathF.Sqrt(config.ExplorationConstant * MathF.Log(parentVisits)); - var w = config.MaxScoreWeightingConstant; - var W = 1f - w; - var CVector = new Vector(C); - - var max = (0, 0); - var maxScore = 0f; - for (var i = 0; length > 0; ++i) - { - var iterCount = Math.Min(vecLength, length); - - ref var chunk = ref scores.Data[i]; - var s = new Vector(chunk.ScoreSum.Span); - var vInt = new Vector(chunk.Visits.Span); - var m = new Vector(chunk.MaxScore.Span); - - vInt = Vector.Max(vInt, Vector.One); - var v = Vector.ConvertToSingle(vInt); - - var exploitation = W * (s / v) + w * m; - var exploration = CVector * Intrinsics.ReciprocalSqrt(v); - var evalScores = exploitation + exploration; - - var idx = Intrinsics.HMaxIndex(evalScores, iterCount); - - if (evalScores[idx] >= maxScore) - { - max = (i, idx); - maxScore = evalScores[idx]; - } - - length -= iterCount; - } - - return max; - } - - [Pure] - public Node Select() - { - var node = rootNode; - var nodeVisits = rootScores.Visits; - - while (true) - { - var expandable = !node.State.AvailableActions.IsEmpty; - var likelyTerminal = node.Children.Count == 0; - if (expandable || likelyTerminal) - return node; - - // select the node with the highest score - var at = EvalBestChild(nodeVisits, ref node.ChildScores); - nodeVisits = node.ChildScores.GetVisits(at); - node = node.ChildAt(at)!; - } - } - - public (Node ExpandedNode, float Score) ExpandAndRollout(Random random, Simulator simulator, Node initialNode) - { - ref var initialState = ref initialNode.State; - // expand once - if (initialState.IsComplete) - return (initialNode, initialState.CalculateScore(config) ?? 0); - - var poppedAction = initialState.AvailableActions.PopRandom(random); - var expandedNode = initialNode.Add(Execute(simulator, initialState.State, poppedAction, true)); - - // playout to a terminal state - var currentState = expandedNode.State.State; - var currentCompletionState = expandedNode.State.SimulationCompletionState; - var currentActions = expandedNode.State.AvailableActions; - - - byte actionCount = 0; - Span actions = stackalloc ActionType[Math.Min(config.MaxStepCount - currentState.ActionCount, config.MaxRolloutStepCount)]; - while (SimulationNode.GetCompletionState(currentCompletionState, currentActions) == CompletionState.Incomplete && - actionCount < actions.Length) - { - var nextAction = currentActions.SelectRandom(random); - actions[actionCount++] = nextAction; - (_, currentState) = simulator.Execute(currentState, nextAction); - currentCompletionState = simulator.CompletionState; - if (currentCompletionState != CompletionState.Incomplete) - break; - currentActions = simulator.AvailableActionsHeuristic(true); - } - - // store the result if a max score was reached - var score = SimulationNode.CalculateScoreForState(currentState, currentCompletionState, config) ?? 0; - if (currentCompletionState == CompletionState.ProgressComplete) - { - if (score >= config.ScoreStorageThreshold && score >= MaxScore) - { - var terminalNode = ExecuteActions(simulator, expandedNode, actions[..actionCount], true); - return (terminalNode, score); - } - } - return (expandedNode, score); - } - - public void Backpropagate(Node startNode, float score) - { - while (true) - { - if (startNode == rootNode) - { - rootScores.Visit(score); - break; - } - startNode.ParentScores!.Value.Visit(startNode.ChildIdx, score); - - startNode = startNode.Parent!; - } - } - - private void ShowAllNodes() - { - static void ShowNodes(StringBuilder b, Node node, Stack path) - { - path.Push(node); - b.AppendLine($"{new string(' ', path.Count)}{node.State.Action}"); - { - for (var i = 0; i < node.Children.Count; ++i) - { - var n = node.ChildAt((i >> 3, i & 7))!; - ShowNodes(b, n, path); - } - path.Pop(); - } - } - var b = new StringBuilder(); - ShowNodes(b, rootNode, new()); - Console.WriteLine(b.ToString()); - } - - private bool AllNodesComplete() - { - static bool NodesIncomplete(Node node, Stack path) - { - path.Push(node); - if (node.Children.Count == 0) - { - if (!node.State.AvailableActions.IsEmpty) - return true; - } - else - { - for (var i = 0; i < node.Children.Count; ++i) - { - var n = node.ChildAt((i >> 3, i & 7))!; - if (NodesIncomplete(n, path)) - return true; - } - path.Pop(); - } - return false; - } - return !NodesIncomplete(rootNode, new()); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void Search(int iterations, CancellationToken token) - { - Simulator simulator = new(rootNode.State.State, config.MaxStepCount); - var random = rootNode.State.State.Input.Random; - var n = 0; - for (var i = 0; i < iterations || MaxScore == 0; i++) - { - if (token.IsCancellationRequested) - break; - - var selectedNode = Select(); - var (endNode, score) = ExpandAndRollout(random, simulator, selectedNode); - if (MaxScore == 0) - { - if (endNode == selectedNode) - { - if (n++ > 5000) - { - n = 0; - if (AllNodesComplete()) - { - //Console.WriteLine("All nodes solved for. Can't find a valid solution."); - //ShowAllNodes(); - return; - } - } - } - else - n = 0; - } - - Backpropagate(endNode, score); - } - } - - public static SolverSolution SearchStepwiseFurcated(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) + private static SolverSolution SearchStepwiseFurcated(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) { var definiteActionCount = 0; var bestSims = new List<(float Score, SolverSolution Result)>(); @@ -354,7 +28,7 @@ public static SolverSolution SearchStepwiseFurcated(SolverConfig config, Simulat var st = activeStates[stateIdx]; tasks[i] = Task.Run(() => { - var solver = new Solver(config, activeStates[stateIdx].State); + var solver = new MCTS(new(config), activeStates[stateIdx].State); solver.Search(config.Iterations / config.ForkCount, token); return (solver.MaxScore, stateIdx, solver.Solution()); }, token); @@ -442,7 +116,7 @@ public static SolverSolution SearchStepwiseFurcated(SolverConfig config, Simulat return result; } - public static SolverSolution SearchStepwiseForked(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) + private static SolverSolution SearchStepwiseForked(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) { var actions = new List(); var sim = new Simulator(state, config.MaxStepCount); @@ -460,7 +134,7 @@ public static SolverSolution SearchStepwiseForked(SolverConfig config, Simulatio for (var i = 0; i < config.ForkCount; ++i) tasks[i] = Task.Run(() => { - var solver = new Solver(config, state); + var solver = new MCTS(new(config), state); solver.Search(config.Iterations / config.ForkCount, token); return (solver.MaxScore, solver.Solution()); }, token); @@ -489,7 +163,7 @@ public static SolverSolution SearchStepwiseForked(SolverConfig config, Simulatio return new(actions, state); } - public static SolverSolution SearchStepwise(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) + private static SolverSolution SearchStepwise(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) { var actions = new List(); var sim = new Simulator(state, config.MaxStepCount); @@ -501,7 +175,7 @@ public static SolverSolution SearchStepwise(SolverConfig config, SimulationState if (sim.IsComplete) break; - var solver = new Solver(config, state); + var solver = new MCTS(new(config), state); var s = Stopwatch.StartNew(); solver.Search(config.Iterations, token); @@ -526,13 +200,13 @@ public static SolverSolution SearchStepwise(SolverConfig config, SimulationState return new(actions, state); } - public static SolverSolution SearchOneshotForked(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) + private static SolverSolution SearchOneshotForked(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) { var tasks = new Task<(float MaxScore, SolverSolution Solution)>[config.ForkCount]; for (var i = 0; i < config.ForkCount; ++i) tasks[i] = Task.Run(() => { - var solver = new Solver(config, state); + var solver = new MCTS(new(config), state); solver.Search(config.Iterations / config.ForkCount, token); return (solver.MaxScore, solver.Solution()); }, token); @@ -545,9 +219,9 @@ public static SolverSolution SearchOneshotForked(SolverConfig config, Simulation return solution; } - public static SolverSolution SearchOneshot(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) + private static SolverSolution SearchOneshot(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) { - var solver = new Solver(config, state); + var solver = new MCTS(new(config), state); solver.Search(config.Iterations, token); var solution = solver.Solution(); foreach (var action in solution.Actions) @@ -556,7 +230,7 @@ public static SolverSolution SearchOneshot(SolverConfig config, SimulationState return solution; } - public static SolverSolution Search(SolverConfig config, SimulationState state, Action? actionCallback = null, CancellationToken token = default) + public static SolverSolution Search(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) { Func?, CancellationToken, SolverSolution> func = config.Algorithm switch { @@ -564,7 +238,8 @@ public static SolverSolution Search(SolverConfig config, SimulationState state, SolverAlgorithm.OneshotForked => SearchOneshotForked, SolverAlgorithm.Stepwise => SearchStepwise, SolverAlgorithm.StepwiseForked => SearchStepwiseForked, - SolverAlgorithm.StepwiseFurcated or _ => SearchStepwiseFurcated, + SolverAlgorithm.StepwiseFurcated => SearchStepwiseFurcated, + _ => throw new ArgumentOutOfRangeException(nameof(config), config, $"Invalid algorithm: {config.Algorithm}") }; return func(config, state, actionCallback, token); } diff --git a/Solver/SolverConfig.cs b/Solver/SolverConfig.cs index 15be1db..fffcff3 100644 --- a/Solver/SolverConfig.cs +++ b/Solver/SolverConfig.cs @@ -1,3 +1,5 @@ +using Craftimizer.Simulator.Actions; +using Craftimizer.Simulator; using System.Runtime.InteropServices; namespace Craftimizer.Solver; @@ -65,4 +67,21 @@ public SolverConfig() FurcatedActionCount = Environment.ProcessorCount / 2, Algorithm = SolverAlgorithm.StepwiseForked }; -} + + public SolverSolution? Invoke(SimulationState state, Action? actionCallback = null, CancellationToken token = default) + { + try + { + return Solver.Search(this, state, actionCallback, token); + } + catch (AggregateException e) + { + e.Handle(ex => ex is OperationCanceledException); + } + catch (OperationCanceledException) + { + + } + return null; + } +} \ No newline at end of file From 42a1bc111725bda67ae8f5dea370c8b347641fd3 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Mon, 2 Oct 2023 22:28:12 -0700 Subject: [PATCH 08/58] Use TPL when solving with all algorithms --- .gitattributes | 3 + Benchmark/Program.cs | 12 +- Craftimizer/Windows/Craft.cs | 3 +- Craftimizer/Windows/CraftSolver.cs | 8 +- Craftimizer/Windows/Settings.cs | 40 ++-- Craftimizer/Windows/SimulatorSolver.cs | 11 +- Solver/MCTSConfig.cs | 12 +- Solver/Solver.cs | 259 +++++++++++++++++-------- Solver/SolverConfig.cs | 37 +--- 9 files changed, 241 insertions(+), 144 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fc549fa --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Auto detect text files and perform LF normalization +* text eol=crlf +*.png binary \ No newline at end of file diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index f5acf7b..d3717ca 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -7,7 +7,7 @@ namespace Craftimizer.Benchmark; internal static class Program { - private static void Main() + private static async Task Main() { //var summary = BenchmarkRunner.Run(); //return; @@ -77,12 +77,12 @@ private static void Main() Console.WriteLine($"{state.Quality} {state.CP} {state.Progress} {state.Durability}"); //return; - var (_, s) = config.Invoke(state, a => Console.WriteLine(a))!.Value; + var solver = new Solver.Solver(config, state); + solver.OnLog += s => Console.WriteLine(s); + solver.OnNewAction += s => Console.WriteLine(s); + solver.Start(); + var (_, s) = await solver.GetTask().ConfigureAwait(false); Console.WriteLine($"Qual: {s.Quality}/{s.Input.Recipe.MaxQuality}"); - return; - - config.Invoke(new(input)); - //Benchmark(() => ); } private static void Benchmark(Func search) diff --git a/Craftimizer/Windows/Craft.cs b/Craftimizer/Windows/Craft.cs index 0ff783e..29422e1 100644 --- a/Craftimizer/Windows/Craft.cs +++ b/Craftimizer/Windows/Craft.cs @@ -215,7 +215,8 @@ private void ResetSimulation() public void Dispose() { StopSolve(); - SolverTask?.Wait(); + SolverTaskToken?.Cancel(); + SolverTask?.TryWait(); SolverTask?.Dispose(); SolverTaskToken?.Dispose(); diff --git a/Craftimizer/Windows/CraftSolver.cs b/Craftimizer/Windows/CraftSolver.cs index 6b70fd3..9149981 100644 --- a/Craftimizer/Windows/CraftSolver.cs +++ b/Craftimizer/Windows/CraftSolver.cs @@ -1,6 +1,7 @@ using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; using Dalamud.Interface.Windowing; +using Dalamud.Logging; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -12,7 +13,7 @@ namespace Craftimizer.Plugin.Windows; public sealed unsafe partial class Craft : Window, IDisposable { private SimulationState? SolverState { get; set; } - private Task? SolverTask { get; set; } + private Solver.Solver? SolverTask { get; set; } private CancellationTokenSource? SolverTaskToken { get; set; } private ConcurrentQueue SolverActionQueue { get; } = new(); @@ -48,7 +49,10 @@ private void QueueSolve(SimulationState state) SolverSim = new(state); SolverTaskToken = new(); - SolverTask = Task.Run(() => Config.SynthHelperSolverConfig.Invoke(state, SolverActionQueue.Enqueue, SolverTaskToken.Token)); + SolverTask = new(Config.SynthHelperSolverConfig, state) { Token = SolverTaskToken.Token }; + SolverTask.OnLog += s => PluginLog.Debug(s); + SolverTask.OnNewAction += SolverActionQueue.Enqueue; + SolverTask.Start(); } private void SolveTick() diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index 5fa226e..23bdd9c 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -312,32 +312,32 @@ ref isDirty DrawOption( "Progress", "Amount of weight to give to the craft's progress.", - config.ScoreProgressBonus, - v => config = config with { ScoreProgressBonus = v }, + config.ScoreProgress, + v => config = config with { ScoreProgress = v }, ref isDirty ); DrawOption( "Quality", "Amount of weight to give to the craft's quality.", - config.ScoreQualityBonus, - v => config = config with { ScoreQualityBonus = v }, + config.ScoreQuality, + v => config = config with { ScoreQuality = v }, ref isDirty ); DrawOption( "Durability", "Amount of weight to give to the craft's remaining durability.", - config.ScoreDurabilityBonus, - v => config = config with { ScoreDurabilityBonus = v }, + config.ScoreDurability, + v => config = config with { ScoreDurability = v }, ref isDirty ); DrawOption( "CP", "Amount of weight to give to the craft's remaining CP.", - config.ScoreCPBonus, - v => config = config with { ScoreCPBonus = v }, + config.ScoreCP, + v => config = config with { ScoreCP = v }, ref isDirty ); @@ -345,25 +345,25 @@ ref isDirty "Steps", "Amount of weight to give to the craft's number of steps. The lower\n" + "the step count, the higher the score.", - config.ScoreFewerStepsBonus, - v => config = config with { ScoreFewerStepsBonus = v }, + config.ScoreSteps, + v => config = config with { ScoreSteps = v }, ref isDirty ); if (ImGui.Button("Normalize Weights", OptionButtonSize)) { - var total = config.ScoreProgressBonus + - config.ScoreQualityBonus + - config.ScoreDurabilityBonus + - config.ScoreCPBonus + - config.ScoreFewerStepsBonus; + var total = config.ScoreProgress + + config.ScoreQuality + + config.ScoreDurability + + config.ScoreCP + + config.ScoreSteps; config = config with { - ScoreProgressBonus = config.ScoreProgressBonus / total, - ScoreQualityBonus = config.ScoreQualityBonus / total, - ScoreDurabilityBonus = config.ScoreDurabilityBonus / total, - ScoreCPBonus = config.ScoreCPBonus / total, - ScoreFewerStepsBonus = config.ScoreFewerStepsBonus / total + ScoreProgress = config.ScoreProgress / total, + ScoreQuality = config.ScoreQuality / total, + ScoreDurability = config.ScoreDurability / total, + ScoreCP = config.ScoreCP / total, + ScoreSteps = config.ScoreSteps / total }; isDirty = true; } diff --git a/Craftimizer/Windows/SimulatorSolver.cs b/Craftimizer/Windows/SimulatorSolver.cs index d4df22c..de3eaf6 100644 --- a/Craftimizer/Windows/SimulatorSolver.cs +++ b/Craftimizer/Windows/SimulatorSolver.cs @@ -1,6 +1,7 @@ using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; using Dalamud.Interface.Windowing; +using Dalamud.Logging; using System; using System.Collections.Concurrent; using System.Threading; @@ -10,7 +11,7 @@ namespace Craftimizer.Plugin.Windows; public sealed partial class Simulator : Window, IDisposable { - private Task? SolverTask { get; set; } + private Solver.Solver? SolverTask { get; set; } private CancellationTokenSource? SolverTaskToken { get; set; } private ConcurrentQueue SolverActionQueue { get; } = new(); private int SolverInitialActionCount { get; set; } @@ -83,13 +84,17 @@ private void SolveMacro(SimulationState solverState) SolverInitialActionCount = Actions.Count; SolverTaskToken = new(); - SolverTask = Task.Run(() => Config.SimulatorSolverConfig.Invoke(solverState, SolverActionQueue.Enqueue, SolverTaskToken.Token)); + SolverTask = new(Config.SimulatorSolverConfig, solverState) { Token = SolverTaskToken.Token }; + SolverTask.OnLog += s => PluginLog.Debug(s); + SolverTask.OnNewAction += SolverActionQueue.Enqueue; + SolverTask.Start(); } public void Dispose() { StopSolveMacro(); - SolverTask?.Wait(); + SolverTaskToken?.Cancel(); + SolverTask?.TryWait(); SolverTask?.Dispose(); SolverTaskToken?.Dispose(); } diff --git a/Solver/MCTSConfig.cs b/Solver/MCTSConfig.cs index 71ae268..c0d057d 100644 --- a/Solver/MCTSConfig.cs +++ b/Solver/MCTSConfig.cs @@ -21,7 +21,7 @@ public readonly record struct MCTSConfig public MCTSConfig(SolverConfig config) { - MaxStepCount= config.MaxStepCount; + MaxStepCount = config.MaxStepCount; MaxRolloutStepCount = config.MaxRolloutStepCount; StrictActions = config.StrictActions; @@ -29,10 +29,10 @@ public MCTSConfig(SolverConfig config) ExplorationConstant = config.ExplorationConstant; ScoreStorageThreshold = config.ScoreStorageThreshold; - ScoreProgress = config.ScoreProgressBonus; - ScoreQuality = config.ScoreQualityBonus; - ScoreDurability = config.ScoreDurabilityBonus; - ScoreCP = config.ScoreCPBonus; - ScoreSteps = config.ScoreFewerStepsBonus; + ScoreProgress = config.ScoreProgress; + ScoreQuality = config.ScoreQuality; + ScoreDurability = config.ScoreDurability; + ScoreCP = config.ScoreCP; + ScoreSteps = config.ScoreSteps; } } diff --git a/Solver/Solver.cs b/Solver/Solver.cs index d4b324f..a34abe9 100644 --- a/Solver/Solver.cs +++ b/Solver/Solver.cs @@ -4,57 +4,165 @@ namespace Craftimizer.Solver; -public static class Solver +public sealed class Solver : IDisposable { - private static SolverSolution SearchStepwiseFurcated(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) + public SolverConfig Config { get; } + public SimulationState State { get; } + public CancellationToken Token { get; init; } + public SolverSolution? Solution { get; private set; } + + public bool IsStarted => CompletionTask != null; + public bool IsCompletedSuccessfully => Solution != null; + public bool IsCompleted => CompletionTask?.IsCompleted ?? false; + + private Func> SearchFunc { get; } + private MCTSConfig MCTSConfig => new(Config); + private Task? CompletionTask { get; set; } + + public delegate void LogDelegate(string text); + public delegate void WorkerProgressDelegate(SolverSolution solution, float score); + public delegate void NewActionDelegate(ActionType action); + public delegate void SolutionDelegate(SolverSolution solution); + + // Print to console or plugin log. + public event LogDelegate? OnLog; + + // Isn't always called. This is just meant to show as an indicator to the user. + // Solution contains the best terminal state, and its actions to get there exclude the ones provided by OnNewAction. + // For example, to get to the terminal state, execute all OnNewAction actions, then execute all Solution actions. + public event WorkerProgressDelegate? OnWorkerProgress; + + // Always called when a new step is generated. + public event NewActionDelegate? OnNewAction; + + // Always called when the solver is fully complete. + public event SolutionDelegate? OnSolution; + + public Solver(SolverConfig config, SimulationState state) + { + Config = config; + State = state; + + SearchFunc = Config.Algorithm switch + { + SolverAlgorithm.Oneshot => SearchOneshot, + SolverAlgorithm.OneshotForked => SearchOneshotForked, + SolverAlgorithm.Stepwise => SearchStepwise, + SolverAlgorithm.StepwiseForked => SearchStepwiseForked, + SolverAlgorithm.StepwiseFurcated => SearchStepwiseFurcated, + _ => throw new ArgumentOutOfRangeException(nameof(config), config, $"Invalid algorithm: {config.Algorithm}") + }; + } + + public void Start() + { + if (IsStarted) + throw new InvalidOperationException("Solver has already started."); + + CompletionTask = RunTask(); + } + + private async Task RunTask() + { + if (Token.IsCancellationRequested) + return; + + Solution = await SearchFunc().ConfigureAwait(false); + } + + public async Task GetTask() + { + if (!IsStarted) + throw new InvalidOperationException("Solver has not started."); + + await CompletionTask!.ConfigureAwait(false); + + return Solution!.Value; + } + + public async Task GetSafeTask() + { + try + { + return await GetTask().ConfigureAwait(false); + } + catch (AggregateException e) + { + e.Handle(ex => ex is OperationCanceledException); + } + catch (OperationCanceledException) + { + + } + return null; + } + + public void TryWait() + { + if (IsStarted && !IsCompleted) + GetSafeTask().Wait(); + } + + public void Dispose() + { + CompletionTask?.Dispose(); + } + + private async Task SearchStepwiseFurcated() { var definiteActionCount = 0; var bestSims = new List<(float Score, SolverSolution Result)>(); - var sim = new Simulator(state, config.MaxStepCount); + var state = State; + var sim = new Simulator(state, Config.MaxStepCount); var activeStates = new List() { new(new(), state) }; while (activeStates.Count != 0) { - if (token.IsCancellationRequested) + if (Token.IsCancellationRequested) break; var s = Stopwatch.StartNew(); - var tasks = new Task<(float MaxScore, int FurcatedActionIdx, SolverSolution Solution)>[config.ForkCount]; - for (var i = 0; i < config.ForkCount; i++) + var tasks = new Task<(float MaxScore, int FurcatedActionIdx, SolverSolution Solution)>[Config.ForkCount]; + for (var i = 0; i < Config.ForkCount; i++) { - var stateIdx = (int)((float)i / config.ForkCount * activeStates.Count); - var st = activeStates[stateIdx]; + var stateIdx = (int)((float)i / Config.ForkCount * activeStates.Count); tasks[i] = Task.Run(() => { - var solver = new MCTS(new(config), activeStates[stateIdx].State); - solver.Search(config.Iterations / config.ForkCount, token); - return (solver.MaxScore, stateIdx, solver.Solution()); - }, token); + var solver = new MCTS(MCTSConfig, activeStates[stateIdx].State); + solver.Search(Config.Iterations / Config.ForkCount, Token); + var solution = solver.Solution(); + var progressActions = activeStates[stateIdx].Actions.Concat(solution.Actions).Skip(definiteActionCount).ToList(); + OnWorkerProgress?.Invoke(solution with { Actions = progressActions }, solver.MaxScore); + return (solver.MaxScore, stateIdx, solution); + }, Token); } - Task.WaitAll(tasks, token); + await Task.WhenAll(tasks).WaitAsync(Token).ConfigureAwait(false); s.Stop(); + OnLog?.Invoke($"{s.Elapsed.TotalMilliseconds:0.00}ms {Config.Iterations / Config.ForkCount / s.Elapsed.TotalSeconds / 1000:0.00} kI/s/t"); - if (token.IsCancellationRequested) + if (Token.IsCancellationRequested) break; - var bestActions = tasks.Select(t => t.Result).OrderByDescending(r => r.MaxScore).Take(config.FurcatedActionCount).ToArray(); + var bestActions = tasks.Select(t => t.Result).OrderByDescending(r => r.MaxScore).Take(Config.FurcatedActionCount).ToArray(); var bestAction = bestActions[0]; - if (bestAction.MaxScore >= config.ScoreStorageThreshold) + if (bestAction.MaxScore >= Config.ScoreStorageThreshold) { - var (maxScore, furcatedActionIdx, solution) = bestAction; - var (activeActions, activeState) = activeStates[furcatedActionIdx]; + var (_, furcatedActionIdx, solution) = bestAction; + var (activeActions, _) = activeStates[furcatedActionIdx]; activeActions.AddRange(solution.Actions); + foreach (var action in activeActions.Skip(definiteActionCount)) + OnNewAction?.Invoke(action); return solution with { Actions = activeActions }; } - var newStates = new List(config.FurcatedActionCount); + var newStates = new List(Config.FurcatedActionCount); for (var i = 0; i < bestActions.Length; ++i) { - var (maxScore, furcatedActionIdx, (solutionActions, solutionNode)) = bestActions[i]; + var (maxScore, furcatedActionIdx, (solutionActions, _)) = bestActions[i]; if (solutionActions.Count == 0) continue; @@ -94,67 +202,69 @@ private static SolverSolution SearchStepwiseFurcated(SolverConfig config, Simula } if (definiteCount != equalCount) { - for (var i = definiteCount; i < equalCount; ++i) - actionCallback?.Invoke(refActions[i]); + foreach(var action in refActions.Take(equalCount).Skip(definiteCount)) + OnNewAction?.Invoke(action); definiteActionCount = equalCount; } } activeStates = newStates; - - Console.WriteLine($"{s.Elapsed.TotalMilliseconds:0.00}ms {config.Iterations / config.ForkCount / s.Elapsed.TotalSeconds / 1000:0.00} kI/s/t"); } if (bestSims.Count == 0) return new(new(), state); var result = bestSims.MaxBy(s => s.Score).Result; - for (var i = definiteActionCount; i < result.Actions.Count; ++i) - actionCallback?.Invoke(result.Actions[i]); + foreach (var action in result.Actions.Skip(definiteActionCount)) + OnNewAction?.Invoke(action); return result; } - private static SolverSolution SearchStepwiseForked(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) + private async Task SearchStepwiseForked() { var actions = new List(); - var sim = new Simulator(state, config.MaxStepCount); + var state = State; + var sim = new Simulator(state, Config.MaxStepCount); while (true) { - if (token.IsCancellationRequested) + if (Token.IsCancellationRequested) break; if (sim.IsComplete) break; - var s = Stopwatch.StartNew(); - var tasks = new Task<(float MaxScore, SolverSolution Solution)>[config.ForkCount]; - for (var i = 0; i < config.ForkCount; ++i) + var tasks = new Task<(float MaxScore, SolverSolution Solution)>[Config.ForkCount]; + for (var i = 0; i < Config.ForkCount; ++i) tasks[i] = Task.Run(() => { - var solver = new MCTS(new(config), state); - solver.Search(config.Iterations / config.ForkCount, token); - return (solver.MaxScore, solver.Solution()); - }, token); - Task.WaitAll(tasks, token); + var solver = new MCTS(MCTSConfig, state); + solver.Search(Config.Iterations / Config.ForkCount, Token); + var solution = solver.Solution(); + OnWorkerProgress?.Invoke(solution, solver.MaxScore); + return (solver.MaxScore, solution); + }, Token); + await Task.WhenAll(tasks).WaitAsync(Token).ConfigureAwait(false); s.Stop(); + OnLog?.Invoke($"{s.Elapsed.TotalMilliseconds:0.00}ms {Config.Iterations / Config.ForkCount / s.Elapsed.TotalSeconds / 1000:0.00} kI/s/t"); - if (token.IsCancellationRequested) + if (Token.IsCancellationRequested) break; var (maxScore, solution) = tasks.Select(t => t.Result).MaxBy(r => r.MaxScore); - if (maxScore >= config.ScoreStorageThreshold) + if (maxScore >= Config.ScoreStorageThreshold) { actions.AddRange(solution.Actions); + foreach (var action in solution.Actions) + OnNewAction?.Invoke(action); return solution with { Actions = actions }; } var chosenAction = solution.Actions[0]; - actionCallback?.Invoke(chosenAction); - Console.WriteLine($"{s.Elapsed.TotalMilliseconds:0.00}ms {config.Iterations / config.ForkCount / s.Elapsed.TotalSeconds / 1000:0.00} kI/s/t"); + OnNewAction?.Invoke(chosenAction); (_, state) = sim.Execute(state, chosenAction); actions.Add(chosenAction); @@ -163,84 +273,75 @@ private static SolverSolution SearchStepwiseForked(SolverConfig config, Simulati return new(actions, state); } - private static SolverSolution SearchStepwise(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) + private Task SearchStepwise() { var actions = new List(); - var sim = new Simulator(state, config.MaxStepCount); + var state = State; + var sim = new Simulator(state, Config.MaxStepCount); while (true) { - if (token.IsCancellationRequested) + if (Token.IsCancellationRequested) break; if (sim.IsComplete) break; - var solver = new MCTS(new(config), state); + var solver = new MCTS(MCTSConfig, State); var s = Stopwatch.StartNew(); - solver.Search(config.Iterations, token); + solver.Search(Config.Iterations, Token); s.Stop(); + OnLog?.Invoke($"{s.Elapsed.TotalMilliseconds:0.00}ms {Config.Iterations / s.Elapsed.TotalSeconds / 1000:0.00} kI/s"); var solution = solver.Solution(); - if (solver.MaxScore >= config.ScoreStorageThreshold) + if (solver.MaxScore >= Config.ScoreStorageThreshold) { actions.AddRange(solution.Actions); - return solution with { Actions = actions }; + foreach (var action in solution.Actions) + OnNewAction?.Invoke(action); + return Task.FromResult(solution with { Actions = actions }); } var chosenAction = solution.Actions[0]; - actionCallback?.Invoke(chosenAction); - Console.WriteLine($"{s.Elapsed.TotalMilliseconds:0.00}ms {config.Iterations / s.Elapsed.TotalSeconds / 1000:0.00} kI/s"); + OnNewAction?.Invoke(chosenAction); (_, state) = sim.Execute(state, chosenAction); actions.Add(chosenAction); } - return new(actions, state); + return Task.FromResult(new SolverSolution(actions, state)); } - private static SolverSolution SearchOneshotForked(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) + private async Task SearchOneshotForked() { - var tasks = new Task<(float MaxScore, SolverSolution Solution)>[config.ForkCount]; - for (var i = 0; i < config.ForkCount; ++i) + var tasks = new Task<(float MaxScore, SolverSolution Solution)>[Config.ForkCount]; + for (var i = 0; i < Config.ForkCount; ++i) tasks[i] = Task.Run(() => { - var solver = new MCTS(new(config), state); - solver.Search(config.Iterations / config.ForkCount, token); - return (solver.MaxScore, solver.Solution()); - }, token); - Task.WaitAll(tasks, CancellationToken.None); + var solver = new MCTS(MCTSConfig, State); + solver.Search(Config.Iterations / Config.ForkCount, Token); + var solution = solver.Solution(); + OnWorkerProgress?.Invoke(solution, solver.MaxScore); + return (solver.MaxScore, solution); + }, Token); + await Task.WhenAll(tasks).WaitAsync(Token).ConfigureAwait(false); var solution = tasks.Select(t => t.Result).MaxBy(r => r.MaxScore).Solution; foreach (var action in solution.Actions) - actionCallback?.Invoke(action); + OnNewAction?.Invoke(action); return solution; } - private static SolverSolution SearchOneshot(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) + private Task SearchOneshot() { - var solver = new MCTS(new(config), state); - solver.Search(config.Iterations, token); + var solver = new MCTS(MCTSConfig, State); + solver.Search(Config.Iterations, Token); var solution = solver.Solution(); foreach (var action in solution.Actions) - actionCallback?.Invoke(action); + OnNewAction?.Invoke(action); - return solution; - } - - public static SolverSolution Search(SolverConfig config, SimulationState state, Action? actionCallback, CancellationToken token) - { - Func?, CancellationToken, SolverSolution> func = config.Algorithm switch - { - SolverAlgorithm.Oneshot => SearchOneshot, - SolverAlgorithm.OneshotForked => SearchOneshotForked, - SolverAlgorithm.Stepwise => SearchStepwise, - SolverAlgorithm.StepwiseForked => SearchStepwiseForked, - SolverAlgorithm.StepwiseFurcated => SearchStepwiseFurcated, - _ => throw new ArgumentOutOfRangeException(nameof(config), config, $"Invalid algorithm: {config.Algorithm}") - }; - return func(config, state, actionCallback, token); + return Task.FromResult(solution); } } diff --git a/Solver/SolverConfig.cs b/Solver/SolverConfig.cs index fffcff3..cbb7c5a 100644 --- a/Solver/SolverConfig.cs +++ b/Solver/SolverConfig.cs @@ -26,11 +26,11 @@ public readonly record struct SolverConfig public int FurcatedActionCount { get; init; } public bool StrictActions { get; init; } - public float ScoreProgressBonus { get; init; } - public float ScoreQualityBonus { get; init; } - public float ScoreDurabilityBonus { get; init; } - public float ScoreCPBonus { get; init; } - public float ScoreFewerStepsBonus { get; init; } + public float ScoreProgress { get; init; } + public float ScoreQuality { get; init; } + public float ScoreDurability { get; init; } + public float ScoreCP { get; init; } + public float ScoreSteps { get; init; } public SolverAlgorithm Algorithm { get; init; } @@ -46,11 +46,11 @@ public SolverConfig() FurcatedActionCount = ForkCount / 2; StrictActions = true; - ScoreProgressBonus = .20f; - ScoreQualityBonus = .65f; - ScoreDurabilityBonus = .05f; - ScoreCPBonus = .05f; - ScoreFewerStepsBonus = .05f; + ScoreProgress = .20f; + ScoreQuality = .65f; + ScoreDurability = .05f; + ScoreCP = .05f; + ScoreSteps = .05f; Algorithm = SolverAlgorithm.StepwiseFurcated; } @@ -67,21 +67,4 @@ public SolverConfig() FurcatedActionCount = Environment.ProcessorCount / 2, Algorithm = SolverAlgorithm.StepwiseForked }; - - public SolverSolution? Invoke(SimulationState state, Action? actionCallback = null, CancellationToken token = default) - { - try - { - return Solver.Search(this, state, actionCallback, token); - } - catch (AggregateException e) - { - e.Handle(ex => ex is OperationCanceledException); - } - catch (OperationCanceledException) - { - - } - return null; - } } \ No newline at end of file From 6bfff06ac4c56b339acb2ca1808c8e25ec5b1ce2 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Mon, 2 Oct 2023 23:02:11 -0700 Subject: [PATCH 09/58] Implement max thread count setting using semaphores --- Craftimizer/Windows/Settings.cs | 16 +++++++++ Solver/MCTS.cs | 3 +- Solver/MCTSConfig.cs | 2 ++ Solver/Solver.cs | 60 +++++++++++++++++++++++---------- Solver/SolverConfig.cs | 1 + 5 files changed, 62 insertions(+), 20 deletions(-) diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index 23bdd9c..87d7e7a 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -240,6 +240,22 @@ ref isDirty ref isDirty ); + ImGui.BeginDisabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated)); + DrawOption( + "Max Core Count", + "The number of cores to use when solving. You should use as many\n" + + "as you can. If it's too high, it will have an effect on your gameplay\n" + + $"experience. A good estimate would be 1 or 2 cores less than your\n" + + $"system (FYI, you have {Environment.ProcessorCount} cores,) but\n" + + $"make sure to accomodate for any other tasks you have in the\n" + + $"background, if you have any.\n" + + "(Only used in the Forked and Furcated algorithms)", + config.MaxThreadCount, + v => config = config with { MaxThreadCount = v }, + ref isDirty + ); + ImGui.EndDisabled(); + ImGui.BeginDisabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated)); DrawOption( "Fork Count", diff --git a/Solver/MCTS.cs b/Solver/MCTS.cs index 4f976fb..8e40a6b 100644 --- a/Solver/MCTS.cs +++ b/Solver/MCTS.cs @@ -285,8 +285,7 @@ public void Search(int iterations, CancellationToken token) var n = 0; for (var i = 0; i < iterations || MaxScore == 0; i++) { - if (token.IsCancellationRequested) - break; + token.ThrowIfCancellationRequested(); var selectedNode = Select(); var (endNode, score) = ExpandAndRollout(random, simulator, selectedNode); diff --git a/Solver/MCTSConfig.cs b/Solver/MCTSConfig.cs index c0d057d..a0e554d 100644 --- a/Solver/MCTSConfig.cs +++ b/Solver/MCTSConfig.cs @@ -5,6 +5,8 @@ namespace Craftimizer.Solver; [StructLayout(LayoutKind.Auto)] public readonly record struct MCTSConfig { + public int MaxThreadCount { get; init; } + public int MaxStepCount { get; init; } public int MaxRolloutStepCount { get; init; } public bool StrictActions { get; init; } diff --git a/Solver/Solver.cs b/Solver/Solver.cs index a34abe9..080c5bd 100644 --- a/Solver/Solver.cs +++ b/Solver/Solver.cs @@ -64,8 +64,7 @@ public void Start() private async Task RunTask() { - if (Token.IsCancellationRequested) - return; + Token.ThrowIfCancellationRequested(); Solution = await SearchFunc().ConfigureAwait(false); } @@ -120,30 +119,38 @@ private async Task SearchStepwiseFurcated() while (activeStates.Count != 0) { - if (Token.IsCancellationRequested) - break; + Token.ThrowIfCancellationRequested(); + using var semaphore = new SemaphoreSlim(0, Config.MaxThreadCount); var s = Stopwatch.StartNew(); var tasks = new Task<(float MaxScore, int FurcatedActionIdx, SolverSolution Solution)>[Config.ForkCount]; for (var i = 0; i < Config.ForkCount; i++) { var stateIdx = (int)((float)i / Config.ForkCount * activeStates.Count); - tasks[i] = Task.Run(() => + tasks[i] = Task.Run(async () => { var solver = new MCTS(MCTSConfig, activeStates[stateIdx].State); - solver.Search(Config.Iterations / Config.ForkCount, Token); + await semaphore.WaitAsync(Token).ConfigureAwait(false); + try + { + solver.Search(Config.Iterations / Config.ForkCount, Token); + } + finally + { + semaphore.Release(); + } var solution = solver.Solution(); var progressActions = activeStates[stateIdx].Actions.Concat(solution.Actions).Skip(definiteActionCount).ToList(); OnWorkerProgress?.Invoke(solution with { Actions = progressActions }, solver.MaxScore); return (solver.MaxScore, stateIdx, solution); }, Token); } + semaphore.Release(Config.MaxThreadCount); await Task.WhenAll(tasks).WaitAsync(Token).ConfigureAwait(false); s.Stop(); OnLog?.Invoke($"{s.Elapsed.TotalMilliseconds:0.00}ms {Config.Iterations / Config.ForkCount / s.Elapsed.TotalSeconds / 1000:0.00} kI/s/t"); - if (Token.IsCancellationRequested) - break; + Token.ThrowIfCancellationRequested(); var bestActions = tasks.Select(t => t.Result).OrderByDescending(r => r.MaxScore).Take(Config.FurcatedActionCount).ToArray(); @@ -229,29 +236,37 @@ private async Task SearchStepwiseForked() var sim = new Simulator(state, Config.MaxStepCount); while (true) { - if (Token.IsCancellationRequested) - break; + Token.ThrowIfCancellationRequested(); if (sim.IsComplete) break; + using var semaphore = new SemaphoreSlim(0, Config.MaxThreadCount); var s = Stopwatch.StartNew(); var tasks = new Task<(float MaxScore, SolverSolution Solution)>[Config.ForkCount]; for (var i = 0; i < Config.ForkCount; ++i) - tasks[i] = Task.Run(() => + tasks[i] = Task.Run(async () => { var solver = new MCTS(MCTSConfig, state); - solver.Search(Config.Iterations / Config.ForkCount, Token); + await semaphore.WaitAsync(Token).ConfigureAwait(false); + try + { + solver.Search(Config.Iterations / Config.ForkCount, Token); + } + finally + { + semaphore.Release(); + } var solution = solver.Solution(); OnWorkerProgress?.Invoke(solution, solver.MaxScore); return (solver.MaxScore, solution); }, Token); + semaphore.Release(Config.MaxThreadCount); await Task.WhenAll(tasks).WaitAsync(Token).ConfigureAwait(false); s.Stop(); OnLog?.Invoke($"{s.Elapsed.TotalMilliseconds:0.00}ms {Config.Iterations / Config.ForkCount / s.Elapsed.TotalSeconds / 1000:0.00} kI/s/t"); - if (Token.IsCancellationRequested) - break; + Token.ThrowIfCancellationRequested(); var (maxScore, solution) = tasks.Select(t => t.Result).MaxBy(r => r.MaxScore); @@ -280,8 +295,7 @@ private Task SearchStepwise() var sim = new Simulator(state, Config.MaxStepCount); while (true) { - if (Token.IsCancellationRequested) - break; + Token.ThrowIfCancellationRequested(); if (sim.IsComplete) break; @@ -315,16 +329,26 @@ private Task SearchStepwise() private async Task SearchOneshotForked() { + using var semaphore = new SemaphoreSlim(0, Config.MaxThreadCount); var tasks = new Task<(float MaxScore, SolverSolution Solution)>[Config.ForkCount]; for (var i = 0; i < Config.ForkCount; ++i) - tasks[i] = Task.Run(() => + tasks[i] = Task.Run(async () => { var solver = new MCTS(MCTSConfig, State); - solver.Search(Config.Iterations / Config.ForkCount, Token); + await semaphore.WaitAsync(Token).ConfigureAwait(false); + try + { + solver.Search(Config.Iterations / Config.ForkCount, Token); + } + finally + { + semaphore.Release(); + } var solution = solver.Solution(); OnWorkerProgress?.Invoke(solution, solver.MaxScore); return (solver.MaxScore, solution); }, Token); + semaphore.Release(Config.MaxThreadCount); await Task.WhenAll(tasks).WaitAsync(Token).ConfigureAwait(false); var solution = tasks.Select(t => t.Result).MaxBy(r => r.MaxScore).Solution; diff --git a/Solver/SolverConfig.cs b/Solver/SolverConfig.cs index cbb7c5a..5122f51 100644 --- a/Solver/SolverConfig.cs +++ b/Solver/SolverConfig.cs @@ -22,6 +22,7 @@ public readonly record struct SolverConfig public float ExplorationConstant { get; init; } public int MaxStepCount { get; init; } public int MaxRolloutStepCount { get; init; } + public int MaxThreadCount { get; init; } public int ForkCount { get; init; } public int FurcatedActionCount { get; init; } public bool StrictActions { get; init; } From bdabbbdafa39a58eff7a3948ec6a1b04924aa77c Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 10 Oct 2023 00:39:23 -0700 Subject: [PATCH 10/58] API9 updates --- Benchmark/Craftimizer.Benchmark.csproj | 4 ++-- Craftimizer/Configuration.cs | 1 - Craftimizer/Craftimizer.csproj | 8 ++------ Craftimizer/Craftimizer.json | 1 - Craftimizer/Plugin.cs | 3 ++- Craftimizer/Service.cs | 6 ++++-- Craftimizer/SimulatorUtils.cs | 5 +++-- Craftimizer/Utils/Gearsets.cs | 2 +- Craftimizer/Utils/Hooks.cs | 4 ++-- Craftimizer/Utils/IconManager.cs | 6 +++--- Craftimizer/Utils/Log.cs | 12 ++++++++++++ Craftimizer/Utils/RecipeNote.cs | 5 +++-- Craftimizer/Windows/Craft.cs | 1 + Craftimizer/Windows/CraftSolver.cs | 5 ++--- Craftimizer/Windows/CraftingLog.cs | 16 +++++++--------- Craftimizer/Windows/RecipeNote.cs | 23 +++++++++++++++-------- Craftimizer/Windows/Settings.cs | 1 + Craftimizer/Windows/SimulatorDrawer.cs | 5 +++-- Craftimizer/Windows/SimulatorSolver.cs | 5 ++--- Craftimizer/packages.lock.json | 12 ++++++------ Simulator/Craftimizer.Simulator.csproj | 2 +- Solver/Craftimizer.Solver.csproj | 2 +- 22 files changed, 73 insertions(+), 56 deletions(-) create mode 100644 Craftimizer/Utils/Log.cs diff --git a/Benchmark/Craftimizer.Benchmark.csproj b/Benchmark/Craftimizer.Benchmark.csproj index f4fbfb0..9d68105 100644 --- a/Benchmark/Craftimizer.Benchmark.csproj +++ b/Benchmark/Craftimizer.Benchmark.csproj @@ -8,8 +8,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Craftimizer/Configuration.cs b/Craftimizer/Configuration.cs index efddaa8..b0c00a6 100644 --- a/Craftimizer/Configuration.cs +++ b/Craftimizer/Configuration.cs @@ -4,7 +4,6 @@ using Dalamud.Configuration; using System; using System.Collections.Generic; -using System.Threading; namespace Craftimizer.Plugin; diff --git a/Craftimizer/Craftimizer.csproj b/Craftimizer/Craftimizer.csproj index c021083..d4f8fb8 100644 --- a/Craftimizer/Craftimizer.csproj +++ b/Craftimizer/Craftimizer.csproj @@ -37,8 +37,8 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -56,10 +56,6 @@ $(DalamudLibPath)Dalamud.dll false - - $(DalamudLibPath)Dalamud.Interface.dll - false - $(DalamudLibPath)ImGui.NET.dll false diff --git a/Craftimizer/Craftimizer.json b/Craftimizer/Craftimizer.json index 745cb52..6acd17f 100644 --- a/Craftimizer/Craftimizer.json +++ b/Craftimizer/Craftimizer.json @@ -6,7 +6,6 @@ "RepoUrl": "https://github.com/WorkingRobot/craftimizer", "InternalName": "craftimizer", "ApplicableVersion": "any", - "DalamudApiLevel": 8, "Tags": [ "crafting", "doh", diff --git a/Craftimizer/Plugin.cs b/Craftimizer/Plugin.cs index c62db6e..60c9a44 100644 --- a/Craftimizer/Plugin.cs +++ b/Craftimizer/Plugin.cs @@ -2,6 +2,7 @@ using Craftimizer.Simulator; using Craftimizer.Utils; using Craftimizer.Windows; +using Dalamud.Interface.Internal; using Dalamud.Interface.Windowing; using Dalamud.IoC; using Dalamud.Plugin; @@ -18,7 +19,7 @@ public sealed class Plugin : IDalamudPlugin public string Version { get; } public string Author { get; } public string BuildConfiguration { get; } - public TextureWrap Icon { get; } + public IDalamudTextureWrap Icon { get; } public WindowSystem WindowSystem { get; } public Settings SettingsWindow { get; } diff --git a/Craftimizer/Service.cs b/Craftimizer/Service.cs index e09304e..3f41681 100644 --- a/Craftimizer/Service.cs +++ b/Craftimizer/Service.cs @@ -21,8 +21,10 @@ public sealed class Service [PluginService] public static IDataManager DataManager { get; private set; } [PluginService] public static ITextureProvider TextureProvider { get; private set; } [PluginService] public static ITargetManager TargetManager { get; private set; } - [PluginService] public static Condition Condition { get; private set; } - [PluginService] public static Framework Framework { get; private set; } + [PluginService] public static ICondition Condition { get; private set; } + [PluginService] public static IFramework Framework { get; private set; } + [PluginService] public static IPluginLog PluginLog { get; private set; } + [PluginService] public static IGameInteropProvider GameInteropProvider { get; private set; } public static Plugin Plugin { get; private set; } public static Configuration Configuration => Plugin.Configuration; diff --git a/Craftimizer/SimulatorUtils.cs b/Craftimizer/SimulatorUtils.cs index 806f40a..4ab3dd5 100644 --- a/Craftimizer/SimulatorUtils.cs +++ b/Craftimizer/SimulatorUtils.cs @@ -1,6 +1,7 @@ using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface.Internal; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Object; @@ -86,7 +87,7 @@ public static string GetName(this ActionType me, ClassJob classJob) return "Unknown"; } - public static TextureWrap GetIcon(this ActionType me, ClassJob classJob) + public static IDalamudTextureWrap GetIcon(this ActionType me, ClassJob classJob) { var (craftAction, action) = GetActionRow(me, classJob); if (craftAction != null) @@ -325,7 +326,7 @@ public static ushort GetIconId(this EffectType me, int strength) return (ushort)iconId; } - public static TextureWrap GetIcon(this EffectType me, int strength) => + public static IDalamudTextureWrap GetIcon(this EffectType me, int strength) => Service.IconManager.GetIcon(me.GetIconId(strength)); public static string GetTooltip(this EffectType me, int strength, int duration) diff --git a/Craftimizer/Utils/Gearsets.cs b/Craftimizer/Utils/Gearsets.cs index 0a6dd19..20293f8 100644 --- a/Craftimizer/Utils/Gearsets.cs +++ b/Craftimizer/Utils/Gearsets.cs @@ -33,7 +33,7 @@ public static GearsetItem[] GetGearsetItems(InventoryContainer* container) public static GearsetItem[] GetGearsetItems(RaptureGearsetModule.GearsetEntry* entry) { - var gearsetItems = new Span(entry->ItemsData, 14); + var gearsetItems = entry->ItemsSpan; var items = new GearsetItem[14]; for (var i = 0; i < 14; ++i) { diff --git a/Craftimizer/Utils/Hooks.cs b/Craftimizer/Utils/Hooks.cs index a535ed3..c177d51 100644 --- a/Craftimizer/Utils/Hooks.cs +++ b/Craftimizer/Utils/Hooks.cs @@ -20,7 +20,7 @@ public sealed unsafe class Hooks : IDisposable public Hooks() { - UseActionHook = Hook.FromAddress((nint)ActionManager.MemberFunctionPointers.UseAction, UseActionDetour); + UseActionHook = Service.GameInteropProvider.HookFromAddress((nint)ActionManager.MemberFunctionPointers.UseAction, UseActionDetour); UseActionHook.Enable(); } @@ -28,7 +28,7 @@ private bool UseActionDetour(ActionManager* manager, CSActionType actionType, ui { var canCast = manager->GetActionStatus(actionType, actionId) == 0; var ret = UseActionHook.Original(manager, actionType, actionId, targetId, param, useType, pvp, a8); - if (canCast && ret && (actionType == CSActionType.CraftAction || actionType == CSActionType.Spell)) + if (canCast && ret && (actionType == CSActionType.CraftAction || actionType == CSActionType.Action)) { var classJob = ClassJobUtils.GetClassJobFromIdx((byte)(Service.ClientState.LocalPlayer?.ClassJob.Id ?? 0)); if (classJob != null) diff --git a/Craftimizer/Utils/IconManager.cs b/Craftimizer/Utils/IconManager.cs index abefac2..c6a93da 100644 --- a/Craftimizer/Utils/IconManager.cs +++ b/Craftimizer/Utils/IconManager.cs @@ -12,7 +12,7 @@ public sealed class IconManager : IDisposable { private readonly Dictionary iconCache = new(); private readonly Dictionary textureCache = new(); - private readonly Dictionary assemblyCache = new(); + private readonly Dictionary assemblyCache = new(); public IDalamudTextureWrap GetIcon(uint id) { @@ -30,14 +30,14 @@ public IDalamudTextureWrap GetTexture(string path) return ret; } - public TextureWrap GetAssemblyTexture(string filename) + public IDalamudTextureWrap GetAssemblyTexture(string filename) { if (!assemblyCache.TryGetValue(filename, out var ret)) assemblyCache.Add(filename, ret = GetAssemblyTextureInternal(filename)); return ret; } - private static TextureWrap GetAssemblyTextureInternal(string filename) + private static IDalamudTextureWrap GetAssemblyTextureInternal(string filename) { var assembly = Assembly.GetExecutingAssembly(); byte[] iconData; diff --git a/Craftimizer/Utils/Log.cs b/Craftimizer/Utils/Log.cs new file mode 100644 index 0000000..e85ccf1 --- /dev/null +++ b/Craftimizer/Utils/Log.cs @@ -0,0 +1,12 @@ +using Craftimizer.Plugin; +using System; + +namespace Craftimizer.Utils; + +public static class Log +{ + public static void Debug(string line) => Service.PluginLog.Debug(line); + + public static void Error(string line) => Service.PluginLog.Error(line); + public static void Error(Exception e, string line) => Service.PluginLog.Error(e, line); +} diff --git a/Craftimizer/Utils/RecipeNote.cs b/Craftimizer/Utils/RecipeNote.cs index d27154b..3a372d7 100644 --- a/Craftimizer/Utils/RecipeNote.cs +++ b/Craftimizer/Utils/RecipeNote.cs @@ -2,6 +2,7 @@ using Craftimizer.Simulator; using Dalamud.Game; using Dalamud.Logging; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.UI; @@ -84,7 +85,7 @@ public RecipeNote() Service.Framework.Update += FrameworkUpdate; } - private void FrameworkUpdate(Framework f) + private void FrameworkUpdate(IFramework f) { HasValidRecipe = false; try @@ -93,7 +94,7 @@ private void FrameworkUpdate(Framework f) } catch (Exception e) { - PluginLog.LogError(e, "RecipeNote framework update failed"); + Log.Error(e, "RecipeNote framework update failed"); } } diff --git a/Craftimizer/Windows/Craft.cs b/Craftimizer/Windows/Craft.cs index 29422e1..813b063 100644 --- a/Craftimizer/Windows/Craft.cs +++ b/Craftimizer/Windows/Craft.cs @@ -2,6 +2,7 @@ using Craftimizer.Utils; using Dalamud.Interface; using Dalamud.Interface.Components; +using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using FFXIVClientStructs.FFXIV.Client.Game; using ImGuiNET; diff --git a/Craftimizer/Windows/CraftSolver.cs b/Craftimizer/Windows/CraftSolver.cs index 9149981..a2f9881 100644 --- a/Craftimizer/Windows/CraftSolver.cs +++ b/Craftimizer/Windows/CraftSolver.cs @@ -1,12 +1,11 @@ using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; +using Craftimizer.Utils; using Dalamud.Interface.Windowing; -using Dalamud.Logging; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; -using System.Threading.Tasks; namespace Craftimizer.Plugin.Windows; @@ -50,7 +49,7 @@ private void QueueSolve(SimulationState state) SolverTaskToken = new(); SolverTask = new(Config.SynthHelperSolverConfig, state) { Token = SolverTaskToken.Token }; - SolverTask.OnLog += s => PluginLog.Debug(s); + SolverTask.OnLog += s => Log.Debug(s); SolverTask.OnNewAction += SolverActionQueue.Enqueue; SolverTask.Start(); } diff --git a/Craftimizer/Windows/CraftingLog.cs b/Craftimizer/Windows/CraftingLog.cs index d4d01b5..09e0533 100644 --- a/Craftimizer/Windows/CraftingLog.cs +++ b/Craftimizer/Windows/CraftingLog.cs @@ -394,22 +394,20 @@ private void DrawGearsets() for (var i = 0; i < 100; i++) { - var gearset = inst->Gearset[i]; - if (gearset == null) + var gearset = inst->EntriesSpan[i]; + if (gearset.ID != i) continue; - if (gearset->ID != i) - continue; - if (!gearset->Flags.HasFlag(RaptureGearsetModule.GearsetFlag.Exists)) + if (!gearset.Flags.HasFlag(RaptureGearsetModule.GearsetFlag.Exists)) continue; - if (ClassJobUtils.GetClassJobFromIdx(gearset->ClassJob) != RecipeUtils.ClassJob) + if (ClassJobUtils.GetClassJobFromIdx(gearset.ClassJob) != RecipeUtils.ClassJob) continue; - var items = Gearsets.GetGearsetItems(gearset); + var items = Gearsets.GetGearsetItems(&gearset); var stats = Gearsets.CalculateCharacterStats(items, RecipeUtils.CharacterLevel, RecipeUtils.CanUseManipulation); - var gearsetId = gearset->ID + 1; + var gearsetId = gearset.ID + 1; - ImGuiUtils.BeginGroupPanel($"{SafeMemory.ReadString((nint)gearset->Name, 47)} ({gearsetId})"); + ImGuiUtils.BeginGroupPanel($"{SafeMemory.ReadString((nint)gearset.Name, 47)} ({gearsetId})"); ImGui.Text(GetCharacterStatsText(stats)); ImGui.SameLine(); if (ImGuiComponents.IconButton($"SwapGearset{gearsetId}", FontAwesomeIcon.SyncAlt)) diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index ee348fe..5c8778d 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -1,13 +1,17 @@ -using Craftimizer.Plugin; +using Craftimizer.Plugin; using Craftimizer.Plugin.Utils; using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; +using Craftimizer.Solver; using Craftimizer.Utils; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Raii; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Style; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; @@ -56,6 +60,11 @@ public enum CraftableStatus private TextureWrap SplendorousBadge { get; } private TextureWrap SpecialistBadge { get; } private TextureWrap NoManipulationBadge { get; } + private IDalamudTextureWrap ExpertBadge { get; } + private IDalamudTextureWrap CollectibleBadge { get; } + private IDalamudTextureWrap SplendorousBadge { get; } + private IDalamudTextureWrap SpecialistBadge { get; } + private IDalamudTextureWrap NoManipulationBadge { get; } private GameFontHandle AxisFont { get; } public RecipeNote() : base("Craftimizer RecipeNode", WindowFlags, false) @@ -563,14 +572,12 @@ private static string ResolveNpcResidentName(uint npcRowId) var gearsetModule = RaptureGearsetModule.Instance(); for (var i = 0; i < 100; i++) { - var gearset = gearsetModule->Gearset[i]; - if (gearset == null) + var gearset = gearsetModule->EntriesSpan[i]; + if (!gearset.Flags.HasFlag(RaptureGearsetModule.GearsetFlag.Exists)) continue; - if (!gearset->Flags.HasFlag(RaptureGearsetModule.GearsetFlag.Exists)) + if (gearset.ID != i) continue; - if (gearset->ID != i) - continue; - if (gearset->ClassJob != job.GetClassJobIndex()) + if (gearset.ClassJob != job.GetClassJobIndex()) continue; return i; } diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index 87d7e7a..85e333d 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -1,5 +1,6 @@ using Craftimizer.Solver; using Dalamud.Interface; +using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using ImGuiNET; using System; diff --git a/Craftimizer/Windows/SimulatorDrawer.cs b/Craftimizer/Windows/SimulatorDrawer.cs index acc0d57..85a8432 100644 --- a/Craftimizer/Windows/SimulatorDrawer.cs +++ b/Craftimizer/Windows/SimulatorDrawer.cs @@ -3,10 +3,11 @@ using Dalamud.Game.Text; using Dalamud.Interface; using Dalamud.Interface.Components; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Utility; using ImGuiNET; -using ImGuiScene; using System; using System.Linq; using System.Numerics; @@ -204,7 +205,7 @@ private void DrawSimulationEffects(SynthDrawParams drawParams) ImGuiUtils.BeginGroupPanel("Effects", drawParams.Total); var effectHeight = ImGui.GetFontSize() * 2f; - Vector2 GetEffectSize(TextureWrap icon) => new(icon.Width * effectHeight / icon.Height, effectHeight); + Vector2 GetEffectSize(IDalamudTextureWrap icon) => new(icon.Width * effectHeight / icon.Height, effectHeight); ImGui.Dummy(new(0, effectHeight)); ImGui.SameLine(0, 0); diff --git a/Craftimizer/Windows/SimulatorSolver.cs b/Craftimizer/Windows/SimulatorSolver.cs index de3eaf6..cd20dca 100644 --- a/Craftimizer/Windows/SimulatorSolver.cs +++ b/Craftimizer/Windows/SimulatorSolver.cs @@ -1,11 +1,10 @@ using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; +using Craftimizer.Utils; using Dalamud.Interface.Windowing; -using Dalamud.Logging; using System; using System.Collections.Concurrent; using System.Threading; -using System.Threading.Tasks; namespace Craftimizer.Plugin.Windows; @@ -85,7 +84,7 @@ private void SolveMacro(SimulationState solverState) SolverInitialActionCount = Actions.Count; SolverTaskToken = new(); SolverTask = new(Config.SimulatorSolverConfig, solverState) { Token = SolverTaskToken.Token }; - SolverTask.OnLog += s => PluginLog.Debug(s); + SolverTask.OnLog += s => Log.Debug(s); SolverTask.OnNewAction += SolverActionQueue.Enqueue; SolverTask.Start(); } diff --git a/Craftimizer/packages.lock.json b/Craftimizer/packages.lock.json index aae1cef..21e74c6 100644 --- a/Craftimizer/packages.lock.json +++ b/Craftimizer/packages.lock.json @@ -4,15 +4,15 @@ "net7.0-windows7.0": { "DalamudPackager": { "type": "Direct", - "requested": "[2.1.11, )", - "resolved": "2.1.11", - "contentHash": "9qlAWoRRTiL/geAvuwR/g6Bcbrd/bJJgVnB/RurBiyKs6srsP0bvpoo8IK+Eg8EA6jWeM6/YJWs66w4FIAzqPw==" + "requested": "[2.1.12, )", + "resolved": "2.1.12", + "contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg==" }, "Meziantou.Analyzer": { "type": "Direct", - "requested": "[2.0.62, )", - "resolved": "2.0.62", - "contentHash": "uG2CiDIm97q8KrUt8B34WdElpEDDLOe4YzrLWpwlQmesXrSX2WuJZ+HwIGWrJgDBBMi2a3tVjeF8oKjV+AhUdA==" + "requested": "[2.0.92, )", + "resolved": "2.0.92", + "contentHash": "gVyPM2gDPfxvZ2rGUKzTNZsNhdgsetfYd+OKowKwMqZ9K00j1amUU+SnlRI26629EKK4cbJWJwHs00UPXRr0BA==" }, "craftimizer.simulator": { "type": "Project" diff --git a/Simulator/Craftimizer.Simulator.csproj b/Simulator/Craftimizer.Simulator.csproj index a921291..ec99977 100644 --- a/Simulator/Craftimizer.Simulator.csproj +++ b/Simulator/Craftimizer.Simulator.csproj @@ -7,7 +7,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Solver/Craftimizer.Solver.csproj b/Solver/Craftimizer.Solver.csproj index dd73ca2..8ff7314 100644 --- a/Solver/Craftimizer.Solver.csproj +++ b/Solver/Craftimizer.Solver.csproj @@ -8,7 +8,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 7327a1a8444ba122662b744fe34f641e5cafc420 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 10 Oct 2023 00:39:38 -0700 Subject: [PATCH 11/58] Add MaxThreadCount default --- Solver/SolverConfig.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Solver/SolverConfig.cs b/Solver/SolverConfig.cs index 5122f51..d261949 100644 --- a/Solver/SolverConfig.cs +++ b/Solver/SolverConfig.cs @@ -43,6 +43,9 @@ public SolverConfig() ExplorationConstant = 4; MaxStepCount = 30; MaxRolloutStepCount = 99; + // Use 80% of all cores if less than 20 cores are available, otherwise use all but 4 cores. Keep at least 1 core. + MaxThreadCount = Math.Max(1, Math.Max(Environment.ProcessorCount - 4, (int)MathF.Floor(Environment.ProcessorCount * 0.8f))); + // Use 32 forks at minimum, or the number of cores, whichever is higher. ForkCount = Math.Max(Environment.ProcessorCount, 32); FurcatedActionCount = ForkCount / 2; StrictActions = true; From 65adab77405833e4b390970614491e93396fed4f Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 10 Oct 2023 00:40:33 -0700 Subject: [PATCH 12/58] Add multi-execution helper for simulator --- Simulator/Simulator.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Simulator/Simulator.cs b/Simulator/Simulator.cs index b049eb2..849c288 100644 --- a/Simulator/Simulator.cs +++ b/Simulator/Simulator.cs @@ -73,6 +73,20 @@ private ActionResponse Execute(ActionType action) return ActionResponse.UsedAction; } + public (ActionResponse Response, SimulationState NewState, int FailedActionIdx) ExecuteMultiple(SimulationState state, IEnumerable actions) + { + State = state; + var i = 0; + foreach(var action in actions) + { + var resp = Execute(action); + if (resp != ActionResponse.UsedAction) + return (resp, State, i); + i++; + } + return (ActionResponse.UsedAction, State, -1); + } + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public int GetEffectStrength(EffectType effect) => From f8f876d5f4d8f12cfbbc1803fd792c7d611cc2ad Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 10 Oct 2023 01:40:52 -0700 Subject: [PATCH 13/58] Implement macro scoring and suggesting --- Craftimizer/ImGuiUtils.cs | 18 +++- Craftimizer/Windows/Craft.cs | 2 +- Craftimizer/Windows/RecipeNote.cs | 121 ++++++++++++++++++++++--- Craftimizer/Windows/Settings.cs | 2 +- Craftimizer/Windows/SimulatorDrawer.cs | 4 +- Solver/Solver.cs | 2 +- 6 files changed, 128 insertions(+), 21 deletions(-) diff --git a/Craftimizer/ImGuiUtils.cs b/Craftimizer/ImGuiUtils.cs index 0e34dc6..c50b7a7 100644 --- a/Craftimizer/ImGuiUtils.cs +++ b/Craftimizer/ImGuiUtils.cs @@ -8,9 +8,6 @@ namespace Craftimizer.Plugin; internal static class ImGuiUtils { - public static float ButtonHeight => - ImGui.CalcTextSize("A").Y + (ImGui.GetStyle().FramePadding.Y * 2); - private static readonly Stack<(Vector2 Min, Vector2 Max)> GroupPanelLabelStack = new(); // Adapted from https://github.com/ocornut/imgui/issues/1496#issuecomment-655048353 @@ -165,6 +162,15 @@ public static void AlignCentered(float width) ImGui.SetCursorPosX(ImGui.GetCursorPos().X + (availWidth - width) / 2); } + public static void AlignMiddle(Vector2 size) + { + var availSize = ImGui.GetContentRegionAvail(); + if (availSize.X > size.X) + ImGui.SetCursorPosX(ImGui.GetCursorPos().X + (availSize.X - size.X) / 2); + if (availSize.Y > size.Y) + ImGui.SetCursorPosY(ImGui.GetCursorPos().Y + (availSize.Y - size.Y) / 2); + } + // https://stackoverflow.com/a/67855985 public static void TextCentered(string text) { @@ -172,6 +178,12 @@ public static void TextCentered(string text) ImGui.TextUnformatted(text); } + public static void TextMiddle(string text) + { + AlignMiddle(ImGui.CalcTextSize(text)); + ImGui.TextUnformatted(text); + } + public static bool ButtonCentered(string text) { AlignCentered(ImGui.CalcTextSize(text).X + ImGui.GetStyle().FramePadding.Y * 2); diff --git a/Craftimizer/Windows/Craft.cs b/Craftimizer/Windows/Craft.cs index 813b063..b3462f1 100644 --- a/Craftimizer/Windows/Craft.cs +++ b/Craftimizer/Windows/Craft.cs @@ -58,7 +58,7 @@ public override void Draw() var cogWidth = ImGui.CalcTextSize(FontAwesomeIcon.Cog.ToIconString()).X + (ImGui.GetStyle().FramePadding.X * 2); ImGui.PopFont(); - DrawSolveButton(new(WindowWidth - ImGui.GetStyle().ItemSpacing.X - cogWidth, ImGuiUtils.ButtonHeight)); + DrawSolveButton(new(WindowWidth - ImGui.GetStyle().ItemSpacing.X - cogWidth, ImGui.GetFrameHeight())); ImGui.SameLine(); if (ImGuiComponents.IconButton("synthSettingsButton", FontAwesomeIcon.Cog)) diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index 5c8778d..f4d4f3e 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -6,6 +6,7 @@ using Craftimizer.Utils; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; +using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; @@ -22,8 +23,11 @@ using ImGuiNET; using ImGuiScene; using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; +using System.Threading; +using System.Threading.Tasks; using ActionType = Craftimizer.Simulator.Actions.ActionType; using ClassJob = Craftimizer.Simulator.ClassJob; using CSRecipeNote = FFXIVClientStructs.FFXIV.Client.Game.UI.RecipeNote; @@ -55,11 +59,11 @@ public enum CraftableStatus public CharacterStats? CharacterStats { get; private set; } public CraftableStatus CraftStatus { get; private set; } - private TextureWrap ExpertBadge { get; } - private TextureWrap CollectibleBadge { get; } - private TextureWrap SplendorousBadge { get; } - private TextureWrap SpecialistBadge { get; } - private TextureWrap NoManipulationBadge { get; } + private CancellationTokenSource? BestMacroTokenSource { get; set; } + private Exception? BestMacroException { get; set; } + public (Macro, SimulationState)? BestSavedMacro { get; private set; } + public SolverSolution? BestSuggestedMacro { get; private set; } + private IDalamudTextureWrap ExpertBadge { get; } private IDalamudTextureWrap CollectibleBadge { get; } private IDalamudTextureWrap SplendorousBadge { get; } @@ -190,8 +194,10 @@ public override void Draw() ImGui.Separator(); ImGuiUtils.TextCentered("Best Saved Macro"); + DrawMacro("savedMacro", BestSavedMacro == null ? null : (BestSavedMacro.Value.Item1.Actions, BestSavedMacro.Value.Item2)); ImGuiUtils.ButtonCentered("View Saved Macros"); ImGuiUtils.TextCentered("Suggested Macro"); + DrawMacro("suggestedMacro", BestSuggestedMacro == null ? null : (BestSuggestedMacro.Value.Actions, BestSuggestedMacro.Value.State)); ImGuiUtils.ButtonCentered("Open Simulator"); } @@ -210,7 +216,7 @@ private void DrawCharacterStats() var levelText = string.Empty; if (level != 0) levelText = SqText.ToLevelString(level); - var imageSize = ImGuiUtils.ButtonHeight; + var imageSize = ImGui.GetFrameHeight(); bool hasSplendorous = false, hasSpecialist = false, shouldHaveManip = false; if (CraftStatus is not (CraftableStatus.LockedClassJob or CraftableStatus.WrongClassJob)) { @@ -285,7 +291,7 @@ private void DrawCharacterStats() var (questGiver, questTerritory, questLocation, mapPayload) = ResolveLevelData(unlockQuest.IssuerLocation.Row); var unlockText = $"Unlock it from {questGiver}"; - ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGuiUtils.ButtonHeight); + ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGui.GetFrameHeight()); ImGui.AlignTextToFramePadding(); ImGui.Text(unlockText); ImGui.SameLine(0, 5); @@ -319,7 +325,7 @@ private void DrawCharacterStats() var (vendorName, vendorTerritory, vendorLoation, mapPayload) = ResolveLevelData(5891399); var unlockText = $"Trade a Soul of the Crafter to {vendorName}"; - ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGuiUtils.ButtonHeight); + ImGuiUtils.AlignCentered(ImGui.CalcTextSize(unlockText).X + 5 + ImGui.GetFrameHeight()); ImGui.AlignTextToFramePadding(); ImGui.Text(unlockText); ImGui.SameLine(0, 5); @@ -335,7 +341,7 @@ private void DrawCharacterStats() { var item = RecipeData.Recipe.ItemRequired.Value!; var itemName = item.Name.ToDalamudString().ToString(); - var imageSize = ImGuiUtils.ButtonHeight; + var imageSize = ImGui.GetFrameHeight(); ImGuiUtils.TextCentered($"You are missing the required equipment."); ImGuiUtils.AlignCentered(imageSize + 5 + ImGui.CalcTextSize(itemName).X); @@ -350,7 +356,7 @@ private void DrawCharacterStats() var status = RecipeData.Recipe.StatusRequired.Value!; var statusName = status.Name.ToDalamudString().ToString(); var statusIcon = Service.IconManager.GetIcon(status.Icon); - var imageSize = new Vector2(ImGuiUtils.ButtonHeight * statusIcon.Width / statusIcon.Height, ImGuiUtils.ButtonHeight); + var imageSize = new Vector2(ImGui.GetFrameHeight() * statusIcon.Width / statusIcon.Height, ImGui.GetFrameHeight()); ImGuiUtils.TextCentered($"You are missing the required status effect."); ImGuiUtils.AlignCentered(imageSize.X + 5 + ImGui.CalcTextSize(statusName).X); @@ -416,8 +422,8 @@ private void DrawRecipeStats() var textLevel = SqText.ToLevelString(RecipeData.RecipeInfo.ClassJobLevel); var isExpert = RecipeData.RecipeInfo.IsExpert; var isCollectable = RecipeData.Recipe.ItemResult.Value!.IsCollectable; - var imageSize = ImGuiUtils.ButtonHeight; - var textSize = ImGui.CalcTextSize("A").Y; + var imageSize = ImGui.GetFrameHeight(); + var textSize = ImGui.GetFontSize(); var badgeSize = new Vector2(textSize * ExpertBadge.Width / ExpertBadge.Height, textSize); var badgeOffset = (imageSize - badgeSize.Y) / 2; @@ -488,6 +494,31 @@ private void DrawRecipeStats() } } + private void DrawMacro(string macroName, (List Actions, SimulationState State)? macro) + { + var availWidth = ImGui.GetContentRegionAvail().X; + + using var window = ImRaii.Child(macroName, new(availWidth, 2 * ImGui.GetFrameHeight()), false); + + if (macro == null) + { + if (BestMacroException == null) + ImGuiUtils.TextMiddle("Calculating..."); + else + { + ImGui.AlignTextToFramePadding(); + using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed)) + ImGuiUtils.TextCentered("An exception occurred"); + if (ImGuiUtils.ButtonCentered("Copy Error Message")) + ImGui.SetClipboardText(BestMacroException.ToString()); + } + return; + } + + ImGuiUtils.TextCentered($"{macro.Value.Actions.Count} Actions"); + ImGuiUtils.TextCentered($"{macro.Value.State.Quality} Quality"); + } + private static void DrawRequiredStatsTable(int current, int required) { if (current >= required) @@ -586,7 +617,71 @@ private static string ResolveNpcResidentName(uint npcRowId) private void CalculateBestMacros() { - throw new NotImplementedException(); + BestMacroTokenSource?.Cancel(); + BestMacroTokenSource = new(); + BestMacroException = null; + BestSavedMacro = null; + BestSuggestedMacro = null; + + var token = BestMacroTokenSource.Token; + _ = Task.Run(() => CalculateBestMacrosTask(token), token) + .ContinueWith(t => + { + if (token.IsCancellationRequested) + return; + + try + { + t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException); + } + catch (AggregateException e) + { + BestMacroException = e; + Log.Error(e, "Calculating macros failed"); + } + }, TaskContinuationOptions.OnlyOnFaulted); + } + + private void CalculateBestMacrosTask(CancellationToken token) + { + var input = new SimulationInput(CharacterStats!, RecipeData!.RecipeInfo); + var state = new SimulationState(input); + var config = Service.Configuration.SimulatorSolverConfig; + var mctsConfig = new MCTSConfig(config); + var simulator = new Solver.Simulator(state, mctsConfig.MaxStepCount); + List macros = new(Service.Configuration.Macros); + + token.ThrowIfCancellationRequested(); + + var bestSaved = macros + .Select(macro => + { + var (resp, outState, failedIdx) = simulator.ExecuteMultiple(state, macro.Actions); + outState.ActionCount = macro.Actions.Count; + var score = SimulationNode.CalculateScoreForState(outState, simulator.CompletionState, mctsConfig) ?? 0; + if (resp != ActionResponse.SimulationComplete) + { + if (failedIdx != -1) + score /= 2; + } + return (macro, outState, score); + }) + .MaxBy(m => m.score); + + token.ThrowIfCancellationRequested(); + + BestSavedMacro = (bestSaved.macro, bestSaved.outState); + + token.ThrowIfCancellationRequested(); + + var solver = new Solver.Solver(config, state) { Token = token }; + solver.OnLog += Log.Debug; + solver.Start(); + var solution = solver.GetTask().GetAwaiter().GetResult(); + + token.ThrowIfCancellationRequested(); + + BestSuggestedMacro = solution; } public void Dispose() diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index 85e333d..7fe8ce3 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -13,7 +13,7 @@ public class Settings : Window private static Configuration Config => Service.Configuration; private const int OptionWidth = 200; - private static Vector2 OptionButtonSize => new(OptionWidth, ImGuiUtils.ButtonHeight); + private static Vector2 OptionButtonSize => new(OptionWidth, ImGui.GetFrameHeight()); public const string TabGeneral = "General"; public const string TabSimulator = "Simulator"; diff --git a/Craftimizer/Windows/SimulatorDrawer.cs b/Craftimizer/Windows/SimulatorDrawer.cs index 85a8432..af6a8da 100644 --- a/Craftimizer/Windows/SimulatorDrawer.cs +++ b/Craftimizer/Windows/SimulatorDrawer.cs @@ -310,8 +310,8 @@ private void DrawSimulationButtons(SynthDrawParams drawParams) var totalWidth = drawParams.Total; var halfWidth = (totalWidth - ImGui.GetStyle().ItemSpacing.X) / 2f; var quarterWidth = (halfWidth - ImGui.GetStyle().ItemSpacing.X) / 2f; - var halfButtonSize = new Vector2(halfWidth, ImGuiUtils.ButtonHeight); - var quarterButtonSize = new Vector2(quarterWidth, ImGuiUtils.ButtonHeight); + var halfButtonSize = new Vector2(halfWidth, ImGui.GetFrameHeight()); + var quarterButtonSize = new Vector2(quarterWidth, ImGui.GetFrameHeight()); var conditionRandomnessText = "Condition Randomness"; var conditionRandomness = Config.ConditionRandomness; diff --git a/Solver/Solver.cs b/Solver/Solver.cs index 080c5bd..e07b5f0 100644 --- a/Solver/Solver.cs +++ b/Solver/Solver.cs @@ -87,7 +87,7 @@ public async Task GetTask() } catch (AggregateException e) { - e.Handle(ex => ex is OperationCanceledException); + e.Flatten().Handle(ex => ex is OperationCanceledException); } catch (OperationCanceledException) { From 9a924c8049611baea229e2dc66c50e08b1bbcdf1 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 10 Oct 2023 18:18:22 -0700 Subject: [PATCH 14/58] Ignore if semaphore is used after task is cancelled --- Solver/Solver.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Solver/Solver.cs b/Solver/Solver.cs index e07b5f0..3dd5e26 100644 --- a/Solver/Solver.cs +++ b/Solver/Solver.cs @@ -137,7 +137,11 @@ private async Task SearchStepwiseFurcated() } finally { - semaphore.Release(); + try + { + semaphore.Release(); + } + catch (ObjectDisposedException) { } } var solution = solver.Solution(); var progressActions = activeStates[stateIdx].Actions.Concat(solution.Actions).Skip(definiteActionCount).ToList(); @@ -255,7 +259,11 @@ private async Task SearchStepwiseForked() } finally { - semaphore.Release(); + try + { + semaphore.Release(); + } + catch (ObjectDisposedException) { } } var solution = solver.Solution(); OnWorkerProgress?.Invoke(solution, solver.MaxScore); @@ -342,7 +350,11 @@ private async Task SearchOneshotForked() } finally { - semaphore.Release(); + try + { + semaphore.Release(); + } + catch (ObjectDisposedException) { } } var solution = solver.Solution(); OnWorkerProgress?.Invoke(solution, solver.MaxScore); From 1da2acc5c565b217a9e5802f21d30a063ebf4321 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 10 Oct 2023 18:38:01 -0700 Subject: [PATCH 15/58] Finish crafting log helper window --- Craftimizer/Configuration.cs | 1 + Craftimizer/ImGuiUtils.cs | 128 +++++++++++++++++-- Craftimizer/Utils/RecipeNote.cs | 2 +- Craftimizer/Windows/RecipeNote.cs | 169 ++++++++++++++++++++++--- Craftimizer/Windows/Settings.cs | 10 ++ Craftimizer/Windows/SimulatorDrawer.cs | 10 +- Solver/SimulationNode.cs | 2 +- 7 files changed, 288 insertions(+), 34 deletions(-) diff --git a/Craftimizer/Configuration.cs b/Craftimizer/Configuration.cs index b0c00a6..c37a89e 100644 --- a/Craftimizer/Configuration.cs +++ b/Craftimizer/Configuration.cs @@ -26,6 +26,7 @@ public class Configuration : IPluginConfiguration public SolverConfig SimulatorSolverConfig { get; set; } = SolverConfig.SimulatorDefault; public SolverConfig SynthHelperSolverConfig { get; set; } = SolverConfig.SynthHelperDefault; public bool EnableSynthHelper { get; set; } = true; + public bool ShowOptimalMacroStat { get; set; } = true; public int SynthHelperStepCount { get; set; } = 5; public Simulator.Simulator CreateSimulator(SimulationState state) => diff --git a/Craftimizer/ImGuiUtils.cs b/Craftimizer/ImGuiUtils.cs index c50b7a7..24132e2 100644 --- a/Craftimizer/ImGuiUtils.cs +++ b/Craftimizer/ImGuiUtils.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Numerics; + namespace Craftimizer.Plugin; internal static class ImGuiUtils @@ -124,6 +125,96 @@ public static void EndGroupPanel() ImGui.EndGroup(); } + private static Vector2 UnitCircle(float theta) + { + var (s, c) = MathF.SinCos(theta); + // SinCos positive y is downwards, but we want it upwards for ImGui + return new Vector2(c, -s); + } + + private static float Lerp(float a, float b, float t) => + MathF.FusedMultiplyAdd(b - a, t, a); + + private static void ArcSegment(Vector2 o, Vector2 prev, Vector2 cur, Vector2? next, float radius, float ratio, uint color) + { + var d = ImGui.GetWindowDrawList(); + + d.PathLineTo(o + cur * radius); + d.PathLineTo(o + prev * radius); + d.PathLineTo(o + prev * radius * ratio); + d.PathLineTo(o + cur * radius * ratio); + if (next is { } nextValue) + d.PathLineTo(o + nextValue * radius); + d.PathFillConvex(color); + } + + public static void Arc(float startAngle, float endAngle, float radius, float ratio, uint backgroundColor, uint filledColor, bool addDummy = true) + { + // Fix normals when drawing (for antialiasing) + if (startAngle > endAngle) + (startAngle, endAngle) = (endAngle, startAngle); + + var offset = ImGui.GetCursorScreenPos() + new Vector2(radius); + + var segments = ImGui.GetWindowDrawList()._CalcCircleAutoSegmentCount(radius * 2); + var incrementAngle = MathF.Tau / segments; + var isFullCircle = (endAngle - startAngle) % MathF.Tau == 0; + + var prevA = startAngle; + var prev = UnitCircle(prevA); + for (var i = 1; i <= segments; ++i) + { + var a = startAngle + incrementAngle * i; + var cur = UnitCircle(a); + + var nextA = a + incrementAngle; + var next = UnitCircle(nextA); + + // full segment is background + if (prevA >= endAngle) + { + // don't overlap with the first segment + if (i == segments && !isFullCircle) + ArcSegment(offset, prev, cur, null, radius, ratio, backgroundColor); + else + ArcSegment(offset, prev, cur, next, radius, ratio, backgroundColor); + } + // segment is partially filled + else if (a > endAngle && !isFullCircle) + { + // we split the drawing in two + var end = UnitCircle(endAngle); + ArcSegment(offset, prev, end, null, radius, ratio, filledColor); + ArcSegment(offset, end, cur, next, radius, ratio, backgroundColor); + // set the previous segment to endAngle + a = endAngle; + cur = end; + } + // full segment is filled + else + { + // if the next segment will be partially filled, the next segment will be the endAngle + if (nextA > endAngle && !isFullCircle) + { + var end = UnitCircle(endAngle); + ArcSegment(offset, prev, cur, end, radius, ratio, filledColor); + } + else + ArcSegment(offset, prev, cur, next, radius, ratio, filledColor); + } + prevA = a; + prev = cur; + } + + if (addDummy) + ImGui.Dummy(new Vector2(radius * 2)); + } + + public static void ArcProgress(float value, float radiusInner, float radiusOuter, uint backgroundColor, uint filledColor) + { + Arc(MathF.PI / 2, MathF.PI / 2 - MathF.Tau * Math.Clamp(value, 0, 1), radiusInner, radiusOuter, backgroundColor, filledColor); + } + public static bool IconButtonSized(FontAwesomeIcon icon, Vector2 size) { ImGui.PushFont(UiBuilder.IconFont); @@ -155,16 +246,18 @@ public static unsafe void Hyperlink(string text, string url) } } - public static void AlignCentered(float width) + public static void AlignCentered(float width, float availWidth = default) { - var availWidth = ImGui.GetContentRegionAvail().X; + if (availWidth == default) + availWidth = ImGui.GetContentRegionAvail().X; if (availWidth > width) ImGui.SetCursorPosX(ImGui.GetCursorPos().X + (availWidth - width) / 2); } - public static void AlignMiddle(Vector2 size) + public static void AlignMiddle(Vector2 size, Vector2 availSize = default) { - var availSize = ImGui.GetContentRegionAvail(); + if (availSize == default) + availSize = ImGui.GetContentRegionAvail(); if (availSize.X > size.X) ImGui.SetCursorPosX(ImGui.GetCursorPos().X + (availSize.X - size.X) / 2); if (availSize.Y > size.Y) @@ -172,21 +265,34 @@ public static void AlignMiddle(Vector2 size) } // https://stackoverflow.com/a/67855985 - public static void TextCentered(string text) + public static void TextCentered(string text, float availWidth = default) + { + AlignCentered(ImGui.CalcTextSize(text).X, availWidth); + ImGui.TextUnformatted(text); + } + + public static void TextMiddle(string text, Vector2 availSize = default) { - AlignCentered(ImGui.CalcTextSize(text).X); + AlignMiddle(ImGui.CalcTextSize(text), availSize); ImGui.TextUnformatted(text); } - public static void TextMiddle(string text) + public static void TextMiddleNewLine(string text, Vector2 availSize) { - AlignMiddle(ImGui.CalcTextSize(text)); + if (availSize == default) + availSize = ImGui.GetContentRegionAvail(); + var c = ImGui.GetCursorPos(); + AlignMiddle(ImGui.CalcTextSize(text), availSize); ImGui.TextUnformatted(text); + ImGui.SetCursorPos(c + new Vector2(0, availSize.Y + ImGui.GetStyle().ItemSpacing.Y - 1)); } - public static bool ButtonCentered(string text) + public static bool ButtonCentered(string text, Vector2 buttonSize = default) { - AlignCentered(ImGui.CalcTextSize(text).X + ImGui.GetStyle().FramePadding.Y * 2); - return ImGui.Button(text); + var buttonWidth = buttonSize.X; + if (buttonSize == default) + buttonWidth = ImGui.CalcTextSize(text).X + ImGui.GetStyle().FramePadding.X * 2; + AlignCentered(buttonWidth); + return ImGui.Button(text, buttonSize); } } diff --git a/Craftimizer/Utils/RecipeNote.cs b/Craftimizer/Utils/RecipeNote.cs index 3a372d7..6d4f4b3 100644 --- a/Craftimizer/Utils/RecipeNote.cs +++ b/Craftimizer/Utils/RecipeNote.cs @@ -45,7 +45,7 @@ public RecipeData(ushort recipeId) RLvl = (int)Table.RowId, ConditionsFlag = Table.ConditionsFlag, MaxDurability = Table.Durability * Recipe.DurabilityFactor / 100, - MaxQuality = (int)Table.Quality * Recipe.QualityFactor / 100, + MaxQuality = (Recipe.CanHq || Recipe.IsExpert) ? (int)Table.Quality * Recipe.QualityFactor / 100 : 0, MaxProgress = Table.Difficulty * Recipe.DifficultyFactor / 100, QualityModifier = Table.QualityModifier, QualityDivider = Table.QualityDivider, diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index f4d4f3e..02ab6d4 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -10,8 +10,6 @@ using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; -using Dalamud.Interface.Style; -using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Utility; @@ -21,7 +19,6 @@ using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; -using ImGuiScene; using System; using System.Collections.Generic; using System.Linq; @@ -193,12 +190,37 @@ public override void Draw() ImGui.Separator(); - ImGuiUtils.TextCentered("Best Saved Macro"); - DrawMacro("savedMacro", BestSavedMacro == null ? null : (BestSavedMacro.Value.Item1.Actions, BestSavedMacro.Value.Item2)); - ImGuiUtils.ButtonCentered("View Saved Macros"); - ImGuiUtils.TextCentered("Suggested Macro"); - DrawMacro("suggestedMacro", BestSuggestedMacro == null ? null : (BestSuggestedMacro.Value.Actions, BestSuggestedMacro.Value.State)); - ImGuiUtils.ButtonCentered("Open Simulator"); + { + using var table = ImRaii.Table("macros", 1, ImGuiTableFlags.SizingStretchSame); + if (table) + { + ImGui.TableNextColumn(); + + ImGui.AlignTextToFramePadding(); + ImGuiUtils.TextCentered("Best Saved Macro"); + if (BestSavedMacro is { } savedMacro) + { + ImGuiUtils.TextCentered(savedMacro.Item1.Name); + DrawMacro("savedMacro", (savedMacro.Item1.Actions, savedMacro.Item2)); + } + else + { + ImGui.Text(""); + DrawMacro("savedMacro", null); + } + ImGui.Button("View Saved Macros", new(-1, 0)); + + ImGui.Separator(); + + ImGui.AlignTextToFramePadding(); + ImGuiUtils.TextCentered("Suggested Macro"); + if (BestSuggestedMacro is { } suggestedMacro) + DrawMacro("suggestedMacro", (suggestedMacro.Actions, suggestedMacro.State)); + else + DrawMacro("suggestedMacro", null); + ImGui.Button("Open Simulator", new(-1, 0)); + } + } } private void DrawCharacterStats() @@ -494,16 +516,16 @@ private void DrawRecipeStats() } } - private void DrawMacro(string macroName, (List Actions, SimulationState State)? macro) + private void DrawMacro(string imGuiId, (List Actions, SimulationState State)? macroValue) { - var availWidth = ImGui.GetContentRegionAvail().X; + //using var window = ImRaii.Child(imGuiId, new(-1, (name != null ? ImGui.GetTextLineHeightWithSpacing() : 0) + 2 * ImGui.GetFrameHeightWithSpacing()), false, ImGuiWindowFlags.AlwaysAutoResize); - using var window = ImRaii.Child(macroName, new(availWidth, 2 * ImGui.GetFrameHeight()), false); + var windowHeight = 2 * ImGui.GetFrameHeightWithSpacing(); - if (macro == null) + if (macroValue == null) { if (BestMacroException == null) - ImGuiUtils.TextMiddle("Calculating..."); + ImGuiUtils.TextMiddleNewLine("Calculating...", new(ImGui.GetContentRegionAvail().X, windowHeight + 1 + ImGui.GetStyle().ItemSpacing.Y)); else { ImGui.AlignTextToFramePadding(); @@ -515,8 +537,123 @@ private void DrawMacro(string macroName, (List Actions, SimulationSt return; } - ImGuiUtils.TextCentered($"{macro.Value.Actions.Count} Actions"); - ImGuiUtils.TextCentered($"{macro.Value.State.Quality} Quality"); + var macro = macroValue!.Value; + + using var table = ImRaii.Table("table", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame); + if (table) + { + ImGui.TableSetupColumn("desc", ImGuiTableColumnFlags.WidthFixed, 0); + ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, 0); + ImGui.TableSetupColumn("steps", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextRow(ImGuiTableRowFlags.None, windowHeight); + ImGui.TableNextColumn(); + + var spacing = ImGui.GetStyle().ItemSpacing.Y; + var miniRowHeight = (windowHeight - spacing) / 2f; + + //ImGui.Text($"{macro.Actions.Count}"); + { + if (Service.Configuration.ShowOptimalMacroStat) + { + var progressHeight = windowHeight; + if (macro.State.Progress >= macro.State.Input.Recipe.MaxProgress && macro.State.Input.Recipe.MaxQuality > 0) + { + ImGuiUtils.ArcProgress( + (float)macro.State.Quality / macro.State.Input.Recipe.MaxQuality, + progressHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Plugin.Windows.Simulator.QualityColor)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}"); + } + else + { + ImGuiUtils.ArcProgress( + (float)macro.State.Progress / macro.State.Input.Recipe.MaxProgress, + progressHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Plugin.Windows.Simulator.ProgressColor)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}"); + } + } + else + { + ImGuiUtils.ArcProgress( + (float)macro.State.Progress / macro.State.Input.Recipe.MaxProgress, + miniRowHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Plugin.Windows.Simulator.ProgressColor)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}"); + + ImGui.SameLine(0, spacing); + ImGuiUtils.ArcProgress( + (float)macro.State.Quality / macro.State.Input.Recipe.MaxQuality, + miniRowHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Plugin.Windows.Simulator.QualityColor)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}"); + + ImGuiUtils.ArcProgress((float)macro.State.Durability / macro.State.Input.Recipe.MaxDurability, + miniRowHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Plugin.Windows.Simulator.DurabilityColor)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Remaining Durability: {macro.State.Durability} / {macro.State.Input.Recipe.MaxDurability}"); + + ImGui.SameLine(0, spacing); + ImGuiUtils.ArcProgress( + (float)macro.State.CP / macro.State.Input.Stats.CP, + miniRowHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Plugin.Windows.Simulator.CPColor)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Remaining CP: {macro.State.CP} / {macro.State.Input.Stats.CP}"); + } + } + + ImGui.TableNextColumn(); + { + ImGuiUtils.TextMiddleNewLine($"{macro.Actions.Count}", new(miniRowHeight)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"{macro.Actions.Count} Step{(macro.Actions.Count != 1 ? "s" : "")}"); + using (var iconFont = ImRaii.PushFont(UiBuilder.IconFont)) + if (ImGuiUtils.ButtonCentered(FontAwesomeIcon.Copy.ToIconString(), new(miniRowHeight))) + { + throw new NotImplementedException(); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Copy to Clipboard"); + } + + ImGui.TableNextColumn(); + { + var itemsPerRow = (int)MathF.Ceiling((ImGui.GetContentRegionAvail().X + spacing) / (miniRowHeight + spacing)); + var itemCount = Math.Min(macro.Actions.Count, itemsPerRow * 2); + for (var i = 0; i < itemsPerRow * 2; i++) + { + if (i % itemsPerRow != 0) + ImGui.SameLine(0, spacing); + if (i < itemCount) + { + ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(macro.Actions[i].GetName(RecipeData!.ClassJob)); + } + else + ImGui.Dummy(new(miniRowHeight)); + } + } + } } private static void DrawRequiredStatsTable(int current, int required) diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index 7fe8ce3..a41261e 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -154,6 +154,16 @@ ref isDirty ref isDirty ); + DrawOption( + "Show Only One Macro Stat", + "Only one stat will be shown for a macro. If a craft will be finished, quality\n" + + "is shown. Otherwise, progress is shown. Durability and remaining CP will be\n" + + "hidden.", + Config.ShowOptimalMacroStat, + v => Config.ShowOptimalMacroStat = v, + ref isDirty + ); + if (isDirty) Config.Save(); diff --git a/Craftimizer/Windows/SimulatorDrawer.cs b/Craftimizer/Windows/SimulatorDrawer.cs index af6a8da..615eadc 100644 --- a/Craftimizer/Windows/SimulatorDrawer.cs +++ b/Craftimizer/Windows/SimulatorDrawer.cs @@ -24,11 +24,11 @@ public sealed partial class Simulator : Window, IDisposable private static readonly Vector2 ProgressBarSizeOld = new(200, 20); public static readonly Vector2 TooltipProgressBarSize = new(100, 5); - private static readonly Vector4 ProgressColor = new(0.44f, 0.65f, 0.18f, 1f); - private static readonly Vector4 QualityColor = new(0.26f, 0.71f, 0.69f, 1f); - private static readonly Vector4 DurabilityColor = new(0.13f, 0.52f, 0.93f, 1f); - private static readonly Vector4 HQColor = new(0.592f, 0.863f, 0.376f, 1f); - private static readonly Vector4 CPColor = new(0.63f, 0.37f, 0.75f, 1f); + public static readonly Vector4 ProgressColor = new(0.44f, 0.65f, 0.18f, 1f); + public static readonly Vector4 QualityColor = new(0.26f, 0.71f, 0.69f, 1f); + public static readonly Vector4 DurabilityColor = new(0.13f, 0.52f, 0.93f, 1f); + public static readonly Vector4 HQColor = new(0.592f, 0.863f, 0.376f, 1f); + public static readonly Vector4 CPColor = new(0.63f, 0.37f, 0.75f, 1f); private static readonly Vector4 BadActionImageTint = new(1f, .5f, .5f, 1f); private static readonly Vector4 BadActionImageColor = new(1f, .3f, .3f, 1f); diff --git a/Solver/SimulationNode.cs b/Solver/SimulationNode.cs index e17c35f..3f8639e 100644 --- a/Solver/SimulationNode.cs +++ b/Solver/SimulationNode.cs @@ -47,7 +47,7 @@ private static bool CanByregot(SimulationState state) return null; static float Apply(float bonus, float value, float target) => - bonus * Math.Min(1f, value / target); + bonus * (target > 0 ? Math.Min(1f, value / target) : 1); var progressScore = Apply( config.ScoreProgress, From 398c7f05004d2157b2fd8e21814d2d076314cd5e Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 10 Oct 2023 23:36:17 -0700 Subject: [PATCH 16/58] Remove old windows --- Craftimizer/Plugin.cs | 11 - Craftimizer/Windows/CraftingLog.cs | 578 -------------------- Craftimizer/Windows/RecipeNote.cs | 2 +- Craftimizer/Windows/Simulator.cs | 64 --- Craftimizer/Windows/SimulatorActions.cs | 89 --- Craftimizer/Windows/SimulatorDrawer.cs | 440 --------------- Craftimizer/Windows/SimulatorDrawerUtils.cs | 146 ----- Craftimizer/Windows/SimulatorSolver.cs | 100 ---- 8 files changed, 1 insertion(+), 1429 deletions(-) delete mode 100644 Craftimizer/Windows/CraftingLog.cs delete mode 100644 Craftimizer/Windows/Simulator.cs delete mode 100644 Craftimizer/Windows/SimulatorActions.cs delete mode 100644 Craftimizer/Windows/SimulatorDrawer.cs delete mode 100644 Craftimizer/Windows/SimulatorDrawerUtils.cs delete mode 100644 Craftimizer/Windows/SimulatorSolver.cs diff --git a/Craftimizer/Plugin.cs b/Craftimizer/Plugin.cs index 60c9a44..cbb5534 100644 --- a/Craftimizer/Plugin.cs +++ b/Craftimizer/Plugin.cs @@ -25,7 +25,6 @@ public sealed class Plugin : IDalamudPlugin public Settings SettingsWindow { get; } public Craftimizer.Windows.RecipeNote RecipeNoteWindow { get; } public Craft SynthesisWindow { get; } - public Windows.Simulator? SimulatorWindow { get; set; } public Configuration Configuration { get; } public Hooks Hooks { get; } @@ -56,16 +55,6 @@ public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface) Service.PluginInterface.UiBuilder.OpenConfigUi += OpenSettingsWindow; } - public void OpenSimulatorWindow(Item item, bool isExpert, SimulationInput input, ClassJob classJob, Macro? macro) - { - if (SimulatorWindow != null) - { - SimulatorWindow.IsOpen = false; - WindowSystem.RemoveWindow(SimulatorWindow); - } - SimulatorWindow = new(item, isExpert, input, classJob, macro); - } - public void OpenSettingsWindow() { SettingsWindow.IsOpen = true; diff --git a/Craftimizer/Windows/CraftingLog.cs b/Craftimizer/Windows/CraftingLog.cs deleted file mode 100644 index 09e0533..0000000 --- a/Craftimizer/Windows/CraftingLog.cs +++ /dev/null @@ -1,578 +0,0 @@ -using Craftimizer.Plugin.Utils; -using Craftimizer.Simulator; -using Craftimizer.Simulator.Actions; -using Dalamud; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using Dalamud.Interface.Windowing; -using Dalamud.Utility; -using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.UI.Misc; -using FFXIVClientStructs.FFXIV.Component.GUI; -using ImGuiNET; -using Lumina.Excel.GeneratedSheets; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Text; -using ActionType = Craftimizer.Simulator.Actions.ActionType; -using RecipeNote = Craftimizer.Utils.RecipeNote; - -namespace Craftimizer.Plugin.Windows; - -public unsafe class CraftingLog : Window -{ - private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration - | ImGuiWindowFlags.AlwaysAutoResize - | ImGuiWindowFlags.NoSavedSettings - | ImGuiWindowFlags.NoFocusOnAppearing - | ImGuiWindowFlags.NoNavFocus; - - private static Configuration Config => Service.Configuration; - - private const int LeftSideWidth = 350; - - // If relative, increase stat by Value's % (rounded down), and cap increase to Max - // If not relative, increase stat by Value, and ignore Max - [StructLayout(LayoutKind.Auto)] - private record struct FoodStat(bool IsRelative, sbyte Value, short Max, sbyte ValueHQ, short MaxHQ); - private sealed record Food(Item Item, string Name, string NameHQ, FoodStat? Craftsmanship, FoodStat? Control, FoodStat? CP); - - private static Food[] FoodItems { get; } - private static Food[] MedicineItems { get; } - private static Random Random { get; } - - private static RecipeNote RecipeUtils => Service.Plugin.RecipeNote; - private ushort OldRecipeId { get; set; } - - // Set in CalculateCharacterStats (in PreDraw) - private Gearsets.GearsetItem[] CharacterEquipment { get; set; } = null!; - private CharacterStats CharacterStatsNoConsumable { get; set; } = null!; - private Gearsets.GearsetStats CharacterConsumableBonus { get; set; } - private CharacterStats CharacterStatsConsumable { get; set; } = null!; - private CannotCraftReason CharacterCannotCraftReason { get; set; } - private SimulationInput CharacterSimulationInput { get; set; } = null!; - - // Set in UI - private int QualityNotches { get; set; } - private int StartingQuality => - RecipeUtils.HQIngredientCount == 0 ? - 0 : - (int)((float)QualityNotches * RecipeUtils.MaxStartingQuality / RecipeUtils.HQIngredientCount); - - private Food? SelectedFood { get; set; } - private bool SelectedFoodHQ { get; set; } - - private Food? SelectedMedicine { get; set; } - private bool SelectedMedicineHQ { get; set; } - - static CraftingLog() - { - var foods = new List(); - var medicines = new List(); - foreach (var item in LuminaSheets.ItemSheet) - { - var isFood = item.ItemUICategory.Row == 46; - var isMedicine = item.ItemUICategory.Row == 44; - if (!isFood && !isMedicine) - continue; - - if (item.ItemAction.Value == null) - continue; - - if (!(item.ItemAction.Value.Type is 844 or 845 or 846)) - continue; - - var itemFood = LuminaSheets.ItemFoodSheet.GetRow(item.ItemAction.Value.Data[1]); - if (itemFood == null) - continue; - - FoodStat? craftsmanship = null, control = null, cp = null; - foreach (var stat in itemFood.UnkData1) - { - if (stat.BaseParam == 0) - continue; - var foodStat = new FoodStat(stat.IsRelative, stat.Value, stat.Max, stat.ValueHQ, stat.MaxHQ); - switch (stat.BaseParam) - { - case Gearsets.ParamCraftsmanship: craftsmanship = foodStat; break; - case Gearsets.ParamControl: control = foodStat; break; - case Gearsets.ParamCP: cp = foodStat; break; - default: continue; - } - } - - if (craftsmanship != null || control != null || cp != null) - { - var name = item.Name.ToDalamudString().TextValue ?? $"Unknown ({item.RowId})"; - var food = new Food(item, name, $"{name} (HQ)", craftsmanship, control, cp); - if (isFood) - foods.Add(food); - if (isMedicine) - medicines.Add(food); - } - } - foods.Sort((a, b) => b.Item.LevelItem.Row.CompareTo(a.Item.LevelItem.Row)); - medicines.Sort((a, b) => b.Item.LevelItem.Row.CompareTo(a.Item.LevelItem.Row)); - FoodItems = foods.ToArray(); - MedicineItems = medicines.ToArray(); - - Random = new(); - } - - public CraftingLog() : base("Craftimizer RecipeNoteHelper", WindowFlags, true) - { - Service.WindowSystem.AddWindow(this); - - IsOpen = true; - } - - private void CalculateCharacterStats() - { - var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems); - if (container == null) - return; - - CharacterEquipment = Gearsets.GetGearsetItems(container); - CharacterStatsNoConsumable = Gearsets.CalculateCharacterStats(CharacterEquipment, RecipeUtils.CharacterLevel, RecipeUtils.CanUseManipulation); - CharacterConsumableBonus = CalculateConsumableBonus(CharacterStatsNoConsumable); - CharacterStatsConsumable = CharacterStatsNoConsumable with - { - Craftsmanship = CharacterStatsNoConsumable.Craftsmanship + CharacterConsumableBonus.Craftsmanship, - Control = CharacterStatsNoConsumable.Control + CharacterConsumableBonus.Control, - CP = CharacterStatsNoConsumable.CP + CharacterConsumableBonus.CP, - }; - CharacterCannotCraftReason = Config.OverrideUncraftability ? CannotCraftReason.OK : CanCraftRecipe(CharacterEquipment, CharacterStatsConsumable); - - CharacterSimulationInput = new(CharacterStatsConsumable, RecipeUtils.Info, StartingQuality, Random); - } - - public override void Draw() - { - ImGui.BeginTable("craftlog", 2, ImGuiTableFlags.BordersInnerV); - - ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, LeftSideWidth); - ImGui.TableNextColumn(); - DrawCraftInfo(); - - ImGui.TableNextColumn(); - DrawGearsets(); - - ImGui.EndTable(); - } - - private void DrawCraftInfo() - { - ImGui.BeginTable("craftinfo", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame); - - ImGui.TableNextColumn(); - DrawRecipeInfo(); - - ImGui.TableNextColumn(); - DrawCharacterInfo(); - ImGui.EndTable(); - - ImGui.Separator(); - - DrawCraftParameters(); - DrawMacros(); - } - - private void DrawRecipeInfo() - { - var s = new StringBuilder(); - s.AppendLine($"{RecipeUtils.ClassJob.GetName()} {new string('★', RecipeUtils.Table.Stars)}"); - s.AppendLine($"Level {RecipeUtils.Table.ClassJobLevel} (RLvl {RecipeUtils.Info.RLvl})"); - s.AppendLine($"Durability: {RecipeUtils.Info.MaxDurability}"); - s.AppendLine($"Progress: {RecipeUtils.Info.MaxProgress}"); - s.AppendLine($"Quality: {RecipeUtils.Info.MaxQuality}"); - ImGui.Text(s.ToString()); - } - - private void DrawCharacterInfo() - { - if (CharacterCannotCraftReason != CannotCraftReason.OK) - { - ImGui.TextWrapped(GetCannotCraftReasonText(CharacterCannotCraftReason)); - return; - } - - ImGui.Text(GetCharacterStatsText(CharacterStatsConsumable)); - } - - private void DrawCraftParameters() - { - ImGui.BeginDisabled(RecipeUtils.HQIngredientCount == 0); - var qualityNotches = QualityNotches; - ImGui.SetNextItemWidth(LeftSideWidth - 115); - if (ImGui.SliderInt("Starting Quality", ref qualityNotches, 0, RecipeUtils.HQIngredientCount, StartingQuality.ToString(), ImGuiSliderFlags.NoInput | ImGuiSliderFlags.AlwaysClamp)) - QualityNotches = qualityNotches; - ImGui.EndDisabled(); - - ImGui.BeginTable("craftfood", 2, ImGuiTableFlags.BordersInnerV); - - ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, LeftSideWidth - 120); - ImGui.TableNextColumn(); - - if (ImGui.BeginCombo("Food", SelectedFood != null ? (SelectedFoodHQ ? SelectedFood.NameHQ : SelectedFood.Name) : "None")) - { - if (ImGui.Selectable("None", SelectedFood == null)) - { - SelectedFood = null; - SelectedFoodHQ = false; - } - - foreach (var food in FoodItems) - { - if (ImGui.Selectable(food.Name, food == SelectedFood && !SelectedFoodHQ)) - { - SelectedFood = food; - SelectedFoodHQ = false; - } - else if (ImGui.Selectable($"{food.Name} (HQ)", food == SelectedFood && SelectedFoodHQ)) - { - SelectedFood = food; - SelectedFoodHQ = true; - } - } - - ImGui.EndCombo(); - } - - if (ImGui.BeginCombo("Medicine", SelectedMedicine != null ? (SelectedMedicineHQ ? SelectedMedicine.NameHQ : SelectedMedicine.Name) : "None")) - { - if (ImGui.Selectable("None", SelectedMedicine == null)) - { - SelectedMedicine = null; - SelectedMedicineHQ = false; - } - - - foreach (var food in MedicineItems) - { - if (ImGui.Selectable(food.Name, food == SelectedMedicine && !SelectedMedicineHQ)) - { - SelectedMedicine = food; - SelectedMedicineHQ = false; - } - else if (ImGui.Selectable($"{food.Name} (HQ)", food == SelectedMedicine && SelectedMedicineHQ)) - { - SelectedMedicine = food; - SelectedMedicineHQ = true; - } - } - - ImGui.EndCombo(); - } - - ImGui.TableNextColumn(); - - var s = new StringBuilder(); - s.AppendLine($"+{CharacterConsumableBonus.Craftsmanship} Craftsmanship"); - s.AppendLine($"+{CharacterConsumableBonus.Control} Control"); - s.AppendLine($"+{CharacterConsumableBonus.CP} CP"); - ImGui.Text(s.ToString()); - - ImGui.EndTable(); - } - - private void DrawMacros() - { - var padding = ImGui.GetStyle().FramePadding; - - var fontSize = ImGui.GetFontSize(); - var height = fontSize + (padding.Y * 2); - var width = ImGui.GetContentRegionAvail().X; - var size = new Vector2(width, height); - var infoColWidth = Simulator.TooltipProgressBarSize.X; - var infoButtonCount = 3; - var infoButtonWidth = (infoColWidth - ImGui.GetStyle().ItemSpacing.X * (infoButtonCount - 1)) / infoButtonCount; - var infoButtonSize = new Vector2(infoButtonWidth, height); - var actionColWidth = width - infoColWidth - ImGui.GetStyle().FramePadding.X * 2; - var actionCount = 6; - var actionSize = new Vector2((actionColWidth - (ImGui.GetStyle().ItemSpacing.X * (actionCount - 1))) / actionCount); - - if (ImGui.Button("Open Simulator", size)) - OpenSimulatorWindow(null); - ImGui.SameLine(); - ImGui.Button("Generate a new macro", size); - - ImGui.BeginTable("macrotable", 2, ImGuiTableFlags.BordersInner); - ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, infoColWidth); - ImGui.TableSetupColumn(""); - var simulation = new SimulatorNoRandom(new(CharacterSimulationInput)); - for (var i = 0; i < Config.Macros.Count; ++i) - { - var macro = Config.Macros[i]; - ImGui.PushID(i); - ImGui.TableNextRow(); - - SimulationState? state = null; - if (CharacterCannotCraftReason == CannotCraftReason.OK) - { - state = new(CharacterSimulationInput); - foreach (var action in macro.Actions) - (_, state) = simulation.Execute(state.Value, action); - } - - ImGui.TableNextColumn(); - ImGui.TextWrapped(macro.Name); - if (state.HasValue) - Simulator.DrawAllProgressTooltips(state!.Value); - - if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Copy, infoButtonSize)) - CopyMacroToClipboard(macro); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Copy macro to clipboard\nHold Shift to exclude wait modifiers"); - ImGui.SameLine(); - if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.ShareSquare, infoButtonSize)) - OpenSimulatorWindow(macro); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Open macro in simulator"); - ImGui.SameLine(); - if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Trash, infoButtonSize)) - Config.Macros.RemoveAt(i); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Delete macro"); - - ImGui.TableNextColumn(); - var j = 0; - foreach (var action in macro.Actions) - { - ImGui.Image(action.GetIcon(RecipeUtils.ClassJob).ImGuiHandle, actionSize); - if (j++ % actionCount != actionCount - 1) - ImGui.SameLine(); - if (j == actionCount * 2) - break; - } - ImGui.Dummy(Vector2.Zero); - ImGui.PopID(); - } - ImGui.EndTable(); - } - - private void OpenSimulatorWindow(Macro? macro) - { - Service.Plugin.OpenSimulatorWindow(RecipeUtils.Recipe.ItemResult.Value!, RecipeUtils.Recipe.IsExpert, CharacterSimulationInput, RecipeUtils.ClassJob, macro); - } - - private string GetMacroCommand(ActionType action, bool addWaitTimes) - { - var actionBase = action.Base(); - if (actionBase is BaseComboAction comboActionBase) - return $"{GetMacroCommand(comboActionBase.ActionTypeA, addWaitTimes)}\n{GetMacroCommand(comboActionBase.ActionTypeB, addWaitTimes)}"; - if (addWaitTimes) - return $"/ac \"{action.GetName(RecipeUtils.ClassJob)}\" "; - else - return $"/ac \"{action.GetName(RecipeUtils.ClassJob)}\""; - } - - private void CopyMacroToClipboard(Macro macro) - { - var s = new StringBuilder(); - if (ImGui.IsKeyDown(ImGuiKey.ModShift)) - { - foreach (var action in macro.Actions) - s.AppendLine(GetMacroCommand(action, false)); - } - else - { - foreach (var action in macro.Actions) - s.AppendLine(GetMacroCommand(action, true)); - s.AppendLine($"/echo Macro Complete! "); - } - ImGui.SetClipboardText(s.ToString()); - } - - private void DrawGearsets() - { - ImGui.Text("Available Gearsets"); - - var inst = RaptureGearsetModule.Instance(); - - for (var i = 0; i < 100; i++) - { - var gearset = inst->EntriesSpan[i]; - if (gearset.ID != i) - continue; - if (!gearset.Flags.HasFlag(RaptureGearsetModule.GearsetFlag.Exists)) - continue; - - if (ClassJobUtils.GetClassJobFromIdx(gearset.ClassJob) != RecipeUtils.ClassJob) - continue; - - var items = Gearsets.GetGearsetItems(&gearset); - var stats = Gearsets.CalculateCharacterStats(items, RecipeUtils.CharacterLevel, RecipeUtils.CanUseManipulation); - var gearsetId = gearset.ID + 1; - - ImGuiUtils.BeginGroupPanel($"{SafeMemory.ReadString((nint)gearset.Name, 47)} ({gearsetId})"); - ImGui.Text(GetCharacterStatsText(stats)); - ImGui.SameLine(); - if (ImGuiComponents.IconButton($"SwapGearset{gearsetId}", FontAwesomeIcon.SyncAlt)) - Chat.SendMessage($"/gearset change {gearsetId}"); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip($"Swap to gearset {gearsetId}"); - ImGuiUtils.EndGroupPanel(); - } - } - - public override bool DrawConditions() - { - if (!RecipeUtils.HasValidRecipe) - return false; - - if (OldRecipeId != RecipeUtils.RecipeId) - QualityNotches = 0; - OldRecipeId = RecipeUtils.RecipeId; - - if (RecipeUtils.AddonRecipe == null) - return false; - - // Check if RecipeNote addon is visible - if (RecipeUtils.AddonRecipe->AtkUnitBase.WindowNode == null) - return false; - - // Check if RecipeNote has a visible selected recipe - if (!RecipeUtils.AddonRecipe->Unk258->IsVisible) - return false; - - return base.DrawConditions(); - } - - public override unsafe void PreDraw() - { - var addon = RecipeUtils.AddonRecipe; - ref var unit = ref addon->AtkUnitBase; - var scale = unit.Scale; - var pos = new Vector2(unit.X, unit.Y); - var size = new Vector2(unit.WindowNode->AtkResNode.Width, unit.WindowNode->AtkResNode.Height) * scale; - - var node = (AtkResNode*)addon->Unk458; // unit.GetNodeById(59); - var nodeParent = addon->Unk258; // unit.GetNodeById(57); - - Position = pos + new Vector2(size.X, (nodeParent->Y + node->Y) * scale); - SizeConstraints = new WindowSizeConstraints - { - MinimumSize = new(-1), - MaximumSize = new(10000, 10000) - }; - - CalculateCharacterStats(); - - base.PreDraw(); - } - - private Gearsets.GearsetStats CalculateConsumableBonus(CharacterStats stats) - { - static int CalculateBonus(int param, bool isHq, FoodStat? stat) - { - if (stat == null) - return 0; - - var foodStat = stat.Value; - var (value, max) = isHq ? (foodStat.ValueHQ, foodStat.MaxHQ) : (foodStat.Value, foodStat.Max); - - if (!foodStat.IsRelative) - return value; - - return Math.Min((int)MathF.Floor((float)value * param), max); - } - - Gearsets.GearsetStats ret = new(); - - if (SelectedFood != null) - { - ret.CP += CalculateBonus(stats.CP, SelectedFoodHQ, SelectedFood.CP); - ret.Craftsmanship += CalculateBonus(stats.Craftsmanship, SelectedFoodHQ, SelectedFood.Craftsmanship); - ret.Control += CalculateBonus(stats.Control, SelectedFoodHQ, SelectedFood.Control); - } - - if (SelectedMedicine != null) - { - ret.CP += CalculateBonus(stats.CP, SelectedMedicineHQ, SelectedMedicine.CP); - ret.Craftsmanship += CalculateBonus(stats.Craftsmanship, SelectedMedicineHQ, SelectedMedicine.Craftsmanship); - ret.Control += CalculateBonus(stats.Control, SelectedMedicineHQ, SelectedMedicine.Control); - } - - return ret; - } - - private enum CannotCraftReason - { - OK, - WrongClassJob, - SpecialistRequired, - RequiredItem, - RequiredStatus, - CraftsmanshipTooLow, - ControlTooLow, - } - - private CannotCraftReason CanCraftRecipe(Gearsets.GearsetItem[] items, CharacterStats stats) - { - if (ClassJobUtils.GetClassJobFromIdx((byte)Service.ClientState.LocalPlayer!.ClassJob.Id) != RecipeUtils.ClassJob) - return CannotCraftReason.WrongClassJob; - - var recipe = RecipeUtils.Recipe; - - if (recipe.IsSpecializationRequired && !stats.IsSpecialist) - return CannotCraftReason.SpecialistRequired; - - if (recipe.ItemRequired.Row != 0) - { - if (recipe.ItemRequired.Value != null) - { - if (!items.Any(i => Gearsets.IsItem(i, recipe.ItemRequired.Row))) - { - return CannotCraftReason.RequiredItem; - } - } - } - - if (recipe.StatusRequired.Row != 0) - { - if (recipe.StatusRequired.Value != null) - { - if (!Service.ClientState.LocalPlayer.StatusList.Any(s => s.StatusId == recipe.StatusRequired.Row)) - return CannotCraftReason.RequiredStatus; - } - } - - if (recipe.RequiredCraftsmanship > stats.Craftsmanship) - return CannotCraftReason.CraftsmanshipTooLow; - - if (recipe.RequiredControl > stats.Control) - return CannotCraftReason.ControlTooLow; - - return CannotCraftReason.OK; - } - - private static string GetCannotCraftReasonText(CannotCraftReason reason) => - reason switch - { - CannotCraftReason.OK => "You can craft this recipe.", - CannotCraftReason.WrongClassJob => "Your current class cannot craft this recipe.", - CannotCraftReason.SpecialistRequired => "You must be a specialist to craft this recipe.", - CannotCraftReason.RequiredItem => "You do not have the required item to craft this recipe.", - CannotCraftReason.RequiredStatus => "You do not have the required status effect to craft this recipe.", - CannotCraftReason.CraftsmanshipTooLow => "Your craftsmanship is too low to craft this recipe.", - CannotCraftReason.ControlTooLow => "Your control is too low to craft this recipe.", - _ => "Unknown reason.", - }; - - private static string GetCharacterStatsText(CharacterStats stats) - { - var s = new StringBuilder(); - s.AppendLine($"Level {stats.Level} (CLvl {stats.CLvl})"); - s.AppendLine($"Craftsmanship {stats.Craftsmanship}"); - s.AppendLine($"Control {stats.Control}"); - s.AppendLine($"CP {stats.CP}"); - if (stats.IsSpecialist) - s.AppendLine($" + Specialist"); - if (stats.HasSplendorousBuff) - s.AppendLine($" + Splendorous Tool"); - return s.ToString(); - } -} diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index 02ab6d4..185c489 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -68,7 +68,7 @@ public enum CraftableStatus private IDalamudTextureWrap NoManipulationBadge { get; } private GameFontHandle AxisFont { get; } - public RecipeNote() : base("Craftimizer RecipeNode", WindowFlags, false) + public RecipeNote() : base("Craftimizer RecipeNote", WindowFlags, false) { ExpertBadge = Service.IconManager.GetAssemblyTexture("Graphics.expert_badge.png"); CollectibleBadge = Service.IconManager.GetAssemblyTexture("Graphics.collectible_badge.png"); diff --git a/Craftimizer/Windows/Simulator.cs b/Craftimizer/Windows/Simulator.cs deleted file mode 100644 index 11f81e8..0000000 --- a/Craftimizer/Windows/Simulator.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Craftimizer.Simulator; -using Craftimizer.Simulator.Actions; -using Dalamud.Interface.Windowing; -using ImGuiNET; -using Lumina.Excel.GeneratedSheets; -using System; -using System.Collections.Generic; -using ClassJob = Craftimizer.Simulator.ClassJob; - -namespace Craftimizer.Plugin.Windows; - -public sealed partial class Simulator : Window, IDisposable -{ - private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.AlwaysAutoResize; - - private static Configuration Config => Service.Configuration; - - private Item Item { get; } - private bool IsExpert { get; } - private SimulationInput Input { get; } - private ClassJob ClassJob { get; } - private Macro? Macro { get; set; } - private string MacroName { get; set; } - // State is the state of the simulation *after* its corresponding action is executed. - private List<(ActionType Action, string Tooltip, ActionResponse Response, SimulationState State)> Actions { get; } - private Craftimizer.Simulator.Simulator Sim { get; set; } - - private SimulationState LatestState => Actions.Count == 0 ? new(Input) : Actions[^1].State; - - // Simulator is set by ResetSimulator() -#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - public Simulator(Item item, bool isExpert, SimulationInput input, ClassJob classJob, Macro? macro) : base("Craftimizer Simulator", WindowFlags) -#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - { - Service.WindowSystem.AddWindow(this); - - Item = item; - IsExpert = isExpert; - Input = input; - ClassJob = classJob; - Macro = macro; - MacroName = Macro?.Name ?? $"Macro {Config.Macros.Count + 1}"; - Actions = new(); - ResetSimulator(); - - IsOpen = true; - - CollapsedCondition = ImGuiCond.Appearing; - Collapsed = false; - - SizeCondition = ImGuiCond.Appearing; - Size = SizeConstraints?.MinimumSize ?? new(10); - - if (Macro != null) - foreach (var action in Macro.Actions) - AppendAction(action); - } - - private void ResetSimulator() - { - Sim = Config.CreateSimulator(LatestState); - ReexecuteAllActions(); - } -} diff --git a/Craftimizer/Windows/SimulatorActions.cs b/Craftimizer/Windows/SimulatorActions.cs deleted file mode 100644 index 604a2e7..0000000 --- a/Craftimizer/Windows/SimulatorActions.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Craftimizer.Simulator.Actions; -using Dalamud.Interface.Windowing; -using System; - -namespace Craftimizer.Plugin.Windows; - -public sealed partial class Simulator : Window, IDisposable -{ - private void AppendAction(ActionType action) - { - OnActionsChanged(); - - AppendGeneratedAction(action); - } - - private void AppendGeneratedAction(ActionType action) - { - var actionBase = action.Base(); - if (actionBase is BaseComboAction comboActionBase) - { - AppendGeneratedAction(comboActionBase.ActionTypeA); - AppendGeneratedAction(comboActionBase.ActionTypeB); - } - else - { - var tooltip = actionBase.GetTooltip(Sim, false); - var (response, state) = Sim.Execute(LatestState, action); - Actions.Add((action, tooltip, response, state)); - } - } - - private void RemoveAction(int actionIndex) - { - OnActionsChanged(); - - // Remove action - Actions.RemoveAt(actionIndex); - - // Take note of all actions afterwards - Span succeedingActions = stackalloc ActionType[Actions.Count - actionIndex]; - for (var i = 0; i < succeedingActions.Length; i++) - succeedingActions[i] = Actions[i + actionIndex].Action; - - // Remove all future actions - Actions.RemoveRange(actionIndex, succeedingActions.Length); - - // Re-execute all future actions - foreach (var action in succeedingActions) - AppendAction(action); - } - - private void InsertAction(int actionIndex, ActionType action) - { - OnActionsChanged(); - - // Take note of all actions afterwards - Span succeedingActions = stackalloc ActionType[Actions.Count - actionIndex]; - for (var i = 0; i < succeedingActions.Length; i++) - succeedingActions[i] = Actions[i + actionIndex].Action; - - // Remove all future actions - Actions.RemoveRange(actionIndex, succeedingActions.Length); - - // Execute new action - AppendAction(action); - - // Re-execute all future actions - foreach (var succeededAction in succeedingActions) - AppendAction(succeededAction); - } - - private void ClearAllActions() - { - OnActionsChanged(); - - Actions.Clear(); - } - - private void ReexecuteAllActions() - { - Span actions = stackalloc ActionType[Actions.Count]; - for (var i = 0; i < actions.Length; i++) - actions[i] = Actions[i].Action; - - Actions.Clear(); - foreach (var action in actions) - AppendAction(action); - } -} diff --git a/Craftimizer/Windows/SimulatorDrawer.cs b/Craftimizer/Windows/SimulatorDrawer.cs deleted file mode 100644 index 615eadc..0000000 --- a/Craftimizer/Windows/SimulatorDrawer.cs +++ /dev/null @@ -1,440 +0,0 @@ -using Craftimizer.Simulator; -using Craftimizer.Simulator.Actions; -using Dalamud.Game.Text; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; -using Dalamud.Interface.Windowing; -using Dalamud.Utility; -using ImGuiNET; -using System; -using System.Linq; -using System.Numerics; - -namespace Craftimizer.Plugin.Windows; - -public sealed partial class Simulator : Window, IDisposable -{ - private const int ActionColumnSize = 260; - - private static readonly Vector2 ProgressBarSize = new(200, 20); - private static readonly Vector2 DurabilityBarSize = new(100, 20); - private static readonly Vector2 ConditionBarSize = new(20, 20); - private static readonly Vector2 ProgressBarSizeOld = new(200, 20); - public static readonly Vector2 TooltipProgressBarSize = new(100, 5); - - public static readonly Vector4 ProgressColor = new(0.44f, 0.65f, 0.18f, 1f); - public static readonly Vector4 QualityColor = new(0.26f, 0.71f, 0.69f, 1f); - public static readonly Vector4 DurabilityColor = new(0.13f, 0.52f, 0.93f, 1f); - public static readonly Vector4 HQColor = new(0.592f, 0.863f, 0.376f, 1f); - public static readonly Vector4 CPColor = new(0.63f, 0.37f, 0.75f, 1f); - - private static readonly Vector4 BadActionImageTint = new(1f, .5f, .5f, 1f); - private static readonly Vector4 BadActionImageColor = new(1f, .3f, .3f, 1f); - - private static readonly Vector4 BadActionTextColor = new(1f, .2f, .2f, 1f); - - private static readonly (ActionCategory Category, ActionType[] Actions)[] SortedActions; - - static Simulator() - { - SortedActions = Enum.GetValues() - .Where(a => a.Category() != ActionCategory.Combo) - .GroupBy(a => a.Category()) - .Select(g => (g.Key, g.OrderBy(a => a.Level()).ToArray())) - .ToArray(); - } - - public override void Draw() - { - while (SolverActionQueue.TryDequeue(out var poppedAction)) - AppendGeneratedAction(poppedAction); - - ImGui.BeginTable("simulatorWindow", 2, ImGuiTableFlags.BordersInnerV); - ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, ActionColumnSize); - ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableNextColumn(); - DrawActions(); - ImGui.TableNextColumn(); - DrawSimulation(); - ImGui.EndTable(); - } - - private void DrawActions() - { - var hideUnlearnedActions = Config.HideUnlearnedActions; - if (ImGui.Checkbox("Show only learned actions", ref hideUnlearnedActions)) - { - Config.HideUnlearnedActions = hideUnlearnedActions; - Config.Save(); - } - - Sim.SetState(LatestState); - - var actionSize = new Vector2((ActionColumnSize / 5) - ImGui.GetStyle().ItemSpacing.X * (6f / 5)); - ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero); - ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero); - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero); - ImGui.BeginDisabled(!CanModifyActions); - - foreach (var (category, actions) in SortedActions) - { - var i = 0; - ImGuiUtils.BeginGroupPanel(category.GetDisplayName(), ActionColumnSize); - foreach (var action in actions) - { - var baseAction = action.Base(); - - var cannotUse = action.Level() > Input.Stats.Level || (action == ActionType.Manipulation && !Input.Stats.CanUseManipulation); - if (cannotUse && Config.HideUnlearnedActions) - continue; - - var shouldNotUse = !baseAction.CanUse(Sim) || Sim.IsComplete; - - ImGui.BeginDisabled(cannotUse); - - if (ImGui.ImageButton(action.GetIcon(ClassJob).ImGuiHandle, actionSize, Vector2.Zero, Vector2.One, 0, default, shouldNotUse ? BadActionImageTint : Vector4.One)) - AppendAction(action); - - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - ImGui.SetTooltip($"{action.GetName(ClassJob)}\n{baseAction.GetTooltip(Sim, true)}"); - - ImGui.EndDisabled(); - - if (++i % 5 != 0) - ImGui.SameLine(); - } - if (i == 0) - ImGui.Dummy(actionSize); - ImGuiUtils.EndGroupPanel(); - } - - ImGui.EndDisabled(); - ImGui.PopStyleColor(3); - } - - private void DrawSimulation() - { - var drawParams = CalculateSynthDrawParams(); - - DrawSimulationHeader(); - DrawSimulationBars(drawParams); - ImGuiHelpers.ScaledDummy(5); - DrawSimulationEffects(drawParams); - ImGuiHelpers.ScaledDummy(5); - DrawSimulationActions(drawParams); - var bottom = ImGui.GetContentRegionAvail().Y - ImGui.GetStyle().FramePadding.Y * 2; - var buttonHeight = ImGui.GetFrameHeightWithSpacing() * 2 + ImGui.GetFrameHeight(); - ImGuiHelpers.ScaledDummy(bottom - buttonHeight); - DrawSimulationButtons(drawParams); - } - - private void DrawSimulationHeader() - { - var imageSize = new Vector2(ImGui.GetFontSize() * 2.25f); - - ImGui.Image(Service.IconManager.GetIcon(Item.Icon).ImGuiHandle, imageSize); - ImGui.SameLine(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize.Y - ImGui.GetFontSize()) / 2f); - ImGui.TextUnformatted(Item.Name.ToDalamudString().ToString()); - if (Item.IsCollectable) - { - ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize.Y - ImGui.GetFontSize()) / 2f); - ImGui.TextColored(new(0.98f, 0.98f, 0.61f, 1), SeIconChar.Collectible.ToIconString()); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Collectable"); - } - if (IsExpert) - { - ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize.Y - ImGui.GetFontSize()) / 2f); - // Using ItemLevel icon instead of '◈' because the game fonts hate - // me and I can't bother to include a font just for this one icon. - ImGui.TextColored(new(0.93f, 0.59f, 0.45f, 1), SeIconChar.ItemLevel.ToIconString()); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Expert Recipe"); - } - var availWidth = ImGui.GetContentRegionAvail().X; - var text = $"Step {LatestState.StepCount + 1}"; - var textWidth = ImGui.CalcTextSize(text).X; - ImGui.SameLine(availWidth - textWidth); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize.Y - ImGui.GetFontSize()) / 2f); - ImGui.TextUnformatted(text); - ImGui.Separator(); - } - - private void DrawSimulationBars(SynthDrawParams drawParams) - { - var state = LatestState; - - var (leftColumn, rightColumn, leftText, rightText) = (drawParams.LeftColumn, drawParams.RightColumn, drawParams.LeftText, drawParams.RightText); - - ImGui.BeginTable("simSynth", 2); - - ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, leftColumn); - ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, rightColumn); - ImGui.TableNextColumn(); - - DrawSynthProgress("Durability", state.Durability, Input.Recipe.MaxDurability, DurabilityBarSize, DurabilityColor, leftText); - - DrawSynthCircle("Condition", state.Condition.Name(), ConditionBarSize, new Vector4(.35f, .35f, .35f, 0) + state.Condition.GetColor(DateTime.UtcNow.TimeOfDay), DurabilityBarSize, leftText); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip(state.Condition.Description(state.Input.Stats.HasSplendorousBuff)); - - if (Item.IsCollectable) - { - var collectibility = Math.Max(state.Quality / 10, 1); - DrawSynthBar("Collectability", collectibility, Input.Recipe.MaxQuality / 10, $"{collectibility}", DurabilityBarSize, HQColor, leftText); - } - else - DrawSynthBar("HQ %", state.HQPercent, 100, $"{state.HQPercent}%", DurabilityBarSize, HQColor, leftText); - - ImGui.TableNextColumn(); - - DrawSynthProgress("Progress", state.Progress, Input.Recipe.MaxProgress, ProgressBarSize, ProgressColor, rightText); - DrawSynthProgress("Quality", state.Quality, Input.Recipe.MaxQuality, ProgressBarSize, QualityColor, rightText); - DrawSynthProgress("CP", state.CP, Input.Stats.CP, ProgressBarSize, CPColor, rightText); - - ImGui.EndTable(); - } - - private void DrawSimulationEffects(SynthDrawParams drawParams) - { - ImGuiUtils.BeginGroupPanel("Effects", drawParams.Total); - - var effectHeight = ImGui.GetFontSize() * 2f; - Vector2 GetEffectSize(IDalamudTextureWrap icon) => new(icon.Width * effectHeight / icon.Height, effectHeight); - - ImGui.Dummy(new(0, effectHeight)); - ImGui.SameLine(0, 0); - foreach (var effect in Enum.GetValues()) - { - var duration = Sim.GetEffectDuration(effect); - if (duration == 0) - continue; - - var strength = Sim.GetEffectStrength(effect); - var icon = effect.GetIcon(strength); - var iconSize = GetEffectSize(icon); - - ImGui.Image(icon.ImGuiHandle, iconSize); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip(effect.GetTooltip(strength, duration)); - if (duration != 0) - { - ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (effectHeight - ImGui.GetFontSize()) / 2f); - ImGui.Text($"{duration}"); - } - ImGui.SameLine(); - } - ImGui.Dummy(Vector2.Zero); - - ImGuiUtils.EndGroupPanel(); - } - - private void DrawSimulationActions(SynthDrawParams drawParams) - { - ImGuiUtils.BeginGroupPanel("Actions", drawParams.Total); - - ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero); - ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero); - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero); - var actionSize = new Vector2((drawParams.Total / 10) - ImGui.GetStyle().ItemSpacing.X * (11f / 10)); - ImGui.Dummy(new(0, actionSize.Y)); - ImGui.SameLine(0, 0); - for (var i = 0; i < Actions.Count; ++i) - { - var (action, tooltip, response, state) = Actions[i]; - ImGui.PushID(i); - if (ImGui.ImageButton(action.GetIcon(ClassJob).ImGuiHandle, actionSize, Vector2.Zero, Vector2.One, 0, default, response != ActionResponse.UsedAction ? BadActionImageTint : Vector4.One)) - if (CanModifyActions) - RemoveAction(i); - if (CanModifyActions) - { - if (ImGui.BeginDragDropSource()) - { - unsafe { ImGui.SetDragDropPayload("simulationAction", (nint)(&i), sizeof(int)); } - ImGui.ImageButton(Actions[i].Action.GetIcon(ClassJob).ImGuiHandle, actionSize); - ImGui.EndDragDropSource(); - } - if (ImGui.BeginDragDropTarget()) - { - var payload = ImGui.AcceptDragDropPayload("simulationAction"); - bool isValidPayload; - unsafe { isValidPayload = payload.NativePtr != null; } - if (isValidPayload) - { - int draggedIdx; - unsafe { draggedIdx = *(int*)payload.Data; } - var draggedAction = Actions[draggedIdx].Action; - RemoveAction(draggedIdx); - InsertAction(i, draggedAction); - } - ImGui.EndDragDropTarget(); - } - } - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - var responseText = response switch - { - ActionResponse.SimulationComplete => "Recipe Complete", - ActionResponse.ActionNotUnlocked => "Action Not Unlocked", - ActionResponse.NotEnoughCP => "Not Enough CP", - ActionResponse.NoDurability => "No More Durability", - ActionResponse.CannotUseAction => "Cannot Use", - _ => string.Empty, - }; - if (response != ActionResponse.UsedAction) - ImGui.TextColored(BadActionTextColor, responseText); - ImGui.Text($"{action.GetName(ClassJob)}\n{tooltip}"); - DrawAllProgressTooltips(state); - if (CanModifyActions) - ImGui.Text("Click to Remove\nDrag to Move"); - ImGui.EndTooltip(); - } - ImGui.PopID(); - if (i % 10 != 9) - ImGui.SameLine(); - } - ImGui.PopStyleColor(3); - - ImGuiUtils.EndGroupPanel(); - } - - private void DrawSimulationButtons(SynthDrawParams drawParams) - { - var totalWidth = drawParams.Total; - var halfWidth = (totalWidth - ImGui.GetStyle().ItemSpacing.X) / 2f; - var quarterWidth = (halfWidth - ImGui.GetStyle().ItemSpacing.X) / 2f; - var halfButtonSize = new Vector2(halfWidth, ImGui.GetFrameHeight()); - var quarterButtonSize = new Vector2(quarterWidth, ImGui.GetFrameHeight()); - - var conditionRandomnessText = "Condition Randomness"; - var conditionRandomness = Config.ConditionRandomness; - ImGui.BeginDisabled(!CanModifyActions); - if (ImGui.Checkbox(conditionRandomnessText, ref conditionRandomness)) - { - Config.ConditionRandomness = conditionRandomness; - Config.Save(); - ResetSimulator(); - } - ImGui.EndDisabled(); - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - ImGui.SetTooltip("Allows the condition to fluctuate randomly like a real craft.\nTurns off when generating a macro."); - - var labelSize = ImGui.CalcTextSize(conditionRandomnessText); - var checkboxWidth = ImGui.GetFrameHeight() + (labelSize.X > 0 ? ImGui.GetStyle().ItemInnerSpacing.X + labelSize.X : 0); - ImGui.PushFont(UiBuilder.IconFont); - var cogWidth = ImGui.CalcTextSize(FontAwesomeIcon.Cog.ToIconString()).X; - ImGui.PopFont(); - ImGui.SameLine(0, totalWidth - ImGui.GetStyle().ItemSpacing.X - checkboxWidth - cogWidth); - if (ImGuiComponents.IconButton("simSettingsButton", FontAwesomeIcon.Cog)) - Service.Plugin.OpenSettingsTab(Settings.TabSimulator); - - // - - var macroName = MacroName; - ImGui.SetNextItemWidth(halfWidth); - if (ImGui.InputTextWithHint("", "Macro Name", ref macroName, 64)) - MacroName = macroName; - - ImGui.SameLine(); - - DrawSimulationGenerateButton(halfButtonSize); - - // - - ImGui.BeginDisabled(!CanModifyActions); - if (Macro != null) - { - if (ImGui.Button("Save", quarterButtonSize)) - { - Macro.Name = MacroName; - Macro.Actions = Actions.Select(a => a.Action).ToList(); - Config.Save(); - } - ImGui.SameLine(); - } - if (ImGui.Button("Save New", Macro == null ? halfButtonSize : quarterButtonSize)) - { - Macro = new() { Name = MacroName, Actions = Actions.Select(a => a.Action).ToList() }; - Config.Macros.Add(Macro); - Config.Save(); - } - ImGui.SameLine(); - if (ImGui.Button("Reset", halfButtonSize)) - ClearAllActions(); - ImGui.EndDisabled(); - } - - private void DrawSimulationGenerateButton(Vector2 buttonSize) - { - var state = GenerateSolverState(); - string buttonText; - string tooltipText; - bool isEnabled; - var taskCompleted = SolverTask?.IsCompleted ?? true; - var taskCancelled = SolverTaskToken?.IsCancellationRequested ?? false; - if (!taskCompleted) - { - if (taskCancelled) - { - buttonText = "Cancelling..."; - tooltipText = "Cancelling macro generation. This shouldn't take long."; - isEnabled = false; - } - else - { - buttonText = "Cancel"; - tooltipText = "Cancel macro generation"; - isEnabled = true; - } - } - else - { - if (SolverActionsChanged) - { - buttonText = "Generate"; - tooltipText = "Generate a set of actions to finish the macro."; - isEnabled = state.HasValue; - if (!isEnabled) - tooltipText += "\nMake sure your craft so far is valid (without random condition changes)"; - } - else - { - buttonText = "Regenerate"; - tooltipText = "Retry and regenerate a new set of actions to finish the macro."; - isEnabled = true; - } - } - ImGui.BeginDisabled(!isEnabled); - if (ImGui.Button(buttonText, buttonSize)) - { - if (!taskCompleted) - { - if (!taskCancelled) - SolverTaskToken?.Cancel(); - } - else - { - if (SolverActionsChanged) - { - if (state.HasValue) - SolveMacro(state.Value); - } - else - { - Actions.RemoveRange(SolverInitialActionCount, Actions.Count - SolverInitialActionCount); - SolveMacro(GenerateSolverState()!.Value); - } - } - } - ImGui.EndDisabled(); - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - ImGui.SetTooltip(tooltipText); - } -} diff --git a/Craftimizer/Windows/SimulatorDrawerUtils.cs b/Craftimizer/Windows/SimulatorDrawerUtils.cs deleted file mode 100644 index 7711253..0000000 --- a/Craftimizer/Windows/SimulatorDrawerUtils.cs +++ /dev/null @@ -1,146 +0,0 @@ -using Craftimizer.Simulator; -using Dalamud.Interface.Windowing; -using ImGuiNET; -using System; -using System.Numerics; - -namespace Craftimizer.Plugin.Windows; - -public sealed partial class Simulator : Window, IDisposable -{ - private readonly record struct SynthDrawParams - { - public float LeftColumn { get; init; } - public float RightColumn { get; init; } - public float LeftText { get; init; } - public float RightText { get; init; } - public float Total { get; init; } - } - - private SynthDrawParams CalculateSynthDrawParams() - { - var sidePadding = ImGui.GetFrameHeight() / 2; - var separatorTextWidth = ImGui.CalcTextSize(" / ").X; - var itemSpacing = ImGui.GetStyle().ItemSpacing.X; - - var leftDigits = (int)MathF.Floor(MathF.Log10(Input.Recipe.MaxDurability) + 1); - var leftTextWidth = ImGui.CalcTextSize(new string('0', leftDigits)).X; - var leftWidth = DurabilityBarSize.X + sidePadding + itemSpacing * 2 + separatorTextWidth + leftTextWidth * 2; - - - var rightDigits = (int)MathF.Floor(MathF.Log10(Math.Max(Math.Max(Input.Recipe.MaxProgress, Input.Recipe.MaxQuality), Input.Stats.CP)) + 1); - var rightTextWidth = ImGui.CalcTextSize(new string('0', rightDigits)).X; - var rightWidth = ProgressBarSize.X + sidePadding + itemSpacing * 2 + separatorTextWidth + rightTextWidth * 2; - - return new() - { - LeftColumn = leftWidth, - LeftText = leftTextWidth, - RightColumn = rightWidth, - RightText = rightTextWidth, - Total = leftWidth + rightWidth + itemSpacing - }; - } - - // Generic Progress Bar - private static void DrawSynthProgress(string name, int current, int max, Vector2 size, Vector4 color, float textWidth) - { - ImGuiUtils.BeginGroupPanel(name); - - DrawProgressBar(current, max, size, color); - - var w = ImGui.GetStyle().ItemSpacing.X; - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, ImGui.GetStyle().ItemSpacing.Y)); - - ImGui.SameLine(0, textWidth - ImGui.CalcTextSize($"{current}").X + w); - var adjustedHeight = ImGui.GetCursorPosY() - ((ImGui.GetFrameHeight() - ImGui.GetFontSize()) / 2f); - ImGui.SetCursorPosY(adjustedHeight); - ImGui.TextUnformatted($"{current}"); - - ImGui.SameLine(); - ImGui.SetCursorPosY(adjustedHeight); - ImGui.TextUnformatted(" / "); - - ImGui.SameLine(0, textWidth - ImGui.CalcTextSize($"{max}").X); - ImGui.SetCursorPosY(adjustedHeight); - ImGui.TextUnformatted($"{max}"); - - ImGui.PopStyleVar(); - - ImGuiUtils.EndGroupPanel(); - } - - // HQ% / Collectability Bar (has no fractional bar to indicate max) - private static void DrawSynthBar(string name, int current, int max, string text, Vector2 size, Vector4 color, float textWidth) - { - ImGuiUtils.BeginGroupPanel(name); - - DrawProgressBar(current, max, size, color); - - var w = ImGui.GetStyle().ItemSpacing.X; - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, ImGui.GetStyle().ItemSpacing.Y)); - - var totalWidth = textWidth * 2 + ImGui.CalcTextSize(" / ").X; - - ImGui.SameLine(0, totalWidth - ImGui.CalcTextSize(text).X + w); - var adjustedHeight = ImGui.GetCursorPosY() - ((ImGui.GetFrameHeight() - ImGui.GetFontSize()) / 2f); - ImGui.SetCursorPosY(adjustedHeight); - ImGui.TextUnformatted(text); - - ImGui.PopStyleVar(); - - ImGuiUtils.EndGroupPanel(); - } - - // Condition "Bar" Circle (always 100%, is a circle) - private static void DrawSynthCircle(string name, string text, Vector2 size, Vector4 color, Vector2 otherProgressSize, float textWidth) - { - ImGuiUtils.BeginGroupPanel(name); - - var w = ImGui.GetStyle().ItemSpacing.X; - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, ImGui.GetStyle().ItemSpacing.Y)); - - var contentWidth = size.X + w + ImGui.CalcTextSize(text).X; - var totalWidth = otherProgressSize.X + w + textWidth * 2 + ImGui.CalcTextSize(" / ").X; - - ImGui.Dummy(default); - ImGui.SameLine(0, (totalWidth - contentWidth) / 2); - ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, Math.Max(size.X, size.Y)); - DrawProgressBar(1, 1, size, color); - ImGui.PopStyleVar(); - ImGui.SameLine(0, w); - var adjustedHeight = ImGui.GetCursorPosY() - ((ImGui.GetFrameHeight() - ImGui.GetFontSize()) / 2f); - ImGui.SetCursorPosY(adjustedHeight); - ImGui.TextUnformatted(text); - - ImGui.PopStyleVar(); - - ImGuiUtils.EndGroupPanel(); - } - - public static void DrawAllProgressBars(SimulationState state, Vector2 progressBarSize) - { - DrawProgressBar(state.Progress, state.Input.Recipe.MaxProgress, progressBarSize, ProgressColor); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip($"Progress: {state.Progress} / {state.Input.Recipe.MaxProgress}"); - DrawProgressBar(state.Quality, state.Input.Recipe.MaxQuality, progressBarSize, QualityColor); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip($"Quality: {state.Quality} / {state.Input.Recipe.MaxQuality}"); - DrawProgressBar(state.Durability, state.Input.Recipe.MaxDurability, progressBarSize, DurabilityColor); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip($"Durability: {state.Durability} / {state.Input.Recipe.MaxDurability}"); - DrawProgressBar(state.CP, state.Input.Stats.CP, progressBarSize, CPColor); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip($"CP: {state.CP} / {state.Input.Stats.CP}"); - } - - public static void DrawAllProgressTooltips(SimulationState state) => - DrawAllProgressBars(state, TooltipProgressBarSize); - - private static void DrawProgressBar(int progress, int maxProgress, Vector2 size, Vector4 color, string overlay = "") - { - ImGui.PushStyleColor(ImGuiCol.PlotHistogram, color); - ImGui.ProgressBar(Math.Clamp((float)progress / maxProgress, 0f, 1f), size, overlay); - ImGui.PopStyleColor(); - } -} diff --git a/Craftimizer/Windows/SimulatorSolver.cs b/Craftimizer/Windows/SimulatorSolver.cs deleted file mode 100644 index cd20dca..0000000 --- a/Craftimizer/Windows/SimulatorSolver.cs +++ /dev/null @@ -1,100 +0,0 @@ -using Craftimizer.Simulator; -using Craftimizer.Simulator.Actions; -using Craftimizer.Utils; -using Dalamud.Interface.Windowing; -using System; -using System.Collections.Concurrent; -using System.Threading; - -namespace Craftimizer.Plugin.Windows; - -public sealed partial class Simulator : Window, IDisposable -{ - private Solver.Solver? SolverTask { get; set; } - private CancellationTokenSource? SolverTaskToken { get; set; } - private ConcurrentQueue SolverActionQueue { get; } = new(); - private int SolverInitialActionCount { get; set; } - private bool SolverActionsChanged { get; set; } = true; - - private bool CanModifyActions => SolverTask?.IsCompleted ?? true; - - private void OnActionsChanged() - { - SolverActionsChanged = true; - } - - private SimulationState? GenerateSolverState() - { - if (Sim is SimulatorNoRandom) - { - if (!Actions.Exists(a => a.Response != ActionResponse.UsedAction)) - return LatestState; - else - return null; - } - - var ret = new SimulationState(Input); - if (Actions.Count != 0) - { - var tmpSim = new SimulatorNoRandom(ret); - foreach (var action in Actions) - { - (var resp, ret) = tmpSim.Execute(ret, action.Action); - if (resp != ActionResponse.UsedAction) - return null; - } - } - return ret; - } - - private void StopSolveMacro() - { - if (SolverTask == null || SolverTaskToken == null) - return; - - if (!SolverTask.IsCompleted) - SolverTaskToken.Cancel(); - else - { - SolverTaskToken.Dispose(); - SolverTask.Dispose(); - - SolverTask = null; - SolverTaskToken = null; - } - } - - private void SolveMacro(SimulationState solverState) - { - StopSolveMacro(); - - // Prevents the quality bar from being unfair between solves - if (Config.ConditionRandomness) - { - Config.ConditionRandomness = false; - Config.Save(); - - ResetSimulator(); - } - - SolverActionsChanged = false; - - SolverActionQueue.Clear(); - - SolverInitialActionCount = Actions.Count; - SolverTaskToken = new(); - SolverTask = new(Config.SimulatorSolverConfig, solverState) { Token = SolverTaskToken.Token }; - SolverTask.OnLog += s => Log.Debug(s); - SolverTask.OnNewAction += SolverActionQueue.Enqueue; - SolverTask.Start(); - } - - public void Dispose() - { - StopSolveMacro(); - SolverTaskToken?.Cancel(); - SolverTask?.TryWait(); - SolverTask?.Dispose(); - SolverTaskToken?.Dispose(); - } -} From 234eb3a7abc3ce80a36d6688e8b5d49ed01256c2 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 17 Oct 2023 03:36:31 -0700 Subject: [PATCH 17/58] 1.9.0.0 (Testing) Release --- Craftimizer/Configuration.cs | 90 +- Craftimizer/Craftimizer.csproj | 2 +- Craftimizer/ImGuiUtils.cs | 411 +++++-- Craftimizer/Plugin.cs | 81 +- Craftimizer/Service.cs | 1 - Craftimizer/SimulatorUtils.cs | 10 +- Craftimizer/Utils/Chat.cs | 29 +- Craftimizer/Utils/Colors.cs | 12 + Craftimizer/Utils/FoodStatus.cs | 121 ++ Craftimizer/Utils/FuzzyMatcher.cs | 224 ++++ Craftimizer/Utils/Gearsets.cs | 26 +- Craftimizer/Utils/IconManager.cs | 17 +- Craftimizer/Utils/MacroCopy.cs | 152 +++ Craftimizer/Utils/RecipeData.cs | 82 ++ Craftimizer/Utils/RecipeNote.cs | 203 ---- Craftimizer/Utils/SqText.cs | 18 +- Craftimizer/Windows/Craft.cs | 226 ---- Craftimizer/Windows/CraftAddon.cs | 157 --- Craftimizer/Windows/CraftSolver.cs | 99 -- Craftimizer/Windows/CraftState.cs | 81 -- Craftimizer/Windows/MacroClipboard.cs | 79 ++ Craftimizer/Windows/MacroEditor.cs | 1372 +++++++++++++++++++++++ Craftimizer/Windows/MacroList.cs | 361 ++++++ Craftimizer/Windows/RecipeNote.cs | 232 ++-- Craftimizer/Windows/Settings.cs | 758 ++++++++----- Simulator/ActionCategory.cs | 22 + Simulator/Actions/BaseAction.cs | 3 +- Simulator/Actions/BaseBuffAction.cs | 5 +- Simulator/Actions/CarefulObservation.cs | 3 + Simulator/Actions/HeartAndSoul.cs | 3 + Simulator/Actions/Manipulation.cs | 12 +- Simulator/Actions/TrainedEye.cs | 3 + Simulator/Effects.cs | 5 + Simulator/SimulationState.cs | 2 + Simulator/Simulator.cs | 2 + Solver/Solver.cs | 37 +- Solver/SolverSolution.cs | 41 +- 37 files changed, 3680 insertions(+), 1302 deletions(-) create mode 100644 Craftimizer/Utils/Colors.cs create mode 100644 Craftimizer/Utils/FoodStatus.cs create mode 100644 Craftimizer/Utils/FuzzyMatcher.cs create mode 100644 Craftimizer/Utils/MacroCopy.cs create mode 100644 Craftimizer/Utils/RecipeData.cs delete mode 100644 Craftimizer/Utils/RecipeNote.cs delete mode 100644 Craftimizer/Windows/Craft.cs delete mode 100644 Craftimizer/Windows/CraftAddon.cs delete mode 100644 Craftimizer/Windows/CraftSolver.cs delete mode 100644 Craftimizer/Windows/CraftState.cs create mode 100644 Craftimizer/Windows/MacroClipboard.cs create mode 100644 Craftimizer/Windows/MacroEditor.cs create mode 100644 Craftimizer/Windows/MacroList.cs diff --git a/Craftimizer/Configuration.cs b/Craftimizer/Configuration.cs index c37a89e..f03b0bc 100644 --- a/Craftimizer/Configuration.cs +++ b/Craftimizer/Configuration.cs @@ -1,7 +1,7 @@ -using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; using Craftimizer.Solver; using Dalamud.Configuration; +using Newtonsoft.Json; using System; using System.Collections.Generic; @@ -10,8 +10,64 @@ namespace Craftimizer.Plugin; [Serializable] public class Macro { + public static event Action? OnMacroChanged; + public string Name { get; set; } = string.Empty; - public List Actions { get; set; } = new(); + [JsonProperty(PropertyName = "Actions")] + private List actions { get; set; } = new(); + [JsonIgnore] + public IReadOnlyList Actions + { + get => actions; + set => ActionEnumerable = value; + } + [JsonIgnore] + public IEnumerable ActionEnumerable + { + set + { + actions = new(value); + OnMacroChanged?.Invoke(this); + } + } +} + +[Serializable] +public class MacroCopyConfiguration +{ + public enum CopyType + { + OpenWindow, // useful for big macros + CopyToMacro, // (add option for down or right) (max macro count; open copy-paste window if too much) + CopyToClipboard, + } + + public CopyType Type { get; set; } = CopyType.OpenWindow; + + // CopyToMacro + public bool CopyDown { get; set; } + public bool SharedMacro { get; set; } + public int StartMacroIdx { get; set; } = 1; + public int MaxMacroCount { get; set; } = 5; + + // Add /nextmacro [down] + public bool UseNextMacro { get; set; } + + // Add /mlock + public bool UseMacroLock { get; set; } + + public bool AddNotification { get; set; } = true; + + // Requires AddNotification + public bool AddNotificationSound { get; set; } = true; + public int IntermediateNotificationSound { get; set; } = 10; + public int EndNotificationSound { get; set; } = 6; + + // For SND + public bool RemoveWaitTimes { get; set; } + + // For SND; Cannot use CopyToMacro + public bool CombineMacro { get; set; } } [Serializable] @@ -19,9 +75,12 @@ public class Configuration : IPluginConfiguration { public int Version { get; set; } = 1; - public bool OverrideUncraftability { get; set; } = true; - public bool HideUnlearnedActions { get; set; } = true; - public List Macros { get; set; } = new(); + public static event Action? OnMacroListChanged; + + [JsonProperty(PropertyName = "Macros")] + private List macros { get; set; } = new(); + [JsonIgnore] + public IReadOnlyList Macros => macros; public bool ConditionRandomness { get; set; } = true; public SolverConfig SimulatorSolverConfig { get; set; } = SolverConfig.SimulatorDefault; public SolverConfig SynthHelperSolverConfig { get; set; } = SolverConfig.SynthHelperDefault; @@ -29,10 +88,23 @@ public class Configuration : IPluginConfiguration public bool ShowOptimalMacroStat { get; set; } = true; public int SynthHelperStepCount { get; set; } = 5; - public Simulator.Simulator CreateSimulator(SimulationState state) => - ConditionRandomness ? - new Simulator.Simulator(state) : - new SimulatorNoRandom(state); + public MacroCopyConfiguration MacroCopy { get; set; } = new(); + + public void AddMacro(Macro macro) + { + macros.Add(macro); + Save(); + OnMacroListChanged?.Invoke(); + } + + public void RemoveMacro(Macro macro) + { + if (macros.Remove(macro)) + { + Save(); + OnMacroListChanged?.Invoke(); + } + } public void Save() => Service.PluginInterface.SavePluginConfig(this); diff --git a/Craftimizer/Craftimizer.csproj b/Craftimizer/Craftimizer.csproj index d4f8fb8..3397a43 100644 --- a/Craftimizer/Craftimizer.csproj +++ b/Craftimizer/Craftimizer.csproj @@ -2,7 +2,7 @@ Asriel Camora - 1.2.0.0 + 1.9.0.0 https://github.com/WorkingRobot/craftimizer.git diff --git a/Craftimizer/ImGuiUtils.cs b/Craftimizer/ImGuiUtils.cs index 24132e2..88afbb8 100644 --- a/Craftimizer/ImGuiUtils.cs +++ b/Craftimizer/ImGuiUtils.cs @@ -1,128 +1,147 @@ +using Craftimizer.Utils; using Dalamud.Interface; +using Dalamud.Interface.Utility.Raii; using ImGuiNET; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; +using System.Linq; using System.Numerics; +using System.Threading; +using System.Threading.Tasks; namespace Craftimizer.Plugin; internal static class ImGuiUtils { - private static readonly Stack<(Vector2 Min, Vector2 Max)> GroupPanelLabelStack = new(); + private static readonly Stack<(Vector2 Min, Vector2 Max, float TopPadding)> GroupPanelLabelStack = new(); // Adapted from https://github.com/ocornut/imgui/issues/1496#issuecomment-655048353 - public static void BeginGroupPanel(float width = -1, bool addPadding = true) + // width = -1 -> size to parent + // width = 0 -> size to content + // returns available width (better since it accounts for the right side padding) + // ^ only useful if width = -1 + public static float BeginGroupPanel(string name, float width) { + // container group ImGui.BeginGroup(); var itemSpacing = ImGui.GetStyle().ItemSpacing; - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - var frameHeight = ImGui.GetFrameHeight(); + width = width < 0 ? ImGui.GetContentRegionAvail().X - (2 * itemSpacing.X) : width; + var fullWidth = width > 0 ? width + (2 * itemSpacing.X) : 0; + { + using var noPadding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero); + using var noSpacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + + // inner group + ImGui.BeginGroup(); + ImGui.Dummy(new Vector2(fullWidth, 0)); + ImGui.Dummy(new Vector2(itemSpacing.X, 0)); // shifts next group by is.x + ImGui.SameLine(0, 0); + + // label group + ImGui.BeginGroup(); + ImGui.Dummy(new Vector2(frameHeight / 2, 0)); // shifts text by fh/2 + ImGui.SameLine(0, 0); + var textFrameHeight = ImGui.GetFrameHeight(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(name); + GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), textFrameHeight / 2f)); // push rect to stack + ImGui.SameLine(0, 0); + ImGui.Dummy(new Vector2(0f, textFrameHeight + itemSpacing.Y)); // shifts content by fh + is.y + + // content group + ImGui.BeginGroup(); + } - ImGui.BeginGroup(); - ImGui.Dummy(new Vector2(width < 0 ? ImGui.GetContentRegionAvail().X : width, 0)); - ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0)); - ImGui.SameLine(0, 0); - - ImGui.BeginGroup(); - ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0)); - GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax())); - ImGui.SameLine(0, 0); - ImGui.Dummy(new Vector2(0f, frameHeight * (addPadding ? 1 : .5f) + itemSpacing.Y)); - - ImGui.BeginGroup(); - - ImGui.PopStyleVar(2); - - ImGui.PushItemWidth(MathF.Max(0, ImGui.CalcItemWidth() - frameHeight)); + return width; } - public static void BeginGroupPanel(string name, float width = -1, bool addPadding = true) + public static void EndGroupPanel() { - ImGui.BeginGroup(); - var itemSpacing = ImGui.GetStyle().ItemSpacing; - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - - var frameHeight = ImGui.GetFrameHeight(); - - ImGui.BeginGroup(); - ImGui.Dummy(new Vector2(width < 0 ? ImGui.GetContentRegionAvail().X : width, 0)); - ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0)); - ImGui.SameLine(0, 0); + { + using var noPadding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero); + using var noSpacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - ImGui.BeginGroup(); - ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0)); - ImGui.SameLine(0, 0); - ImGui.TextUnformatted(name); - GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax())); - ImGui.SameLine(0, 0); - ImGui.Dummy(new Vector2(0f, frameHeight * (addPadding ? 1 : .5f) + itemSpacing.Y)); + // content group + ImGui.EndGroup(); - ImGui.BeginGroup(); + // label group + ImGui.EndGroup(); - ImGui.PopStyleVar(2); + ImGui.SameLine(0, 0); + // shifts full size by is (for rect placement) + ImGui.Dummy(new(itemSpacing.X, 0)); + ImGui.Dummy(new(0, itemSpacing.Y * 2)); // * 2 for some reason (otherwise the bottom is too skinny) - ImGui.PushItemWidth(MathF.Max(0, ImGui.CalcItemWidth() - frameHeight)); - } + // inner group + ImGui.EndGroup(); - public static void EndGroupPanel() - { - ImGui.PopItemWidth(); + var labelRect = GroupPanelLabelStack.Pop(); + var innerMin = ImGui.GetItemRectMin() + new Vector2(0, labelRect.TopPadding); + var innerMax = ImGui.GetItemRectMax(); - var itemSpacing = ImGui.GetStyle().ItemSpacing; - - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + (Vector2 Min, Vector2 Max) frameRect = (innerMin, innerMax); + // add itemspacing padding on the label's sides + labelRect.Min.X -= itemSpacing.X / 2; + labelRect.Max.X += itemSpacing.X / 2; + for (var i = 0; i < 4; ++i) + { + var (minClip, maxClip) = i switch + { + 0 => (new Vector2(float.NegativeInfinity), new Vector2(labelRect.Min.X, float.PositiveInfinity)), + 1 => (new Vector2(labelRect.Max.X, float.NegativeInfinity), new Vector2(float.PositiveInfinity)), + 2 => (new Vector2(labelRect.Min.X, float.NegativeInfinity), new Vector2(labelRect.Max.X, labelRect.Min.Y)), + 3 => (new Vector2(labelRect.Min.X, labelRect.Max.Y), new Vector2(labelRect.Max.X, float.PositiveInfinity)), + _ => (Vector2.Zero, Vector2.Zero) + }; + + ImGui.PushClipRect(minClip, maxClip, true); + ImGui.GetWindowDrawList().AddRect( + frameRect.Min, frameRect.Max, + ImGui.GetColorU32(ImGuiCol.Border), + itemSpacing.X); + ImGui.PopClipRect(); + } - var frameHeight = ImGui.GetFrameHeight(); + ImGui.Dummy(Vector2.Zero); + } ImGui.EndGroup(); + } - ImGui.EndGroup(); + private struct EndUnconditionally : ImRaii.IEndObject, IDisposable + { + private Action EndAction { get; } - ImGui.SameLine(0, 0); - ImGui.Dummy(new Vector2(frameHeight * 0.5f, 0)); - ImGui.Dummy(new Vector2(0f, frameHeight * 0.5f - itemSpacing.Y)); + public bool Success { get; } - ImGui.EndGroup(); + public bool Disposed { get; private set; } - var itemMin = ImGui.GetItemRectMin(); - var itemMax = ImGui.GetItemRectMax(); - var labelRect = GroupPanelLabelStack.Pop(); + public EndUnconditionally(Action endAction, bool success) + { + EndAction = endAction; + Success = success; + Disposed = false; + } - var halfFrame = new Vector2(frameHeight * 0.25f, frameHeight) * 0.5f; - (Vector2 Min, Vector2 Max) frameRect = (itemMin + halfFrame, itemMax - new Vector2(halfFrame.X, 0)); - labelRect.Min.X -= itemSpacing.X; - labelRect.Max.X += itemSpacing.X; - for (var i = 0; i < 4; ++i) + public void Dispose() { - var (minClip, maxClip) = i switch + if (!Disposed) { - 0 => (new Vector2(float.NegativeInfinity), new Vector2(labelRect.Min.X, float.PositiveInfinity)), - 1 => (new Vector2(labelRect.Max.X, float.NegativeInfinity), new Vector2(float.PositiveInfinity)), - 2 => (new Vector2(labelRect.Min.X, float.NegativeInfinity), new Vector2(labelRect.Max.X, labelRect.Min.Y)), - 3 => (new Vector2(labelRect.Min.X, labelRect.Max.Y), new Vector2(labelRect.Max.X, float.PositiveInfinity)), - _ => (Vector2.Zero, Vector2.Zero) - }; - - ImGui.PushClipRect(minClip, maxClip, true); - ImGui.GetWindowDrawList().AddRect( - frameRect.Min, frameRect.Max, - ImGui.GetColorU32(ImGuiCol.Border), - halfFrame.X); - ImGui.PopClipRect(); + EndAction(); + Disposed = true; + } } + } - ImGui.PopStyleVar(2); - - ImGui.Dummy(Vector2.Zero); - - ImGui.EndGroup(); + public static ImRaii.IEndObject GroupPanel(string name, float width, out float internalWidth) + { + internalWidth = BeginGroupPanel(name, width); + return new EndUnconditionally(EndGroupPanel, true); } private static Vector2 UnitCircle(float theta) @@ -156,7 +175,7 @@ public static void Arc(float startAngle, float endAngle, float radius, float rat var offset = ImGui.GetCursorScreenPos() + new Vector2(radius); - var segments = ImGui.GetWindowDrawList()._CalcCircleAutoSegmentCount(radius * 2); + var segments = ImGui.GetWindowDrawList()._CalcCircleAutoSegmentCount(radius); var incrementAngle = MathF.Tau / segments; var isFullCircle = (endAngle - startAngle) % MathF.Tau == 0; @@ -215,6 +234,216 @@ public static void ArcProgress(float value, float radiusInner, float radiusOuter Arc(MathF.PI / 2, MathF.PI / 2 - MathF.Tau * Math.Clamp(value, 0, 1), radiusInner, radiusOuter, backgroundColor, filledColor); } + private sealed class SearchableComboData where T : class + { + public readonly ImmutableArray items; + public List filteredItems; + public T selectedItem; + public string input; + public bool wasTextActive; + public bool wasPopupActive; + public CancellationTokenSource? cts; + public Task? task; + + private readonly Func getString; + + public SearchableComboData(IEnumerable items, T selectedItem, Func getString) + { + this.items = items.ToImmutableArray(); + filteredItems = new() { selectedItem }; + this.selectedItem = selectedItem; + this.getString = getString; + input = GetString(selectedItem); + } + + public void SetItem(T selectedItem) + { + if (this.selectedItem != selectedItem) + { + input = GetString(selectedItem); + this.selectedItem = selectedItem; + } + } + + public string GetString(T item) => getString(item); + + public void Filter() + { + cts?.Cancel(); + var inp = input; + cts = new(); + var token = cts.Token; + task = Task.Run(() => FilterTask(inp, token), token) + .ContinueWith(t => + { + if (cts.IsCancellationRequested) + return; + + try + { + t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException); + } + catch (Exception e) + { + Log.Error(e, "Filtering recipes failed"); + } + }, TaskContinuationOptions.OnlyOnFaulted); + } + + private void FilterTask(string input, CancellationToken token) + { + if (string.IsNullOrWhiteSpace(input)) + { + filteredItems = items.ToList(); + return; + } + var matcher = new FuzzyMatcher(input.ToLowerInvariant(), MatchMode.FuzzyParts); + var query = items.AsParallel().Select(i => (Item: i, Score: matcher.Matches(getString(i).ToLowerInvariant()))) + .Where(t => t.Score > 0) + .OrderByDescending(t => t.Score) + .Select(t => t.Item); + token.ThrowIfCancellationRequested(); + filteredItems = query.ToList(); + } + } + private static readonly Dictionary ComboData = new(); + + private static SearchableComboData GetComboData(uint comboKey, IEnumerable items, T selectedItem, Func getString) where T : class => + (SearchableComboData)( + ComboData.TryGetValue(comboKey, out var data) + ? data + : ComboData[comboKey] = new SearchableComboData(items, selectedItem, getString)); + + // https://github.com/ocornut/imgui/issues/718#issuecomment-1563162222 + public static bool SearchableCombo(string id, ref T selectedItem, IEnumerable items, ImFontPtr selectableFont, float width, Func getString, Func getId, Action draw) where T : class + { + var comboKey = ImGui.GetID(id); + var data = GetComboData(comboKey, items, selectedItem, getString); + data.SetItem(selectedItem); + + using var pushId = ImRaii.PushId(id); + + width = width == 0 ? ImGui.GetContentRegionAvail().X : width; + var availableSpace = Math.Min(ImGui.GetContentRegionAvail().X, width); + ImGui.SetNextItemWidth(availableSpace); + var isInputTextEnterPressed = ImGui.InputText("##input", ref data.input, 256, ImGuiInputTextFlags.EnterReturnsTrue); + var min = ImGui.GetItemRectMin(); + var size = ImGui.GetItemRectSize(); + size.X = Math.Min(size.X, availableSpace); + + var isInputTextActivated = ImGui.IsItemActivated(); + + if (isInputTextActivated) + { + ImGui.SetNextWindowPos(min - ImGui.GetStyle().WindowPadding); + ImGui.OpenPopup("##popup"); + data.wasTextActive = false; + } + + using (var popup = ImRaii.Popup("##popup", ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoSavedSettings)) + { + if (popup) + { + data.wasPopupActive = true; + + if (isInputTextActivated) + { + ImGui.SetKeyboardFocusHere(0); + data.Filter(); + } + ImGui.SetNextItemWidth(size.X); + if (ImGui.InputText("##input_popup", ref data.input, 256)) + data.Filter(); + var isActive = ImGui.IsItemActive(); + if (!isActive && data.wasTextActive && ImGui.IsKeyPressed(ImGuiKey.Enter)) + isInputTextEnterPressed = true; + data.wasTextActive = isActive; + + using (var scrollingRegion = ImRaii.Child("scrollingRegion", new Vector2(size.X, size.Y * 10), false, ImGuiWindowFlags.HorizontalScrollbar)) + { + T? _selectedItem = default; + var height = ImGui.GetTextLineHeight(); + var r = ListClip(data.filteredItems, height, t => + { + var name = getString(t); + using (var selectFont = ImRaii.PushFont(selectableFont)) + { + if (ImGui.Selectable($"##{getId(t)}")) + { + _selectedItem = t; + return true; + } + } + ImGui.SameLine(0, ImGui.GetStyle().ItemSpacing.X / 2f); + draw(t); + return false; + }); + if (r) + { + selectedItem = _selectedItem!; + data.SetItem(selectedItem); + ImGui.CloseCurrentPopup(); + return true; + } + } + + if (isInputTextEnterPressed || ImGui.IsKeyPressed(ImGuiKey.Escape)) + { + if (isInputTextEnterPressed && data.filteredItems.Count > 0) + { + selectedItem = data.filteredItems[0]; + data.SetItem(selectedItem); + } + ImGui.CloseCurrentPopup(); + return true; + } + } + else + { + if (data.wasPopupActive) + { + data.wasPopupActive = false; + data.input = getString(selectedItem); + } + } + } + + return false; + } + + private static bool ListClip(IReadOnlyList data, float lineHeight, Predicate func) + { + ImGuiListClipperPtr imGuiListClipperPtr; + unsafe + { + imGuiListClipperPtr = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + } + try + { + imGuiListClipperPtr.Begin(data.Count, lineHeight); + while (imGuiListClipperPtr.Step()) + { + for (var i = imGuiListClipperPtr.DisplayStart; i <= imGuiListClipperPtr.DisplayEnd; i++) + { + if (i >= data.Count) + return false; + + if (i >= 0) + { + if (func(data[i])) + return true; + } + } + } + return false; + } + finally + { + imGuiListClipperPtr.End(); + imGuiListClipperPtr.Destroy(); + } + } + public static bool IconButtonSized(FontAwesomeIcon icon, Vector2 size) { ImGui.PushFont(UiBuilder.IconFont); @@ -254,6 +483,14 @@ public static void AlignCentered(float width, float availWidth = default) ImGui.SetCursorPosX(ImGui.GetCursorPos().X + (availWidth - width) / 2); } + public static void AlignRight(float width, float availWidth = default) + { + if (availWidth == default) + availWidth = ImGui.GetContentRegionAvail().X; + if (availWidth > width) + ImGui.SetCursorPosX(ImGui.GetCursorPos().X + availWidth - width); + } + public static void AlignMiddle(Vector2 size, Vector2 availSize = default) { if (availSize == default) @@ -271,9 +508,9 @@ public static void TextCentered(string text, float availWidth = default) ImGui.TextUnformatted(text); } - public static void TextMiddle(string text, Vector2 availSize = default) + public static void TextRight(string text, float availWidth = default) { - AlignMiddle(ImGui.CalcTextSize(text), availSize); + AlignRight(ImGui.CalcTextSize(text).X, availWidth); ImGui.TextUnformatted(text); } diff --git a/Craftimizer/Plugin.cs b/Craftimizer/Plugin.cs index cbb5534..dea4338 100644 --- a/Craftimizer/Plugin.cs +++ b/Craftimizer/Plugin.cs @@ -1,21 +1,22 @@ +using Craftimizer.Plugin.Utils; using Craftimizer.Plugin.Windows; using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; using Craftimizer.Utils; using Craftimizer.Windows; +using Dalamud.Game.Command; using Dalamud.Interface.Internal; using Dalamud.Interface.Windowing; using Dalamud.IoC; using Dalamud.Plugin; -using ImGuiScene; -using Lumina.Excel.GeneratedSheets; +using System; +using System.Collections.Generic; using System.Reflection; -using ClassJob = Craftimizer.Simulator.ClassJob; namespace Craftimizer.Plugin; public sealed class Plugin : IDalamudPlugin { - public string Name => "Craftimizer"; public string Version { get; } public string Author { get; } public string BuildConfiguration { get; } @@ -23,23 +24,23 @@ public sealed class Plugin : IDalamudPlugin public WindowSystem WindowSystem { get; } public Settings SettingsWindow { get; } - public Craftimizer.Windows.RecipeNote RecipeNoteWindow { get; } - public Craft SynthesisWindow { get; } + public RecipeNote RecipeNoteWindow { get; } + public MacroList ListWindow { get; private set; } + public MacroEditor? EditorWindow { get; private set; } + public MacroClipboard? ClipboardWindow { get; private set; } public Configuration Configuration { get; } public Hooks Hooks { get; } - public Craftimizer.Utils.RecipeNote RecipeNote { get; } public IconManager IconManager { get; } public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface) { Service.Initialize(this, pluginInterface); + WindowSystem = new("Craftimizer"); Configuration = pluginInterface.GetPluginConfig() as Configuration ?? new(); Hooks = new(); - RecipeNote = new(); IconManager = new(); - WindowSystem = new(Name); var assembly = Assembly.GetExecutingAssembly(); Version = assembly.GetCustomAttribute()!.InformationalVersion; @@ -49,16 +50,41 @@ public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface) SettingsWindow = new(); RecipeNoteWindow = new(); - SynthesisWindow = new(); + ListWindow = new(); + + // Trigger static constructors so a huge hitch doesn't occur on first RecipeNote frame. + FoodStatus.Initialize(); + Gearsets.Initialize(); + ActionUtils.Initialize(); Service.PluginInterface.UiBuilder.Draw += WindowSystem.Draw; Service.PluginInterface.UiBuilder.OpenConfigUi += OpenSettingsWindow; + Service.PluginInterface.UiBuilder.OpenMainUi += OpenCraftingLog; + + Service.CommandManager.AddHandler("/craftimizer", new CommandInfo((_, _) => OpenSettingsWindow()) + { + HelpMessage = "Open the settings window.", + }); + Service.CommandManager.AddHandler("/craftmacros", new CommandInfo((_, _) => OpenMacroListWindow()) + { + HelpMessage = "Open the crafting macros window.", + }); + Service.CommandManager.AddHandler("/crafteditor", new CommandInfo((_, _) => OpenSettingsWindow()) + { + HelpMessage = "Open the crafting macro editor.", + }); + } + + public void OpenMacroEditor(CharacterStats characterStats, RecipeData recipeData, MacroEditor.CrafterBuffs buffs, IEnumerable actions, Action>? setter) + { + EditorWindow?.Dispose(); + EditorWindow = new(characterStats, recipeData, buffs, actions, setter); } public void OpenSettingsWindow() { - SettingsWindow.IsOpen = true; - SettingsWindow.BringToFront(); + if (SettingsWindow.IsOpen ^= true) + SettingsWindow.BringToFront(); } public void OpenSettingsTab(string selectedTabLabel) @@ -67,11 +93,36 @@ public void OpenSettingsTab(string selectedTabLabel) SettingsWindow.SelectTab(selectedTabLabel); } + public void OpenMacroListWindow() + { + ListWindow.IsOpen = true; + ListWindow.BringToFront(); + } + + public void OpenCraftingLog() + { + Chat.SendMessage("/craftinglog"); + } + + public void OpenMacroClipboard(List macros) + { + ClipboardWindow?.Dispose(); + ClipboardWindow = new(macros); + } + + public void CopyMacro(IReadOnlyList actions) => + MacroCopy.Copy(actions); + public void Dispose() { - SimulatorWindow?.Dispose(); - SynthesisWindow.Dispose(); - RecipeNote.Dispose(); + Service.CommandManager.RemoveHandler("/craftimizer"); + Service.CommandManager.RemoveHandler("/craftmacros"); + Service.CommandManager.RemoveHandler("/crafteditor"); + SettingsWindow.Dispose(); + RecipeNoteWindow.Dispose(); + ListWindow.Dispose(); + EditorWindow?.Dispose(); + ClipboardWindow?.Dispose(); Hooks.Dispose(); IconManager.Dispose(); } diff --git a/Craftimizer/Service.cs b/Craftimizer/Service.cs index 3f41681..91a4a98 100644 --- a/Craftimizer/Service.cs +++ b/Craftimizer/Service.cs @@ -1,6 +1,5 @@ using Craftimizer.Utils; using Dalamud.Game; -using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Interface.Windowing; using Dalamud.IoC; diff --git a/Craftimizer/SimulatorUtils.cs b/Craftimizer/SimulatorUtils.cs index 4ab3dd5..30d1221 100644 --- a/Craftimizer/SimulatorUtils.cs +++ b/Craftimizer/SimulatorUtils.cs @@ -6,7 +6,6 @@ using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.UI; -using ImGuiScene; using Lumina.Excel.GeneratedSheets; using System; using System.Globalization; @@ -64,6 +63,8 @@ static ActionUtils() } } + public static void Initialize() { } + public static (CraftAction? CraftAction, Action? Action) GetActionRow(this ActionType me, ClassJob classJob) => ActionRows[(int)me, (int)classJob]; @@ -309,11 +310,14 @@ public static uint StatusId(this EffectType me) => EffectType.FinalAppraisal => 2190, EffectType.WasteNot2 => 257, EffectType.MuscleMemory => 2191, - EffectType.Manipulation => 258, + EffectType.Manipulation => 1164, EffectType.HeartAndSoul => 2665, - _ => 3412, + _ => throw new ArgumentOutOfRangeException(nameof(me)), }; + public static bool IsIndefinite(this EffectType me) => + me is EffectType.InnerQuiet or EffectType.HeartAndSoul; + public static Status Status(this EffectType me) => LuminaSheets.StatusSheet.GetRow(me.StatusId())!; diff --git a/Craftimizer/Utils/Chat.cs b/Craftimizer/Utils/Chat.cs index 1098841..3efdd0a 100644 --- a/Craftimizer/Utils/Chat.cs +++ b/Craftimizer/Utils/Chat.cs @@ -1,6 +1,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI; using System; using System.Runtime.InteropServices; using System.Text; @@ -8,7 +9,7 @@ namespace Craftimizer.Plugin.Utils; // https://github.com/Caraxi/SimpleTweaksPlugin/blob/0973b93931cdf8a1b01153984d62f76d998747ff/Utility/ChatHelper.cs#L17 -public static class Chat +public static unsafe class Chat { private static class Signatures { @@ -16,11 +17,11 @@ private static class Signatures internal const string SanitiseString = "E8 ?? ?? ?? ?? EB 0A 48 8D 4C 24 ?? E8 ?? ?? ?? ?? 48 8D 8D"; } - private delegate void ProcessChatBoxDelegate(IntPtr uiModule, IntPtr message, IntPtr unused, byte a4); + private delegate void ProcessChatBoxDelegate(UIModule* uiModule, IntPtr message, IntPtr unused, byte a4); private static ProcessChatBoxDelegate? ProcessChatBox { get; } - private static readonly unsafe delegate* unmanaged _sanitiseString = null!; + private static readonly unsafe delegate* unmanaged SanitiseString = null!; static Chat() { @@ -33,7 +34,7 @@ static Chat() { if (Service.SigScanner.TryScanText(Signatures.SanitiseString, out var sanitisePtr)) { - _sanitiseString = (delegate* unmanaged)sanitisePtr; + SanitiseString = (delegate* unmanaged)sanitisePtr; } } } @@ -58,7 +59,7 @@ public static unsafe void SendMessageUnsafe(byte[] message) throw new InvalidOperationException("Could not find signature for chat sending"); } - var uiModule = (IntPtr)Framework.Instance()->GetUiModule(); + var uiModule = Framework.Instance()->GetUiModule(); using var payload = new ChatPayload(message); var mem1 = Marshal.AllocHGlobal(400); @@ -118,14 +119,14 @@ public static void SendMessage(string message) /// If the signature for this function could not be found public static unsafe string SanitiseText(string text) { - if (_sanitiseString == null) + if (SanitiseString == null) { throw new InvalidOperationException("Could not find signature for chat sanitisation"); } var uText = Utf8String.FromString(text); - _sanitiseString(uText, 0x27F, IntPtr.Zero); + SanitiseString(uText, 0x27F, IntPtr.Zero); var sanitised = uText->ToString(); uText->Dtor(); @@ -151,19 +152,19 @@ public static unsafe string SanitiseText(string text) internal ChatPayload(byte[] stringBytes) { - this.textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30); - Marshal.Copy(stringBytes, 0, this.textPtr, stringBytes.Length); - Marshal.WriteByte(this.textPtr + stringBytes.Length, 0); + textPtr = Marshal.AllocHGlobal(stringBytes.Length + 30); + Marshal.Copy(stringBytes, 0, textPtr, stringBytes.Length); + Marshal.WriteByte(textPtr + stringBytes.Length, 0); - this.textLen = (ulong)(stringBytes.Length + 1); + textLen = (ulong)(stringBytes.Length + 1); - this.unk1 = 64; - this.unk2 = 0; + unk1 = 64; + unk2 = 0; } public void Dispose() { - Marshal.FreeHGlobal(this.textPtr); + Marshal.FreeHGlobal(textPtr); } } } diff --git a/Craftimizer/Utils/Colors.cs b/Craftimizer/Utils/Colors.cs new file mode 100644 index 0000000..20ade66 --- /dev/null +++ b/Craftimizer/Utils/Colors.cs @@ -0,0 +1,12 @@ +using System.Numerics; + +namespace Craftimizer.Utils; + +public static class Colors +{ + public static readonly Vector4 Progress = new(0.44f, 0.65f, 0.18f, 1f); + public static readonly Vector4 Quality = new(0.26f, 0.71f, 0.69f, 1f); + public static readonly Vector4 Durability = new(0.13f, 0.52f, 0.93f, 1f); + public static readonly Vector4 HQ = new(0.592f, 0.863f, 0.376f, 1f); + public static readonly Vector4 CP = new(0.63f, 0.37f, 0.75f, 1f); +} diff --git a/Craftimizer/Utils/FoodStatus.cs b/Craftimizer/Utils/FoodStatus.cs new file mode 100644 index 0000000..6336c41 --- /dev/null +++ b/Craftimizer/Utils/FoodStatus.cs @@ -0,0 +1,121 @@ +using Craftimizer.Plugin; +using Craftimizer.Plugin.Utils; +using Lumina.Excel.GeneratedSheets; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Craftimizer.Utils; + +public static class FoodStatus +{ + private static readonly ReadOnlyDictionary ItemFoodToItemLUT; + private static readonly ReadOnlyDictionary FoodItems; + private static readonly ReadOnlyDictionary MedicineItems; + private static readonly ImmutableArray FoodOrder; + private static readonly ImmutableArray MedicineOrder; + + public readonly record struct FoodStat(bool IsRelative, int Value, int Max, int ValueHQ, int MaxHQ); + public readonly record struct Food(Item Item, FoodStat? Craftsmanship, FoodStat? Control, FoodStat? CP); + + static FoodStatus() + { + var lut = new Dictionary(); + foreach (var item in LuminaSheets.ItemSheet) + { + var isFood = item.ItemUICategory.Row == 46; + var isMedicine = item.ItemUICategory.Row == 44; + if (!isFood && !isMedicine) + continue; + + if (item.ItemAction.Value == null) + continue; + + if (!(item.ItemAction.Value.Type is 844 or 845 or 846)) + continue; + + var itemFood = LuminaSheets.ItemFoodSheet.GetRow(item.ItemAction.Value.Data[1]); + if (itemFood == null) + continue; + + lut.TryAdd(itemFood.RowId, item.RowId); + } + ItemFoodToItemLUT = lut.AsReadOnly(); + + var foods = new Dictionary(); + var medicines = new Dictionary(); + foreach (var item in LuminaSheets.ItemSheet) + { + var isFood = item.ItemUICategory.Row == 46; + var isMedicine = item.ItemUICategory.Row == 44; + if (!isFood && !isMedicine) + continue; + + if (item.ItemAction.Value == null) + continue; + + if (!(item.ItemAction.Value.Type is 844 or 845 or 846)) + continue; + + var itemFood = LuminaSheets.ItemFoodSheet.GetRow(item.ItemAction.Value.Data[1]); + if (itemFood == null) + continue; + + FoodStat? craftsmanship = null, control = null, cp = null; + foreach (var stat in itemFood.UnkData1) + { + if (stat.BaseParam == 0) + continue; + var foodStat = new FoodStat(stat.IsRelative, stat.Value, stat.Max, stat.ValueHQ, stat.MaxHQ); + switch (stat.BaseParam) + { + case Gearsets.ParamCraftsmanship: craftsmanship = foodStat; break; + case Gearsets.ParamControl: control = foodStat; break; + case Gearsets.ParamCP: cp = foodStat; break; + default: continue; + } + } + + if (craftsmanship != null || control != null || cp != null) + { + var food = new Food(item, craftsmanship, control, cp); + if (isFood) + foods.Add(item.RowId, food); + if (isMedicine) + medicines.Add(item.RowId, food); + } + } + + FoodItems = foods.AsReadOnly(); + MedicineItems = medicines.AsReadOnly(); + + FoodOrder = FoodItems.OrderByDescending(a => a.Value.Item.LevelItem.Row).Select(a => a.Key).ToImmutableArray(); + MedicineOrder = MedicineItems.OrderByDescending(a => a.Value.Item.LevelItem.Row).Select(a => a.Key).ToImmutableArray(); + } + + public static void Initialize() { } + + public static IEnumerable OrderedFoods => FoodOrder.Select(id => FoodItems[id]); + public static IEnumerable OrderedMedicines => MedicineOrder.Select(id => MedicineItems[id]); + + public static (uint ItemId, bool IsHQ)? ResolveFoodParam(ushort param) + { + var isHq = param > 10000; + param -= 10000; + + if (!ItemFoodToItemLUT.TryGetValue(param, out var itemId)) + return null; + + return (itemId, isHq); + } + + public static Food? TryGetFood(uint itemId) + { + if (FoodItems.TryGetValue(itemId, out var food)) + return food; + if (MedicineItems.TryGetValue(itemId, out food)) + return food; + return null; + } +} diff --git a/Craftimizer/Utils/FuzzyMatcher.cs b/Craftimizer/Utils/FuzzyMatcher.cs new file mode 100644 index 0000000..0c1cb35 --- /dev/null +++ b/Craftimizer/Utils/FuzzyMatcher.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Craftimizer.Utils; + +internal readonly struct FuzzyMatcher +{ + private const bool IsBorderMatching = true; + private static readonly (int, int)[] EmptySegArray = Array.Empty<(int, int)>(); + + private readonly string needleString = string.Empty; + private readonly int needleFinalPosition = -1; + private readonly (int Start, int End)[] needleSegments = EmptySegArray; + private readonly MatchMode mode = MatchMode.Simple; + + public FuzzyMatcher(string term, MatchMode matchMode) + { + needleString = term; + needleFinalPosition = needleString.Length - 1; + mode = matchMode; + + needleSegments = matchMode switch + { + MatchMode.FuzzyParts => FindNeedleSegments(needleString), + MatchMode.Fuzzy or MatchMode.Simple => EmptySegArray, + _ => throw new ArgumentOutOfRangeException(nameof(matchMode), matchMode, "Invalid match mode"), + }; + } + + private static (int Start, int End)[] FindNeedleSegments(ReadOnlySpan span) + { + var segments = new List<(int, int)>(); + var wordStart = -1; + + for (var i = 0; i < span.Length; i++) + { + if (span[i] is not ' ' and not '\u3000') + { + if (wordStart < 0) + wordStart = i; + } + else if (wordStart >= 0) + { + segments.Add((wordStart, i - 1)); + wordStart = -1; + } + } + + if (wordStart >= 0) + segments.Add((wordStart, span.Length - 1)); + + return segments.ToArray(); + } + + public int Matches(string value) + { + if (needleFinalPosition < 0) + return 0; + + if (mode == MatchMode.Simple) + return value.Contains(needleString, StringComparison.InvariantCultureIgnoreCase) ? 1 : 0; + + if (mode == MatchMode.Fuzzy) + return GetRawScore(value, 0, needleFinalPosition); + + if (mode == MatchMode.FuzzyParts) + { + if (needleSegments.Length < 2) + return GetRawScore(value, 0, needleFinalPosition); + + var total = 0; + for (var i = 0; i < needleSegments.Length; i++) + { + var (start, end) = needleSegments[i]; + var cur = GetRawScore(value, start, end); + if (cur == 0) + return 0; + + total += cur; + } + + return total; + } + + return 8; + } + + public int MatchesAny(params string[] values) + { + var max = 0; + for (var i = 0; i < values.Length; i++) + { + var cur = Matches(values[i]); + if (cur > max) + max = cur; + } + + return max; + } + + private int GetRawScore(ReadOnlySpan haystack, int needleStart, int needleEnd) + { + var (startPos, gaps, consecutive, borderMatches, endPos) = FindForward(haystack, needleStart, needleEnd); + if (startPos < 0) + return 0; + + var needleSize = needleEnd - needleStart + 1; + + var score = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches); + + (startPos, gaps, consecutive, borderMatches) = FindReverse(haystack, endPos, needleStart, needleEnd); + var revScore = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches); + + return int.Max(score, revScore); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CalculateRawScore(int needleSize, int startPos, int gaps, int consecutive, int borderMatches) + { + var score = 100 + + needleSize * 3 + + borderMatches * 3 + + consecutive * 5 + - startPos + - gaps * 2; + if (startPos == 0) + score += 5; + return score < 1 ? 1 : score; + } + + private (int StartPos, int Gaps, int Consecutive, int BorderMatches, int HaystackIndex) FindForward( + ReadOnlySpan haystack, int needleStart, int needleEnd) + { + var needleIndex = needleStart; + var lastMatchIndex = -10; + + var startPos = 0; + var gaps = 0; + var consecutive = 0; + var borderMatches = 0; + + for (var haystackIndex = 0; haystackIndex < haystack.Length; haystackIndex++) + { + if (haystack[haystackIndex] == needleString[needleIndex]) + { + if (IsBorderMatching) + { + if (haystackIndex > 0) + { + if (!char.IsLetterOrDigit(haystack[haystackIndex - 1])) + borderMatches++; + } + } + + needleIndex++; + + if (haystackIndex == lastMatchIndex + 1) + consecutive++; + + if (needleIndex > needleEnd) + return (startPos, gaps, consecutive, borderMatches, haystackIndex); + + lastMatchIndex = haystackIndex; + } + else + { + if (needleIndex > needleStart) + gaps++; + else + startPos++; + } + } + + return (-1, 0, 0, 0, 0); + } + + private (int StartPos, int Gaps, int Consecutive, int BorderMatches) FindReverse( + ReadOnlySpan haystack, int haystackLastMatchIndex, int needleStart, int needleEnd) + { + var needleIndex = needleEnd; + var revLastMatchIndex = haystack.Length + 10; + + var gaps = 0; + var consecutive = 0; + var borderMatches = 0; + + for (var haystackIndex = haystackLastMatchIndex; haystackIndex >= 0; haystackIndex--) + { + if (haystack[haystackIndex] == needleString[needleIndex]) + { + if (IsBorderMatching) + { + if (haystackIndex > 0) + { + if (!char.IsLetterOrDigit(haystack[haystackIndex - 1])) + borderMatches++; + } + } + + needleIndex--; + + if (haystackIndex == revLastMatchIndex - 1) + consecutive++; + + if (needleIndex < needleStart) + return (haystackIndex, gaps, consecutive, borderMatches); + + revLastMatchIndex = haystackIndex; + } + else + gaps++; + } + + return (-1, 0, 0, 0); + } +} + +internal enum MatchMode +{ + Simple, + Fuzzy, + FuzzyParts, +} diff --git a/Craftimizer/Utils/Gearsets.cs b/Craftimizer/Utils/Gearsets.cs index 20293f8..b3bc64e 100644 --- a/Craftimizer/Utils/Gearsets.cs +++ b/Craftimizer/Utils/Gearsets.cs @@ -20,6 +20,24 @@ public record struct GearsetItem(uint itemId, bool isHq, GearsetMateria[] materi public const int ParamCraftsmanship = 70; public const int ParamControl = 71; + private static readonly int[] LevelToCLvlLUT; + + static Gearsets() + { + LevelToCLvlLUT = new int[90]; + for (uint i = 0; i < 80; ++i) { + var level = i + 1; + LevelToCLvlLUT[i] = LuminaSheets.ParamGrowSheet.GetRow(level)!.CraftingLevel; + } + for (var i = 80; i < 90; ++i) + { + var level = i + 1; + LevelToCLvlLUT[i] = (int)LuminaSheets.RecipeLevelTableSheet.First(r => r.ClassJobLevel == level).RowId; + } + } + + public static void Initialize() { } + public static GearsetItem[] GetGearsetItems(InventoryContainer* container) { var items = new GearsetItem[(int)container->Size]; @@ -128,10 +146,10 @@ public static bool IsSpecialistSoulCrystal(GearsetItem item) public static bool IsSplendorousTool(GearsetItem item) => LuminaSheets.ItemSheetEnglish.GetRow(item.itemId)!.Description.ToDalamudString().TextValue.Contains("Increases to quality are 1.75 times higher than normal when material condition is Good.", StringComparison.Ordinal); - public static int CalculateCLvl(int characterLevel) => - characterLevel <= 80 - ? LuminaSheets.ParamGrowSheet.GetRow((uint)characterLevel)!.CraftingLevel - : (int)LuminaSheets.RecipeLevelTableSheet.First(r => r.ClassJobLevel == characterLevel).RowId; + public static int CalculateCLvl(int level) => + (level > 0 && level <= 90) ? + LevelToCLvlLUT[level - 1] : + throw new ArgumentOutOfRangeException(nameof(level), level, "Level is out of range."); // https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/24d0db2d9676f264edf53651b21005305267c84c/apps/client/src/app/modules/gearsets/materia.service.ts#L265 private static int CalculateParamCap(Item item, int paramId) diff --git a/Craftimizer/Utils/IconManager.cs b/Craftimizer/Utils/IconManager.cs index c6a93da..9300e15 100644 --- a/Craftimizer/Utils/IconManager.cs +++ b/Craftimizer/Utils/IconManager.cs @@ -1,6 +1,6 @@ using Craftimizer.Plugin; using Dalamud.Interface.Internal; -using ImGuiScene; +using Dalamud.Plugin.Services; using System; using System.Collections.Generic; using System.IO; @@ -11,6 +11,7 @@ namespace Craftimizer.Utils; public sealed class IconManager : IDisposable { private readonly Dictionary iconCache = new(); + private readonly Dictionary hqIconCache = new(); private readonly Dictionary textureCache = new(); private readonly Dictionary assemblyCache = new(); @@ -22,6 +23,16 @@ public IDalamudTextureWrap GetIcon(uint id) return ret; } + public IDalamudTextureWrap GetHqIcon(uint id, bool isHq = true) + { + if (!isHq) + return GetIcon(id); + if (!hqIconCache.TryGetValue(id, out var ret)) + hqIconCache.Add(id, ret = Service.TextureProvider.GetIcon(id, ITextureProvider.IconFlags.HiRes | ITextureProvider.IconFlags.ItemHighQuality) ?? + throw new ArgumentException($"Invalid hq icon id {id}", nameof(id))); + return ret; + } + public IDalamudTextureWrap GetTexture(string path) { if (!textureCache.TryGetValue(path, out var ret)) @@ -55,6 +66,10 @@ public void Dispose() image.Dispose(); iconCache.Clear(); + foreach (var image in hqIconCache.Values) + image.Dispose(); + hqIconCache.Clear(); + foreach (var image in textureCache.Values) image.Dispose(); textureCache.Clear(); diff --git a/Craftimizer/Utils/MacroCopy.cs b/Craftimizer/Utils/MacroCopy.cs new file mode 100644 index 0000000..849a3ef --- /dev/null +++ b/Craftimizer/Utils/MacroCopy.cs @@ -0,0 +1,152 @@ +using Craftimizer.Plugin; +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; +using Dalamud.Interface.Internal.Notifications; +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using ImGuiNET; +using System; +using System.Collections.Generic; + +namespace Craftimizer.Utils; + +public static class MacroCopy +{ + private const ClassJob DefaultJob = ClassJob.Carpenter; + private const int MacroSize = 15; + + public static void Copy(IReadOnlyList actions) + { + if (actions.Count == 0) + { + Service.PluginInterface.UiBuilder.AddNotification("Could not copy macro. It's empty!", "Craftimizer Macro Not Copied", NotificationType.Error); + return; + } + + var config = Service.Configuration.MacroCopy; + var macros = new List(); + var s = new List(); + foreach(var action in actions) + { + if (s.Count == 0) + { + if (config.UseMacroLock) + s.Add("/mlock"); + } + + s.Add(GetActionCommand(action, config)); + + if (config.Type == MacroCopyConfiguration.CopyType.CopyToMacro || !config.CombineMacro) + { + if (s.Count == MacroSize - 1) + { + if (GetEndCommand(macros.Count, true, config) is { } endCommand) + s.Add(endCommand); + } + if (s.Count == MacroSize) + { + macros.Add(string.Join("\n", s)); + s.Clear(); + } + } + } + if (s.Count > 0) + { + if (GetEndCommand(macros.Count, true, config) is { } endCommand) + s.Add(endCommand); + macros.Add(string.Join("\n", s)); + } + + switch (config.Type) + { + case MacroCopyConfiguration.CopyType.OpenWindow: + Service.Plugin.OpenMacroClipboard(macros); + break; + case MacroCopyConfiguration.CopyType.CopyToMacro: + CopyToMacro(macros, config); + break; + case MacroCopyConfiguration.CopyType.CopyToClipboard: + CopyToClipboard(macros, config); + break; + } + } + + private static string GetActionCommand(ActionType action, MacroCopyConfiguration config) + { + var actionBase = action.Base(); + if (actionBase is BaseComboAction) + throw new ArgumentException("Combo actions are not supported", nameof(action)); + if (config.Type != MacroCopyConfiguration.CopyType.CopyToMacro && config.RemoveWaitTimes) + return $"/ac \"{action.GetName(DefaultJob)}\""; + else + return $"/ac \"{action.GetName(DefaultJob)}\" "; + } + + private static string? GetEndCommand(int macroIdx, bool isEnd, MacroCopyConfiguration config) + { + if (config.UseNextMacro && !isEnd) + { + if (config.Type == MacroCopyConfiguration.CopyType.CopyToMacro && config.CopyDown) + return $"/nextmacro down"; + else + return $"/nextmacro"; + } + + if (config.AddNotification) + { + if (isEnd) + { + if (config.AddNotificationSound) + return $"/echo Craft complete! "; + else + return $"/echo Craft complete!"; + } + else + { + if (config.AddNotificationSound) + return $"/echo Macro #{macroIdx + 1} complete! "; + else + return $"/echo Macro #{macroIdx + 1} complete!"; + } + } + return null; + } + + private static void CopyToMacro(List macros, MacroCopyConfiguration config) + { + int i, macroIdx; + for ( + i = 0, macroIdx = config.StartMacroIdx; + i < macros.Count && i < config.MaxMacroCount && macroIdx < 100; + i++, macroIdx += config.CopyDown ? 10 : 1) + SetMacro(macroIdx, config.SharedMacro, macros[i]); + + Service.PluginInterface.UiBuilder.AddNotification(i > 1 ? "Copied macro to User Macros." : $"Copied {i} macros to User Macros.", "Craftimizer Macro Copied", NotificationType.Success); + if (i < macros.Count) + { + Service.Plugin.OpenMacroClipboard(macros); + var rest = macros.Count - i; + Service.PluginInterface.UiBuilder.AddNotification($"Couldn't copy {rest} macro{(rest == 1 ? "" : "s")}, so a window was opened with all of them.", "Craftimizer Macro Copied", NotificationType.Info); + } + } + + private static unsafe void SetMacro(int idx, bool isShared, string macroText) + { + if (idx >= 100 || idx < 0) + throw new ArgumentOutOfRangeException(nameof(idx), "Macro index must be between 0 and 99"); + + var module = RaptureMacroModule.Instance(); + var macro = module->GetMacro(isShared ? 1u : 0u, (uint)idx); + var text = Utf8String.FromString(macroText); + module->ReplaceMacroLines(macro, text); + text->Dtor(); + IMemorySpace.Free(text); + } + + private static void CopyToClipboard(List macros, MacroCopyConfiguration config) + { + ImGui.SetClipboardText(string.Join("\n\n", macros)); + Service.PluginInterface.UiBuilder.AddNotification(macros.Count > 1 ? "Copied macro to clipboard." : $"Copied {macros.Count} macros to clipboard.", "Craftimizer Macro Copied", NotificationType.Success); + } +} diff --git a/Craftimizer/Utils/RecipeData.cs b/Craftimizer/Utils/RecipeData.cs new file mode 100644 index 0000000..71d32b4 --- /dev/null +++ b/Craftimizer/Utils/RecipeData.cs @@ -0,0 +1,82 @@ +using Craftimizer.Plugin; +using Craftimizer.Simulator; +using Lumina.Excel.GeneratedSheets; +using System; +using System.Collections.Generic; +using System.Linq; +using ClassJob = Craftimizer.Simulator.ClassJob; + +namespace Craftimizer.Utils; + +public sealed record RecipeData +{ + public ushort RecipeId { get; } + + public Recipe Recipe { get; } + public RecipeLevelTable Table { get; } + + public ClassJob ClassJob { get; } + public RecipeInfo RecipeInfo { get; } + public IReadOnlyList<(Item Item, int Amount)> Ingredients { get; } + public int MaxStartingQuality { get; } + private int TotalHqILvls { get; } + + public RecipeData(ushort recipeId) + { + RecipeId = recipeId; + + Recipe = LuminaSheets.RecipeSheet.GetRow(recipeId) ?? + throw new ArgumentException($"Invalid recipe id {recipeId}", nameof(recipeId)); + + Table = Recipe.RecipeLevelTable.Value!; + ClassJob = (ClassJob)Recipe.CraftType.Row; + RecipeInfo = new() + { + IsExpert = Recipe.IsExpert, + ClassJobLevel = Table.ClassJobLevel, + RLvl = (int)Table.RowId, + ConditionsFlag = Table.ConditionsFlag, + MaxDurability = Table.Durability * Recipe.DurabilityFactor / 100, + MaxQuality = (Recipe.CanHq || Recipe.IsExpert) ? (int)Table.Quality * Recipe.QualityFactor / 100 : 0, + MaxProgress = Table.Difficulty * Recipe.DifficultyFactor / 100, + QualityModifier = Table.QualityModifier, + QualityDivider = Table.QualityDivider, + ProgressModifier = Table.ProgressModifier, + ProgressDivider = Table.ProgressDivider, + }; + + Ingredients = Recipe.UnkData5.Take(6) + .Where(i => i != null && i.ItemIngredient != 0) + .Select(i => (LuminaSheets.ItemSheet.GetRow((uint)i.ItemIngredient)!, (int)i.AmountIngredient)) + .Where(i => i.Item1 != null).ToList(); + MaxStartingQuality = (int)Math.Floor(Recipe.MaterialQualityFactor * RecipeInfo.MaxQuality / 100f); + + TotalHqILvls = (int)Ingredients.Where(i => i.Item.CanBeHq).Sum(i => i.Item.LevelItem.Row * i.Amount); + } + + public int CalculateItemStartingQuality(int itemIdx, int amount) + { + if (itemIdx >= Ingredients.Count) + throw new ArgumentOutOfRangeException(nameof(itemIdx)); + + var ingredient = Ingredients[itemIdx]; + if (amount > ingredient.Amount) + throw new ArgumentOutOfRangeException(nameof(amount)); + + if (!ingredient.Item.CanBeHq) + return 0; + + var iLvls = ingredient.Item.LevelItem.Row * amount; + return (int)Math.Floor((float)iLvls / TotalHqILvls * MaxStartingQuality); + } + + public int CalculateStartingQuality(IEnumerable hqQuantities) + { + if (TotalHqILvls == 0) + return 0; + + var iLvls = Ingredients.Zip(hqQuantities).Sum(i => i.First.Item.LevelItem.Row * i.Second); + + return (int)Math.Floor((float)iLvls / TotalHqILvls * MaxStartingQuality); + } +} diff --git a/Craftimizer/Utils/RecipeNote.cs b/Craftimizer/Utils/RecipeNote.cs deleted file mode 100644 index 6d4f4b3..0000000 --- a/Craftimizer/Utils/RecipeNote.cs +++ /dev/null @@ -1,203 +0,0 @@ -using Craftimizer.Plugin; -using Craftimizer.Simulator; -using Dalamud.Game; -using Dalamud.Logging; -using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using FFXIVClientStructs.FFXIV.Client.Game.UI; -using FFXIVClientStructs.FFXIV.Client.UI; -using FFXIVClientStructs.FFXIV.Client.UI.Agent; -using Lumina.Excel.GeneratedSheets; -using System; -using System.Linq; -using ActionType = Craftimizer.Simulator.Actions.ActionType; -using ClassJob = Craftimizer.Simulator.ClassJob; -using CSRecipeNote = FFXIVClientStructs.FFXIV.Client.Game.UI.RecipeNote; - -namespace Craftimizer.Utils; - -public record RecipeData -{ - public ushort RecipeId { get; } - - public Recipe Recipe { get; } - public RecipeLevelTable Table { get; } - - public ClassJob ClassJob { get; } - public RecipeInfo RecipeInfo { get; } - public int HQIngredientCount { get; } - public int MaxStartingQuality { get; } - - public RecipeData(ushort recipeId) - { - RecipeId = recipeId; - - Recipe = LuminaSheets.RecipeSheet.GetRow(recipeId) ?? - throw new ArgumentException($"Invalid recipe id {recipeId}", nameof(recipeId)); - - Table = Recipe.RecipeLevelTable.Value!; - ClassJob = (ClassJob)Recipe.CraftType.Row; - RecipeInfo = new() - { - IsExpert = Recipe.IsExpert, - ClassJobLevel = Table.ClassJobLevel, - RLvl = (int)Table.RowId, - ConditionsFlag = Table.ConditionsFlag, - MaxDurability = Table.Durability * Recipe.DurabilityFactor / 100, - MaxQuality = (Recipe.CanHq || Recipe.IsExpert) ? (int)Table.Quality * Recipe.QualityFactor / 100 : 0, - MaxProgress = Table.Difficulty * Recipe.DifficultyFactor / 100, - QualityModifier = Table.QualityModifier, - QualityDivider = Table.QualityDivider, - ProgressModifier = Table.ProgressModifier, - ProgressDivider = Table.ProgressDivider, - }; - - HQIngredientCount = Recipe.UnkData5 - .Where(i => - i != null && - i.ItemIngredient != 0 && - (LuminaSheets.ItemSheet.GetRow((uint)i.ItemIngredient)?.CanBeHq ?? false) - ).Sum(i => i.AmountIngredient); - MaxStartingQuality = (int)Math.Floor(Recipe.MaterialQualityFactor * RecipeInfo.MaxQuality / 100f); - } -} - -public sealed unsafe class RecipeNote : IDisposable -{ - public AddonRecipeNote* AddonRecipe { get; private set; } - public AddonSynthesis* AddonSynthesis { get; private set; } - public bool IsCrafting { get; private set; } - public ushort RecipeId { get; private set; } - public Recipe Recipe { get; private set; } = null!; - public bool HasValidRecipe { get; private set; } - - public RecipeLevelTable Table { get; private set; } = null!; - public RecipeInfo Info { get; private set; } = null!; - public ClassJob ClassJob { get; private set; } - public short CharacterLevel { get; private set; } - public bool CanUseManipulation { get; private set; } - public int HQIngredientCount { get; private set; } - public int MaxStartingQuality { get; private set; } - - public RecipeNote() - { - Service.Framework.Update += FrameworkUpdate; - } - - private void FrameworkUpdate(IFramework f) - { - HasValidRecipe = false; - try - { - HasValidRecipe = Update(); - } - catch (Exception e) - { - Log.Error(e, "RecipeNote framework update failed"); - } - } - - public bool Update() - { - if (Service.ClientState.LocalPlayer == null) - return false; - - AddonRecipe = (AddonRecipeNote*)Service.GameGui.GetAddonByName("RecipeNote"); - AddonSynthesis = (AddonSynthesis*)Service.GameGui.GetAddonByName("Synthesis"); - - var recipeId = GetRecipeIdFromList(); - if (recipeId == null) - { - recipeId = GetRecipeIdFromAgent(); - if (recipeId == null) - return false; - else - IsCrafting = true; - } - else - IsCrafting = false; - - var isNewRecipe = RecipeId != recipeId.Value; - - RecipeId = recipeId.Value; - - var recipe = LuminaSheets.RecipeSheet.GetRow(RecipeId); - - if (recipe == null) - return false; - - Recipe = recipe; - - if (isNewRecipe) - CalculateStats(); - - return true; - } - - private static ushort? GetRecipeIdFromList() - { - var instance = CSRecipeNote.Instance(); - - var list = instance->RecipeList; - - if (list == null) - return null; - - var recipeEntry = list->SelectedRecipe; - - if (recipeEntry == null) - return null; - - return recipeEntry->RecipeId; - } - - private static ushort? GetRecipeIdFromAgent() - { - var instance = AgentRecipeNote.Instance(); - - var recipeId = instance->ActiveCraftRecipeId; - - if (recipeId == 0) - return null; - - return (ushort)recipeId; - } - - private void CalculateStats() - { - Table = Recipe.RecipeLevelTable.Value!; - Info = CreateInfo(); - ClassJob = (ClassJob)Recipe.CraftType.Row; - CharacterLevel = PlayerState.Instance()->ClassJobLevelArray[ClassJob.GetExpArrayIdx()]; - CanUseManipulation = ActionManager.CanUseActionOnTarget(ActionType.Manipulation.GetId(ClassJob), (GameObject*)Service.ClientState.LocalPlayer!.Address); - HQIngredientCount = Recipe.UnkData5 - .Where(i => - i != null && - i.ItemIngredient != 0 && - (LuminaSheets.ItemSheet.GetRow((uint)i.ItemIngredient)?.CanBeHq ?? false) - ).Sum(i => i.AmountIngredient); - MaxStartingQuality = (int)Math.Floor(Recipe.MaterialQualityFactor * Info.MaxQuality / 100f); - } - - private RecipeInfo CreateInfo() => - new() - { - IsExpert = Recipe.IsExpert, - ClassJobLevel = Table.ClassJobLevel, - RLvl = (int)Table.RowId, - ConditionsFlag = Table.ConditionsFlag, - MaxDurability = Table.Durability * Recipe.DurabilityFactor / 100, - MaxQuality = (int)Table.Quality * Recipe.QualityFactor / 100, - MaxProgress = Table.Difficulty * Recipe.DifficultyFactor / 100, - QualityModifier = Table.QualityModifier, - QualityDivider = Table.QualityDivider, - ProgressModifier = Table.ProgressModifier, - ProgressDivider = Table.ProgressDivider, - }; - - public void Dispose() - { - Service.Framework.Update -= FrameworkUpdate; - } -} diff --git a/Craftimizer/Utils/SqText.cs b/Craftimizer/Utils/SqText.cs index b8ab740..86db04f 100644 --- a/Craftimizer/Utils/SqText.cs +++ b/Craftimizer/Utils/SqText.cs @@ -1,15 +1,16 @@ -using Dalamud.Game.Text; +using Dalamud.Game.Text; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Numerics; -using System.Runtime.CompilerServices; namespace Craftimizer.Utils; public static class SqText { - private static ReadOnlyDictionary levelNumReplacements = new(new Dictionary + public static SeIconChar LevelPrefix => SeIconChar.LevelEn; + + public static readonly ReadOnlyDictionary LevelNumReplacements = new(new Dictionary { ['0'] = SeIconChar.Number0, ['1'] = SeIconChar.Number1, @@ -26,8 +27,15 @@ public static class SqText public static string ToLevelString(T value) where T : IBinaryInteger { var str = value.ToString() ?? throw new FormatException("Failed to format value"); - foreach(var (k, v) in levelNumReplacements) + foreach(var (k, v) in LevelNumReplacements) str = str.Replace(k, v.ToIconChar()); - return SeIconChar.LevelEn.ToIconChar() + str; + return str; + } + + public static bool TryParseLevelString(string str, out int result) + { + foreach(var (k, v) in LevelNumReplacements) + str = str.Replace(v.ToIconChar(), k); + return int.TryParse(str, out result); } } diff --git a/Craftimizer/Windows/Craft.cs b/Craftimizer/Windows/Craft.cs deleted file mode 100644 index b3462f1..0000000 --- a/Craftimizer/Windows/Craft.cs +++ /dev/null @@ -1,226 +0,0 @@ -using Craftimizer.Plugin.Utils; -using Craftimizer.Utils; -using Dalamud.Interface; -using Dalamud.Interface.Components; -using Dalamud.Interface.Utility; -using Dalamud.Interface.Windowing; -using FFXIVClientStructs.FFXIV.Client.Game; -using ImGuiNET; -using System; -using System.Numerics; - -namespace Craftimizer.Plugin.Windows; - -public sealed unsafe partial class Craft : Window, IDisposable -{ - private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.NoDecoration - | ImGuiWindowFlags.AlwaysAutoResize - | ImGuiWindowFlags.NoSavedSettings - | ImGuiWindowFlags.NoFocusOnAppearing - | ImGuiWindowFlags.NoNavFocus; - - private const float WindowWidth = 300; - private const int ActionsPerRow = 5; - private static readonly Vector2 CraftProgressBarSize = new(WindowWidth, 15); - - private static Configuration Config => Service.Configuration; - - private static Random Random { get; } = new(); - private static RecipeNote RecipeUtils => Service.Plugin.RecipeNote; - - private bool WasOpen { get; set; } - - public Craft() : base("Craftimizer SynthesisHelper", WindowFlags, true) - { - Service.WindowSystem.AddWindow(this); - Service.Plugin.Hooks.OnActionUsed += OnActionUsed; - - IsOpen = true; - } - - public override void Draw() - { - SolveTick(); - DequeueSolver(); - - DrawActions(); - - ImGui.SameLine(0, 0); - ImGui.Dummy(default); - - ImGuiHelpers.ScaledDummy(5); - - Simulator.DrawAllProgressBars(SolverLatestState, CraftProgressBarSize); - - ImGuiHelpers.ScaledDummy(5); - - ImGui.PushFont(UiBuilder.IconFont); - var cogWidth = ImGui.CalcTextSize(FontAwesomeIcon.Cog.ToIconString()).X + (ImGui.GetStyle().FramePadding.X * 2); - ImGui.PopFont(); - - DrawSolveButton(new(WindowWidth - ImGui.GetStyle().ItemSpacing.X - cogWidth, ImGui.GetFrameHeight())); - - ImGui.SameLine(); - if (ImGuiComponents.IconButton("synthSettingsButton", FontAwesomeIcon.Cog)) - Service.Plugin.OpenSettingsTab(Settings.TabSynthHelper); - } - - private void DrawActions() - { - var actionSize = new Vector2((WindowWidth / ActionsPerRow) - (ImGui.GetStyle().ItemSpacing.X * ((ActionsPerRow - 1f) / ActionsPerRow))); - ImGui.PushStyleColor(ImGuiCol.Button, Vector4.Zero); - ImGui.PushStyleColor(ImGuiCol.ButtonActive, Vector4.Zero); - ImGui.PushStyleColor(ImGuiCol.ButtonHovered, Vector4.Zero); - - ImGui.Dummy(new(0, actionSize.Y)); - ImGui.SameLine(0, 0); - for (var i = 0; i < SolverActions.Count; ++i) - { - var (action, tooltip, state) = SolverActions[i]; - ImGui.PushID(i); - if (ImGui.ImageButton(action.GetIcon(RecipeUtils.ClassJob).ImGuiHandle, actionSize, Vector2.Zero, Vector2.One, 0)) - { - if (i == 0) - Chat.SendMessage($"/ac \"{action.GetName(RecipeUtils.ClassJob)}\""); - } - if (ImGui.IsItemHovered()) - { - ImGui.BeginTooltip(); - ImGui.Text($"{action.GetName(RecipeUtils.ClassJob)}\n{tooltip}"); - Simulator.DrawAllProgressTooltips(state); - if (i == 0) - ImGui.Text("Click to Execute"); - ImGui.EndTooltip(); - } - ImGui.PopID(); - if (i % ActionsPerRow != (ActionsPerRow - 1)) - ImGui.SameLine(); - } - - ImGui.PopStyleColor(3); - } - - private void DrawSolveButton(Vector2 buttonSize) - { - string buttonText; - string tooltipText; - bool isEnabled; - var taskCompleted = SolverTask?.IsCompleted ?? true; - var taskCancelled = SolverTaskToken?.IsCancellationRequested ?? false; - if (!taskCompleted) - { - if (taskCancelled) - { - buttonText = "Cancelling..."; - tooltipText = "Cancelling action generation. This shouldn't take long."; - isEnabled = false; - } - else - { - buttonText = "Cancel"; - tooltipText = "Cancel action generation"; - isEnabled = true; - } - } - else - { - buttonText = "Retry"; - tooltipText = "Retry and regenerate a new set of recommended actions to finish the craft."; - isEnabled = true; - } - ImGui.BeginDisabled(!isEnabled); - if (ImGui.Button(buttonText, buttonSize)) - { - if (!taskCompleted) - { - if (!taskCancelled) - SolverTaskToken?.Cancel(); - } - else - QueueSolve(GetNextState()!.Value); - } - ImGui.EndDisabled(); - if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) - ImGui.SetTooltip(tooltipText); - } - - public override void PreDraw() - { - var addon = RecipeUtils.AddonSynthesis; - ref var unit = ref addon->AtkUnitBase; - var scale = unit.Scale; - var pos = new Vector2(unit.X, unit.Y); - var size = new Vector2(unit.WindowNode->AtkResNode.Width, unit.WindowNode->AtkResNode.Height) * scale; - - var node = unit.GetNodeById(79); - - Position = pos + new Vector2(size.X, node->Y * scale); - SizeConstraints = new WindowSizeConstraints - { - MinimumSize = new(-1), - MaximumSize = new(10000, 10000) - }; - - if (Input == null) - return; - - base.PreDraw(); - } - - private bool DrawConditionsInner() - { - if (!RecipeUtils.HasValidRecipe) - return false; - - if (!RecipeUtils.IsCrafting) - return false; - - if (RecipeUtils.AddonSynthesis == null) - return false; - - // Check if Synthesis addon is visible - if (RecipeUtils.AddonSynthesis->AtkUnitBase.WindowNode == null) - return false; - - if (RecipeUtils.AddonSynthesis->AtkUnitBase.GetNodeById(79) == null) - return false; - - return base.DrawConditions(); - } - - public override bool DrawConditions() - { - if (!Config.EnableSynthHelper) - return false; - - var ret = DrawConditionsInner(); - if (ret && !WasOpen) - ResetSimulation(); - - WasOpen = ret; - return ret; - } - - private void ResetSimulation() - { - var container = InventoryManager.Instance()->GetInventoryContainer(InventoryType.EquippedItems); - if (container == null) - return; - - CharacterStats = Gearsets.CalculateCharacterStats(Gearsets.CalculateGearsetCurrentStats(), Gearsets.GetGearsetItems(container), RecipeUtils.CharacterLevel, RecipeUtils.CanUseManipulation); - Input = new(CharacterStats, RecipeUtils.Info, 0, Random); - ActionCount = 0; - ActionStates = new(); - } - - public void Dispose() - { - StopSolve(); - SolverTaskToken?.Cancel(); - SolverTask?.TryWait(); - SolverTask?.Dispose(); - SolverTaskToken?.Dispose(); - - Service.Plugin.Hooks.OnActionUsed -= OnActionUsed; - } -} diff --git a/Craftimizer/Windows/CraftAddon.cs b/Craftimizer/Windows/CraftAddon.cs deleted file mode 100644 index 57cd5be..0000000 --- a/Craftimizer/Windows/CraftAddon.cs +++ /dev/null @@ -1,157 +0,0 @@ -using Craftimizer.Simulator; -using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Interface.Windowing; -using Dalamud.Memory; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using FFXIVClientStructs.FFXIV.Client.UI; -using FFXIVClientStructs.FFXIV.Component.GUI; -using System; -using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; - -namespace Craftimizer.Plugin.Windows; - -public sealed unsafe partial class Craft : Window, IDisposable -{ - // State variables, manually kept track of outside of the addon - private CharacterStats CharacterStats = null!; - private SimulationInput Input = null!; - private int ActionCount; - private ActionStates ActionStates; - - private sealed class AddonValues - { - public AddonSynthesis* Addon { get; } - public AtkValue* Values => Addon->AtkUnitBase.AtkValues; - public ushort ValueCount => Addon->AtkUnitBase.AtkValuesCount; - - public AddonValues(AddonSynthesis* addon) - { - Addon = addon; - if (ValueCount != 26) - throw new ArgumentException("AddonSynthesis must have 26 AtkValues", nameof(addon)); - } - - public unsafe AtkValue* this[int i] => Values + i; - - // Always 0? - private uint Unk0 => GetUInt(0); - // Always true? - private bool Unk1 => GetBool(1); - - public SeString ItemName => GetString(2); - public uint ItemIconId => GetUInt(3); - public uint ItemCount => GetUInt(4); - public uint Progress => GetUInt(5); - public uint MaxProgress => GetUInt(6); - public uint Durability => GetUInt(7); - public uint MaxDurability => GetUInt(8); - public uint Quality => GetUInt(9); - public uint HQChance => GetUInt(10); - private uint IsShowingCollectibleInfoValue => GetUInt(11); - private uint ConditionValue => GetUInt(12); - public SeString ConditionName => GetString(13); - public SeString ConditionNameAndTooltip => GetString(14); - public uint StepCount => GetUInt(15); - public uint ResultItemId => GetUInt(16); - public uint MaxQuality => GetUInt(17); - public uint RequiredQuality => GetUInt(18); - private uint IsCollectibleValue => GetUInt(19); - public uint Collectability => GetUInt(20); - public uint MaxCollectability => GetUInt(21); - public uint CollectabilityCheckpoint1 => GetUInt(22); - public uint CollectabilityCheckpoint2 => GetUInt(23); - public uint CollectabilityCheckpoint3 => GetUInt(24); - public bool IsExpertRecipe => GetBool(25); - - public bool IsShowingCollectibleInfo => IsShowingCollectibleInfoValue != 0; - public Condition Condition => (Condition)(1 << (int)ConditionValue); - public bool IsCollectible => IsCollectibleValue != 0; - - private uint GetUInt(int i) - { - var value = this[i]; - return value->Type == ValueType.UInt ? - value->UInt : - throw new ArgumentException($"Value {i} is not a uint", nameof(i)); - } - - private bool GetBool(int i) - { - var value = this[i]; - return value->Type == ValueType.Bool ? - value->Byte != 0 : - throw new ArgumentException($"Value {i} is not a boolean", nameof(i)); - } - - private SeString GetString(int i) - { - var value = this[i]; - return value->Type switch - { - ValueType.AllocatedString or - ValueType.String => - MemoryHelper.ReadSeStringNullTerminated((nint)value->String), - _ => throw new ArgumentException($"Value {i} is not a string", nameof(i)) - }; - } - } - - private const ushort StatusInnerQuiet = 251; - private const ushort StatusWasteNot = 252; - private const ushort StatusVeneration = 2226; - private const ushort StatusGreatStrides = 254; - private const ushort StatusInnovation = 2189; - private const ushort StatusFinalAppraisal = 2190; - private const ushort StatusWasteNot2 = 257; - private const ushort StatusMuscleMemory = 2191; - private const ushort StatusManipulation = 1164; - private const ushort StatusHeartAndSoul = 2665; - - private SimulationState GetAddonSimulationState() - { - var player = Service.ClientState.LocalPlayer!; - var values = new AddonValues(RecipeUtils.AddonSynthesis); - var statusManager = ((Character*)player.Address)->GetStatusManager(); - - byte GetEffectStack(ushort id) - { - foreach (var status in statusManager->StatusSpan) - if (status.StatusID == id) - return status.StackCount; - return 0; - } - bool HasEffect(ushort id) - { - foreach (var status in statusManager->StatusSpan) - if (status.StatusID == id) - return true; - return false; - } - - return new(Input) - { - ActionCount = ActionCount, - StepCount = (int)values.StepCount - 1, - Progress = (int)values.Progress, - Quality = (int)values.Quality, - Durability = (int)values.Durability, - CP = (int)player.CurrentCp, - Condition = values.Condition, - ActiveEffects = new() - { - InnerQuiet = GetEffectStack(StatusInnerQuiet), - WasteNot = GetEffectStack(StatusWasteNot), - Veneration = GetEffectStack(StatusVeneration), - GreatStrides = GetEffectStack(StatusGreatStrides), - Innovation = GetEffectStack(StatusInnovation), - FinalAppraisal = GetEffectStack(StatusFinalAppraisal), - WasteNot2 = GetEffectStack(StatusWasteNot2), - MuscleMemory = GetEffectStack(StatusMuscleMemory), - Manipulation = GetEffectStack(StatusManipulation), - HeartAndSoul = HasEffect(StatusHeartAndSoul), - }, - ActionStates = ActionStates - }; - } -} - diff --git a/Craftimizer/Windows/CraftSolver.cs b/Craftimizer/Windows/CraftSolver.cs deleted file mode 100644 index a2f9881..0000000 --- a/Craftimizer/Windows/CraftSolver.cs +++ /dev/null @@ -1,99 +0,0 @@ -using Craftimizer.Simulator; -using Craftimizer.Simulator.Actions; -using Craftimizer.Utils; -using Dalamud.Interface.Windowing; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; - -namespace Craftimizer.Plugin.Windows; - -public sealed unsafe partial class Craft : Window, IDisposable -{ - private SimulationState? SolverState { get; set; } - private Solver.Solver? SolverTask { get; set; } - private CancellationTokenSource? SolverTaskToken { get; set; } - private ConcurrentQueue SolverActionQueue { get; } = new(); - - // State is the state of the simulation *after* its corresponding action is executed. - private List<(ActionType Action, string Tooltip, SimulationState State)> SolverActions { get; } = new(); - private SimulatorNoRandom SolverSim { get; set; } = null!; - private SimulationState SolverLatestState => SolverActions.Count == 0 ? SolverState!.Value : SolverActions[^1].State; - - private void StopSolve() - { - if (SolverTask == null || SolverTaskToken == null) - return; - - if (!SolverTask.IsCompleted) - SolverTaskToken.Cancel(); - else - { - SolverTaskToken.Dispose(); - SolverTask.Dispose(); - - SolverTask = null; - SolverTaskToken = null; - } - } - - private void QueueSolve(SimulationState state) - { - StopSolve(); - - SolverActionQueue.Clear(); - SolverActions.Clear(); - SolverState = state; - SolverSim = new(state); - - SolverTaskToken = new(); - SolverTask = new(Config.SynthHelperSolverConfig, state) { Token = SolverTaskToken.Token }; - SolverTask.OnLog += s => Log.Debug(s); - SolverTask.OnNewAction += SolverActionQueue.Enqueue; - SolverTask.Start(); - } - - private void SolveTick() - { - var newState = GetNextState(); - if (SolverState == newState) - return; - - if (newState == null) - StopSolve(); - else - QueueSolve(newState.Value); - } - - private void DequeueSolver() - { - while (SolverActionQueue.TryDequeue(out var poppedAction)) - AppendSolverAction(poppedAction); - } - - private void AppendSolverAction(ActionType action) - { - var actionBase = action.Base(); - if (actionBase is BaseComboAction comboActionBase) - { - AppendSolverAction(comboActionBase.ActionTypeA); - AppendSolverAction(comboActionBase.ActionTypeB); - } - else - { - if (SolverActions.Count >= Config.SynthHelperStepCount) - { - StopSolve(); - return; - } - - var tooltip = actionBase.GetTooltip(SolverSim, false); - var (_, state) = SolverSim.Execute(SolverLatestState, action); - SolverActions.Add((action, tooltip, state)); - - if (SolverActions.Count >= Config.SynthHelperStepCount) - StopSolve(); - } - } -} diff --git a/Craftimizer/Windows/CraftState.cs b/Craftimizer/Windows/CraftState.cs deleted file mode 100644 index 3ba53ce..0000000 --- a/Craftimizer/Windows/CraftState.cs +++ /dev/null @@ -1,81 +0,0 @@ -using Craftimizer.Simulator; -using Craftimizer.Simulator.Actions; -using Dalamud.Interface.Windowing; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; - -namespace Craftimizer.Plugin.Windows; - -public sealed unsafe partial class Craft : Window, IDisposable -{ - private ConcurrentQueue UsedActionQueue { get; set; } = new(); - private IEnumerator? StateTicker { get; set; } - - private SimulationState? GetNextState() - { - if (RecipeUtils.IsCrafting && StateTicker == null) - StateTicker = TickState(); - if (!RecipeUtils.IsCrafting && StateTicker != null) - StateTicker = null; - - if (StateTicker == null) - return null; - StateTicker.MoveNext(); - return StateTicker.Current; - } - - private IEnumerator TickState() - { - while (true) - { - SimulationState state; - - // Dequeue used actions - var sim = new SimulatorNoRandom(new()); - while (true) - { - state = GetAddonSimulationState(); - - var dequeued = false; - while (UsedActionQueue.TryDequeue(out var action)) - { - dequeued = true; - (_, state) = sim.Execute(state, action); - ActionCount++; - ActionStates.MutateState(action.Base()); - } - if (dequeued) - break; - - // If nothing is dequeued and executed, just return the addon state - yield return state; - } - - // Intermediate state, wait for addon change - var intermediateState = GetAddonSimulationState(); - while (true) - { - yield return state; - var newState = GetAddonSimulationState(); - if (!IsStateInIntermediate(newState, intermediateState)) - break; - } - } - } - - private static bool IsStateInIntermediate(SimulationState a, SimulationState b) - { - b.CP = a.CP; - b.ActiveEffects = a.ActiveEffects; - return a == b; - } - - private void OnActionUsed(ActionType action) - { - if (!RecipeUtils.IsCrafting || RecipeUtils.AddonSynthesis == null) - return; - - UsedActionQueue.Enqueue(action); - } -} diff --git a/Craftimizer/Windows/MacroClipboard.cs b/Craftimizer/Windows/MacroClipboard.cs new file mode 100644 index 0000000..54f7cb3 --- /dev/null +++ b/Craftimizer/Windows/MacroClipboard.cs @@ -0,0 +1,79 @@ +using Craftimizer.Plugin; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Interface.Windowing; +using ImGuiNET; +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Linq; + +namespace Craftimizer.Windows; + +public sealed class MacroClipboard : Window, IDisposable +{ + private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.None; + + private List Macros { get; } + + public MacroClipboard(IEnumerable macros) : base("Macro Clipboard", WindowFlags) + { + Macros = new(macros); + + IsOpen = true; + + Service.WindowSystem.AddWindow(this); + } + + public override void Draw() + { + var idx = 0; + foreach(var macro in Macros) + DrawMacro(idx++, macro); + } + + private void DrawMacro(int idx, string macro) + { + using var id = ImRaii.PushId(idx); + using var panel = ImGuiUtils.GroupPanel($"Macro {idx + 1}", -1, out var availWidth); + + var cursor = ImGui.GetCursorPos(); + + ImGuiUtils.AlignRight(ImGui.GetFrameHeight(), availWidth); + var buttonCursor = ImGui.GetCursorPos(); + ImGui.InvisibleButton("##copyInvButton", new(ImGui.GetFrameHeight())); + var buttonHovered = ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenOverlapped | ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); + var buttonActive = buttonHovered && ImGui.GetIO().MouseDown[(int)ImGuiMouseButton.Left]; + var buttonClicked = buttonHovered && ImGui.GetIO().MouseReleased[(int)ImGuiMouseButton.Left]; + ImGui.SetCursorPos(buttonCursor); + { + using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(buttonActive ? ImGuiCol.ButtonActive : ImGuiCol.ButtonHovered), buttonHovered); + ImGuiUtils.IconButtonSized(FontAwesomeIcon.Copy, new(ImGui.GetFrameHeight())); + if (buttonClicked) + { + ImGui.SetClipboardText(macro); + Service.PluginInterface.UiBuilder.AddNotification($"Macro {idx + 1} copied to clipboard.", "Craftimizer Macro Copied", NotificationType.Success); + } + } + if (buttonHovered) + ImGui.SetTooltip("Copy to Clipboard"); + + ImGui.SetCursorPos(cursor); + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + using var padding = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero); + using var bg = ImRaii.PushColor(ImGuiCol.FrameBg, Vector4.Zero); + var lineCount = macro.Count(c => c == '\n') + 1; + ImGui.InputTextMultiline("", ref macro, (uint)macro.Length + 1, new(availWidth, ImGui.GetTextLineHeight() * Math.Max(15, lineCount) + ImGui.GetStyle().FramePadding.Y), ImGuiInputTextFlags.ReadOnly | ImGuiInputTextFlags.AutoSelectAll); + } + + if (buttonHovered) + ImGui.SetMouseCursor(ImGuiMouseCursor.Arrow); + } + + public void Dispose() + { + Service.WindowSystem.RemoveWindow(this); + } +} diff --git a/Craftimizer/Windows/MacroEditor.cs b/Craftimizer/Windows/MacroEditor.cs new file mode 100644 index 0000000..91aafd2 --- /dev/null +++ b/Craftimizer/Windows/MacroEditor.cs @@ -0,0 +1,1372 @@ +using Craftimizer.Plugin; +using Craftimizer.Plugin.Utils; +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; +using Craftimizer.Utils; +using Dalamud.Game.ClientState.Statuses; +using Dalamud.Game.Text; +using Dalamud.Interface; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Interface.Windowing; +using Dalamud.Utility; +using ImGuiNET; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Sim = Craftimizer.Simulator.SimulatorNoRandom; + +namespace Craftimizer.Windows; + +public sealed class MacroEditor : Window, IDisposable +{ + private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.None; + + private CharacterStats characterStats = null!; + public CharacterStats CharacterStats + { + get => characterStats; + private set + { + characterStats = value with + { + Craftsmanship = Math.Clamp(value.Craftsmanship, 0, 9000), + Control = Math.Clamp(value.Control, 0, 9000), + CP = Math.Clamp(value.CP, 180, 1000), + Level = Math.Clamp(value.Level, 1, 90), + CLvl = Gearsets.CalculateCLvl(value.Level), + }; + } + } + public RecipeData RecipeData { get; private set; } + + public record CrafterBuffs + { + public (int Craftsmanship, int Control) FC { get; init; } + public (uint ItemId, bool IsHQ) Food { get; init; } + public (uint ItemId, bool IsHQ) Medicine { get; init; } + + public CrafterBuffs(StatusList? statuses) + { + if (statuses == null) + return; + + foreach (var status in statuses) + { + if (status.StatusId == 48) + Food = FoodStatus.ResolveFoodParam(status.Param) ?? default; + else if (status.StatusId == 49) + Medicine = FoodStatus.ResolveFoodParam(status.Param) ?? default; + else if (status.StatusId == 356) + FC = FC with { Craftsmanship = status.Param / 5 }; + else if (status.StatusId == 357) + FC = FC with { Control = status.Param / 5 }; + } + } + } + public CrafterBuffs Buffs { get; set; } + + private List HQIngredientCounts { get; set; } + private int StartingQuality => RecipeData.CalculateStartingQuality(HQIngredientCounts); + + private sealed record SimulatedActionStep + { + public ActionType Action { get; init; } + // State *after* executing the action + public ActionResponse Response { get; set; } + public SimulationState State { get; set; } + }; + private List Macro { get; set; } = new(); + private SimulationState InitialState { get; set; } + private SimulationState State => Macro.Count > 0 ? Macro[^1].State : InitialState; + private ActionType[] DefaultActions { get; } + private Action>? MacroSetter { get; set; } + + private CancellationTokenSource? SolverTokenSource { get; set; } + private Exception? SolverException { get; set; } + private int? SolverStartStepCount { get; set; } + private bool SolverRunning => SolverTokenSource != null; + + private IDalamudTextureWrap ExpertBadge { get; } + private IDalamudTextureWrap CollectibleBadge { get; } + private IDalamudTextureWrap SplendorousBadge { get; } + private IDalamudTextureWrap SpecialistBadge { get; } + private IDalamudTextureWrap NoManipulationBadge { get; } + private IDalamudTextureWrap ManipulationBadge { get; } + private IDalamudTextureWrap WellFedBadge { get; } + private IDalamudTextureWrap MedicatedBadge { get; } + private IDalamudTextureWrap InControlBadge { get; } + private IDalamudTextureWrap EatFromTheHandBadge { get; } + private GameFontHandle AxisFont { get; } + + public MacroEditor(CharacterStats characterStats, RecipeData recipeData, CrafterBuffs buffs, IEnumerable actions, Action>? setter) : base("Craftimizer Macro Editor", WindowFlags, false) + { + CharacterStats = characterStats; + RecipeData = recipeData; + Buffs = buffs; + MacroSetter = setter; + DefaultActions = actions.ToArray(); + + HQIngredientCounts = new(); + HQIngredientCounts.AddRange(Enumerable.Repeat(0, RecipeData.Ingredients.Count)); + + RecalculateState(); + foreach (var action in DefaultActions) + AddStep(action); + + ExpertBadge = Service.IconManager.GetAssemblyTexture("Graphics.expert_badge.png"); + CollectibleBadge = Service.IconManager.GetAssemblyTexture("Graphics.collectible_badge.png"); + SplendorousBadge = Service.IconManager.GetAssemblyTexture("Graphics.splendorous.png"); + SpecialistBadge = Service.IconManager.GetAssemblyTexture("Graphics.specialist.png"); + NoManipulationBadge = Service.IconManager.GetAssemblyTexture("Graphics.no_manip.png"); + ManipulationBadge = ActionType.Manipulation.GetIcon(RecipeData.ClassJob); + WellFedBadge = Service.IconManager.GetIcon(LuminaSheets.StatusSheet.GetRow(48)!.Icon); + MedicatedBadge = Service.IconManager.GetIcon(LuminaSheets.StatusSheet.GetRow(49)!.Icon); + InControlBadge = Service.IconManager.GetIcon(LuminaSheets.StatusSheet.GetRow(356)!.Icon); + EatFromTheHandBadge = Service.IconManager.GetIcon(LuminaSheets.StatusSheet.GetRow(357)!.Icon); + AxisFont = Service.PluginInterface.UiBuilder.GetGameFontHandle(new(GameFontFamilyAndSize.Axis14)); + + IsOpen = true; + + CollapsedCondition = ImGuiCond.Appearing; + Collapsed = false; + + SizeConstraints = new() { MinimumSize = new(821, 750), MaximumSize = new(float.PositiveInfinity) }; + + Service.WindowSystem.AddWindow(this); + } + + public override void OnClose() + { + SolverTokenSource?.Cancel(); + } + + public override void Draw() + { + var modifiedInput = false; + + using (var table = ImRaii.Table("params", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame)) + { + if (table) + { + ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableNextColumn(); + modifiedInput = DrawCharacterParams(); + ImGui.TableNextColumn(); + modifiedInput |= DrawRecipeParams(); + } + } + + if (modifiedInput) + RecalculateState(); + + ImGui.Separator(); + + using (var table = ImRaii.Table("macroInfo", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame)) { + if (table) + { + ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch, 2); + ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch, 3); + ImGui.TableNextColumn(); + DrawActionHotbars(); + ImGui.TableNextColumn(); + DrawMacroInfo(); + DrawMacro(); + } + } + } + + private bool DrawCharacterParams() + { + var oldStats = CharacterStats; + + ImGuiUtils.TextCentered("Crafter"); + + var textClassName = RecipeData.ClassJob.GetAbbreviation(); + Vector2 textClassSize; + { + var layout = AxisFont.LayoutBuilder(textClassName).Build(); + textClassSize = new(layout.Width, layout.Height); + } + + var imageSize = ImGui.GetFrameHeight(); + ImGuiUtils.AlignCentered( + imageSize + 5 + + textClassSize.X); + ImGui.AlignTextToFramePadding(); + + var uv0 = new Vector2(6, 3); + var uv1 = uv0 + new Vector2(44); + uv0 /= new Vector2(56); + uv1 /= new Vector2(56); + + ImGui.Image(Service.IconManager.GetIcon(RecipeData.ClassJob.GetIconId()).ImGuiHandle, new Vector2(imageSize), uv0, uv1); + ImGui.SameLine(0, 5); + AxisFont.Text(textClassName); + + using (var statsTable = ImRaii.Table("stats", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame)) + { + if (statsTable) + { + ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch, 4.5f); + ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch, 3); + ImGui.TableSetupColumn("col3", ImGuiTableColumnFlags.WidthStretch, 2); + + var inputWidth = ImGui.CalcTextSize(SqText.ToLevelString(9999)).X + ImGui.GetStyle().FramePadding.X * 2 + 5; + + void DrawStat(string name, int value, Action setter) + { + ImGui.AlignTextToFramePadding(); + ImGui.Text(name); + ImGui.SameLine(0, 5); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + + var text = value.ToString(); + if (ImGui.InputText($"##{name}", ref text, 8, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CharsDecimal)) + { + setter( + int.TryParse(text, out var newLevel) + ? Math.Clamp(newLevel, 0, 9999) + : 0); + } + } + + ImGui.TableNextColumn(); + DrawStat("Craftsmanship", CharacterStats.Craftsmanship, v => CharacterStats = CharacterStats with { Craftsmanship = v }); + + ImGui.TableNextColumn(); + DrawStat("Control", CharacterStats.Control, v => CharacterStats = CharacterStats with { Control = v }); + + ImGui.TableNextColumn(); + DrawStat("CP", CharacterStats.CP, v => CharacterStats = CharacterStats with { CP = v }); + } + } + + using (var paramTable = ImRaii.Table("params", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame)) + { + if (paramTable) + { + ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch, 3); + ImGui.TableSetupColumn("col3", ImGuiTableColumnFlags.WidthStretch, 2); + + ImGui.TableNextColumn(); + var levelTextWidth = ImGui.CalcTextSize(SqText.ToLevelString(99)).X + ImGui.GetStyle().FramePadding.X * 2 + 5; + ImGuiUtils.AlignCentered( + ImGui.CalcTextSize(SqText.LevelPrefix.ToIconString()).X + 5 + + levelTextWidth); + + ImGui.AlignTextToFramePadding(); + ImGui.Text(SqText.LevelPrefix.ToIconString()); + ImGui.SameLine(0, 3); + ImGui.SetNextItemWidth(levelTextWidth); + var levelText = SqText.ToLevelString(CharacterStats.Level); + bool textChanged; + unsafe + { + textChanged = ImGui.InputText("##levelText", ref levelText, 8, ImGuiInputTextFlags.CallbackCharFilter | ImGuiInputTextFlags.AutoSelectAll, LevelInputCallback); + } + if (textChanged) + CharacterStats = CharacterStats with + { + Level = + SqText.TryParseLevelString(levelText, out var newLevel) + ? Math.Clamp(newLevel, 1, 90) + : 1 + }; + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"CLvl {Gearsets.CalculateCLvl(CharacterStats.Level)}"); + + var disabledTint = new Vector4(0.5f, 0.5f, 0.5f, 0.75f); + var imageButtonPadding = (int)(ImGui.GetStyle().FramePadding.Y / 2f); + var imageButtonSize = imageSize - imageButtonPadding * 2; + { + var v = CharacterStats.HasSplendorousBuff; + var tint = v ? Vector4.One : disabledTint; + if (ImGui.ImageButton(SplendorousBadge.ImGuiHandle, new Vector2(imageButtonSize), default, Vector2.One, imageButtonPadding, default, tint)) + CharacterStats = CharacterStats with { HasSplendorousBuff = !v }; + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(CharacterStats.HasSplendorousBuff ? $"Splendorous Tool" : "No Splendorous Tool"); + } + ImGui.SameLine(0, 5); + bool? newIsSpecialist = null; + { + var v = CharacterStats.IsSpecialist; + var tint = new Vector4(0.99f, 0.97f, 0.62f, 1f) * (v ? Vector4.One : disabledTint); + if (ImGui.ImageButton(SpecialistBadge.ImGuiHandle, new Vector2(imageButtonSize), default, Vector2.One, imageButtonPadding, default, tint)) + { + v = !v; + newIsSpecialist = v; + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(v ? $"Specialist" : "Not a Specialist"); + } + ImGui.SameLine(0, 5); + { + var manipLevel = ActionType.Manipulation.GetActionRow(RecipeData.ClassJob).Action!.ClassJobLevel; + using (var d = ImRaii.Disabled(manipLevel > CharacterStats.Level)) + { + var v = CharacterStats.CanUseManipulation && manipLevel <= CharacterStats.Level; + var tint = (v || manipLevel > CharacterStats.Level) ? disabledTint : Vector4.One; + if (ImGui.ImageButton(v ? ManipulationBadge.ImGuiHandle : NoManipulationBadge.ImGuiHandle, new Vector2(imageButtonSize), default, Vector2.One, imageButtonPadding, default, tint)) + CharacterStats = CharacterStats with { CanUseManipulation = !v }; + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip(CharacterStats.CanUseManipulation && manipLevel <= CharacterStats.Level ? $"Can Use Manipulation" : "Cannot Use Manipulation"); + } + + ImGui.TableNextColumn(); + + (uint ItemId, bool HQ)? newFoodBuff = null; + var buffImageSize = new Vector2(imageSize * WellFedBadge.Width / WellFedBadge.Height, imageSize); + ImGui.Image(WellFedBadge.ImGuiHandle, buffImageSize); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Food"); + ImGui.SameLine(0, 5); + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + using var combo = ImRaii.Combo("##food", FormatItemBuff(Buffs.Food)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(FormatItemBuffDescription(Buffs.Food)); + if (combo) + { + if (ImGui.Selectable("None", Buffs.Food.ItemId == 0)) + newFoodBuff = (0, false); + + foreach (var food in FoodStatus.OrderedFoods) + { + var row = (food.Item.RowId, false); + if (ImGui.Selectable(FormatItemBuff(row), Buffs.Food == row)) + newFoodBuff = row; + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(FormatItemBuffDescription(row)); + + if (food.Item.CanBeHq) + { + row = (food.Item.RowId, true); + if (ImGui.Selectable(FormatItemBuff(row), Buffs.Food == row)) + newFoodBuff = row; + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(FormatItemBuffDescription(row)); + } + } + } + } + + (uint ItemId, bool HQ)? newMedicineBuff = null; + buffImageSize = new Vector2(imageSize * MedicatedBadge.Width / MedicatedBadge.Height, imageSize); + ImGui.Image(MedicatedBadge.ImGuiHandle, buffImageSize); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Medicine"); + ImGui.SameLine(0, 5); + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + using var combo = ImRaii.Combo("##medicine", FormatItemBuff(Buffs.Medicine)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(FormatItemBuffDescription(Buffs.Medicine)); + if (combo) + { + if (ImGui.Selectable("None", Buffs.Medicine.ItemId == 0)) + newMedicineBuff = (0, false); + + foreach (var medicine in FoodStatus.OrderedMedicines) + { + var row = (medicine.Item.RowId, false); + if (ImGui.Selectable(FormatItemBuff(row), Buffs.Medicine == row)) + newMedicineBuff = row; + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(FormatItemBuffDescription(row)); + + if (medicine.Item.CanBeHq) + { + row = (medicine.Item.RowId, true); + if (ImGui.Selectable(FormatItemBuff(row), Buffs.Medicine == row)) + newMedicineBuff = row; + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(FormatItemBuffDescription(row)); + } + } + } + } + + ImGui.TableNextColumn(); + + int? newFCCraftsmanshipBuff = null; + buffImageSize = new Vector2(imageSize * MedicatedBadge.Width / MedicatedBadge.Height, imageSize); + ImGui.Image(EatFromTheHandBadge.ImGuiHandle, buffImageSize); + var fcBuffName = "Eat from the Hand"; + var fcStatName = "Craftsmanship"; + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(fcBuffName); + ImGui.SameLine(0, 5); + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + using var combo = ImRaii.Combo("##fcCraftsmanship", FormatFCBuff(fcBuffName, Buffs.FC.Craftsmanship)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(FormatFCBuffDescription(fcBuffName, fcStatName, Buffs.FC.Craftsmanship)); + if (combo) + { + if (ImGui.Selectable("None", Buffs.FC.Craftsmanship == 0)) + newFCCraftsmanshipBuff = 0; + + for (var i = 1; i <= 3; ++i) + { + if (ImGui.Selectable(FormatFCBuff(fcBuffName, i), Buffs.FC.Craftsmanship == i)) + newFCCraftsmanshipBuff = i; + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(FormatFCBuffDescription(fcBuffName, fcStatName, i)); + } + } + } + + int? newFCControlBuff = null; + buffImageSize = new Vector2(imageSize * MedicatedBadge.Width / MedicatedBadge.Height, imageSize); + ImGui.Image(InControlBadge.ImGuiHandle, buffImageSize); + fcBuffName = "In Control"; + fcStatName = "Control"; + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(fcBuffName); + ImGui.SameLine(0, 5); + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + using var combo = ImRaii.Combo("##fcControl", FormatFCBuff(fcBuffName, Buffs.FC.Control)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(FormatFCBuffDescription(fcBuffName, fcStatName, Buffs.FC.Control)); + if (combo) + { + if (ImGui.Selectable("None", Buffs.FC.Control == 0)) + newFCControlBuff = 0; + + for (var i = 1; i <= 3; ++i) + { + if (ImGui.Selectable(FormatFCBuff(fcBuffName, i), Buffs.FC.Control == i)) + newFCControlBuff = i; + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(FormatFCBuffDescription(fcBuffName, fcStatName, i)); + } + } + } + + if (newIsSpecialist.HasValue || newFoodBuff.HasValue || newMedicineBuff.HasValue || newFCCraftsmanshipBuff.HasValue || newFCControlBuff.HasValue) + { + var baseStat = GetBaseStats(CharacterStats); + + Buffs = Buffs with + { + Food = newFoodBuff ?? Buffs.Food, + Medicine = newMedicineBuff ?? Buffs.Medicine, + FC = (newFCCraftsmanshipBuff ?? Buffs.FC.Craftsmanship, newFCControlBuff ?? Buffs.FC.Control) + }; + + var newStats = CharacterStats with { Craftsmanship = baseStat.Craftsmanship, Control = baseStat.Control, CP = baseStat.CP }; + if (newIsSpecialist is { } isSpecialist) + { + if (isSpecialist != CharacterStats.IsSpecialist) + { + var craftsmanship = 20; + var control = 20; + var cp = 15; + if (!isSpecialist) + { + craftsmanship *= -1; + control *= -1; + cp *= -1; + } + + newStats = newStats with + { + IsSpecialist = isSpecialist, + Craftsmanship = newStats.Craftsmanship + craftsmanship, + Control = newStats.Control + control, + CP = newStats.CP + cp + }; + } + } + + var bonus = CalculateConsumableBonus(newStats); + CharacterStats = newStats with + { + Craftsmanship = newStats.Craftsmanship + bonus.Craftsmanship, + Control = newStats.Control + bonus.Control, + CP = newStats.CP + bonus.CP + }; + } + } + } + + return oldStats != CharacterStats; + } + + private static unsafe int LevelInputCallback(ImGuiInputTextCallbackData* data) + { + if (data->EventFlag == ImGuiInputTextFlags.CallbackCharFilter) + { + if (SqText.LevelNumReplacements.TryGetValue((char)data->EventChar, out var seChar)) + data->EventChar = seChar.ToIconChar(); + else + return 1; + } + + return 0; + } + + private static string FormatItemBuff((uint ItemId, bool IsHQ) input) + { + if (input.ItemId == 0) + return "None"; + + var name = LuminaSheets.ItemSheet.GetRow(input.ItemId)?.Name.ToDalamudString().ToString() ?? $"Unknown ({input.ItemId})"; + return input.IsHQ ? $"{name} (HQ)" : name; + } + + private static string FormatItemBuffDescription((uint ItemId, bool IsHQ) input) + { + var s = new StringBuilder(FormatItemBuff(input) + "\n"); + + void AddStat(string name, FoodStatus.FoodStat? statNullable) + { + if (statNullable is not { } stat) + return; + + var (value, max) = input.IsHQ ? (stat.ValueHQ, stat.MaxHQ) : (stat.Value, stat.Max); + + if (!stat.IsRelative) + s.AppendLine($"{name} +{value}"); + else + s.AppendLine($"{name} +{value}%% (Max {max})"); + } + + if (FoodStatus.TryGetFood(input.ItemId) is { } food) + { + AddStat("Craftsmanship", food.Craftsmanship); + AddStat("Control", food.Control); + AddStat("CP", food.CP); + } + return s.ToString(); + } + + private static string FormatFCBuff(string name, int level) + { + if (level == 0) + return "None"; + + return $"{name} {new string('I', level)}"; + } + + private static string FormatFCBuffDescription(string name, string statName, int level) + { + if (level == 0) + return FormatFCBuff(name, level); + + return $"{FormatFCBuff(name, level)}\n{statName} +{level * 5}"; + } + + private (int Craftsmanship, int Control, int CP) GetBaseStats(CharacterStats stats) + { + var (craftsmanship, control, cp) = (stats.Craftsmanship, stats.Control, stats.CP); + + craftsmanship -= Buffs.FC.Craftsmanship * 5; + control -= Buffs.FC.Control * 5; + + var food = FoodStatus.TryGetFood(Buffs.Food.ItemId); + var medicine = FoodStatus.TryGetFood(Buffs.Medicine.ItemId); + + static void GetBaseStat(ref int val, bool isHq, FoodStatus.FoodStat? food, out float a, out int b) + { + a = 1; + b = 0; + if (food is { } stat) + { + if (stat.IsRelative) + { + a = (isHq ? stat.ValueHQ : stat.Value) / 100f; + b = isHq ? stat.MaxHQ : stat.Max; + } + else + val -= isHq ? stat.ValueHQ : stat.Value; + } + } + + static int GetBaseStat2(int val, bool foodHq, FoodStatus.FoodStat? food, bool medicineHq, FoodStatus.FoodStat? medicine) + { + GetBaseStat(ref val, foodHq, food, out var a, out var b); + GetBaseStat(ref val, medicineHq, medicine, out var c, out var d); + return CalculateBaseStat(val, a, b, c, d); + } + + craftsmanship = GetBaseStat2(craftsmanship, Buffs.Food.IsHQ, food?.Craftsmanship, Buffs.Medicine.IsHQ, medicine?.Craftsmanship); + control = GetBaseStat2(control, Buffs.Food.IsHQ, food?.Control, Buffs.Medicine.IsHQ, medicine?.Control); + cp = GetBaseStat2(cp, Buffs.Food.IsHQ, food?.CP, Buffs.Medicine.IsHQ, medicine?.CP); + + return (craftsmanship, control, cp); + } + + private (int Craftsmanship, int Control, int CP) CalculateConsumableBonus(CharacterStats stats) + { + int craftsmanship = 0, control = 0, cp = 0; + static int CalculateStatBonus(int val, bool isHq, FoodStatus.FoodStat? food) + { + if (food is { } stat) + { + if (stat.IsRelative) + return (int)Math.Min((isHq ? stat.ValueHQ : stat.Value) / 100f * val, isHq ? stat.MaxHQ : stat.Max); + else + return isHq ? stat.ValueHQ : stat.Value; + } + return 0; + } + var food = FoodStatus.TryGetFood(Buffs.Food.ItemId); + + craftsmanship += CalculateStatBonus(stats.Craftsmanship, Buffs.Food.IsHQ, food?.Craftsmanship); + control += CalculateStatBonus(stats.Control, Buffs.Food.IsHQ, food?.Control); + cp += CalculateStatBonus(stats.CP, Buffs.Food.IsHQ, food?.CP); + + var medicine = FoodStatus.TryGetFood(Buffs.Medicine.ItemId); + craftsmanship += CalculateStatBonus(stats.Craftsmanship, Buffs.Medicine.IsHQ, medicine?.Craftsmanship); + control += CalculateStatBonus(stats.Control, Buffs.Medicine.IsHQ, medicine?.Control); + cp += CalculateStatBonus(stats.CP, Buffs.Medicine.IsHQ, medicine?.CP); + + craftsmanship += Buffs.FC.Craftsmanship * 5; + control += Buffs.FC.Control * 5; + + return (craftsmanship, control, cp); + } + + // y: output stat + // a: coefficient + // b: max value for a product + // c: coefficient + // d: max value for c product + // Implementation of https://www.desmos.com/calculator/qlj9f9qjqy for calculating x from y + private static int CalculateBaseStat(int y, float a, int b, float c, int d) + { + if (y <= 0) + return 0; + + if (d / c < b / a) + (a, b, c, d) = (c, d, a, b); + + var dc = d / c; + var ba = b / a; + if (dc + b + d <= y) + return y - b - d; + else if (y <= (1 + a + c) * ba) + return (int)Math.Ceiling(y / (a + c + 1)); + else + return (int)Math.Ceiling((y - b) / (c + 1)); + } + + private bool DrawRecipeParams() + { + var oldStartingQuality = StartingQuality; + + ImGuiUtils.TextCentered("Recipe"); + + var textStars = new string('★', RecipeData!.Table.Stars); + var textStarsSize = Vector2.Zero; + if (!string.IsNullOrEmpty(textStars)) + { + var layout = AxisFont.LayoutBuilder(textStars).Build(); + textStarsSize = new(layout.Width, layout.Height); + } + var textLevel = SqText.LevelPrefix.ToIconChar() + SqText.ToLevelString(RecipeData.RecipeInfo.ClassJobLevel); + var isExpert = RecipeData.RecipeInfo.IsExpert; + var isCollectable = RecipeData.Recipe.ItemResult.Value!.IsCollectable; + var imageSize = ImGui.GetFrameHeight(); + var textSize = ImGui.GetFontSize(); + var badgeSize = new Vector2(textSize * ExpertBadge.Width / ExpertBadge.Height, textSize); + var badgeOffset = (imageSize - badgeSize.Y) / 2; + + var rightSideWidth = + 5 + ImGui.CalcTextSize(textLevel).X + + (textStarsSize != Vector2.Zero ? textStarsSize.X + 3 : 0) + + (isCollectable ? badgeSize.X + 3 : 0) + + (isExpert ? badgeSize.X + 3 : 0); + ImGui.AlignTextToFramePadding(); + + ImGui.Image(Service.IconManager.GetIcon(RecipeData.Recipe.ItemResult.Value!.Icon).ImGuiHandle, new Vector2(imageSize)); + + ImGui.SameLine(0, 5); + + ushort? newRecipe = null; + { + var recipe = RecipeData.Recipe; + if (ImGuiUtils.SearchableCombo( + "combo", + ref recipe, + LuminaSheets.RecipeSheet.Where(r => r.RecipeLevelTable.Row != 0 && r.ItemResult.Row != 0), + AxisFont.ImFont, + ImGui.GetContentRegionAvail().X - rightSideWidth, + r => r.ItemResult.Value!.Name.ToDalamudString().ToString(), + r => r.RowId.ToString(), + r => + { + ImGui.TextUnformatted($"{r.ItemResult.Value!.Name.ToDalamudString()}"); + + var classJob = (ClassJob)r.CraftType.Row; + var textLevel = SqText.LevelPrefix.ToIconChar() + SqText.ToLevelString(r.RecipeLevelTable.Value!.ClassJobLevel); + var textLevelSize = ImGui.CalcTextSize(textLevel); + ImGui.SameLine(); + + var imageSize = AxisFont.ImFont.FontSize; + ImGuiUtils.AlignRight( + imageSize + 5 + + textLevelSize.X, + ImGui.GetContentRegionAvail().X); + + var uv0 = new Vector2(6, 3); + var uv1 = uv0 + new Vector2(44); + uv0 /= new Vector2(56); + uv1 /= new Vector2(56); + + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetStyle().FramePadding.Y / 2); + ImGui.Image(Service.IconManager.GetIcon(classJob.GetIconId()).ImGuiHandle, new Vector2(imageSize), uv0, uv1); + ImGui.SameLine(0, 5); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (AxisFont.ImFont.FontSize - textLevelSize.Y) / 2); + ImGui.Text(textLevel); + })) + { + newRecipe = (ushort)recipe.RowId; + } + } + + ImGui.SameLine(0, 5); + ImGui.Text(textLevel); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"RLvl {RecipeData.RecipeInfo.RLvl}"); + + if (textStarsSize != Vector2.Zero) + { + ImGui.SameLine(0, 3); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (imageSize - textStarsSize.Y) / 2); + AxisFont.Text(textStars); + } + + if (isCollectable) + { + ImGui.SameLine(0, 3); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + badgeOffset); + ImGui.Image(CollectibleBadge.ImGuiHandle, badgeSize); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Collectible"); + } + + if (isExpert) + { + ImGui.SameLine(0, 3); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + badgeOffset); + ImGui.Image(ExpertBadge.ImGuiHandle, badgeSize); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Expert Recipe"); + } + + using (var statsTable = ImRaii.Table("stats", 3, ImGuiTableFlags.BordersInnerV)) + { + if (statsTable) + { + ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("col3", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.Text("Progress"); + ImGui.SameLine(); + ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxProgress}"); + + ImGui.TableNextColumn(); + ImGui.Text("Quality"); + ImGui.SameLine(); + ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxQuality}"); + + ImGui.TableNextColumn(); + ImGui.Text("Durability"); + ImGui.SameLine(); + ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxDurability}"); + } + } + + using (var table = ImRaii.Table("ingredientTable", 4, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame)) + { + if (table) + { + ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch, 2); + ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch, 2); + ImGui.TableSetupColumn("col3", ImGuiTableColumnFlags.WidthStretch, 2); + ImGui.TableSetupColumn("col4", ImGuiTableColumnFlags.WidthStretch, 2); + + var ingredients = RecipeData.Ingredients.GetEnumerator(); + var hqCount = HQIngredientCounts.GetEnumerator(); + + ImGui.TableNextColumn(); + DrawIngredientHQEntry(0); + DrawIngredientHQEntry(1); + + ImGui.TableNextColumn(); + DrawIngredientHQEntry(2); + DrawIngredientHQEntry(3); + + ImGui.TableNextColumn(); + DrawIngredientHQEntry(4); + DrawIngredientHQEntry(5); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + ImGui.GetStyle().FramePadding.Y); + ImGuiUtils.TextCentered($"Starting Quality"); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().FramePadding.Y); + ImGuiUtils.TextCentered($"{StartingQuality}"); + } + } + + if (newRecipe is { } recipeId) + { + RecipeData = new(recipeId); + HQIngredientCounts.Clear(); + HQIngredientCounts.AddRange(Enumerable.Repeat(0, RecipeData.Ingredients.Count)); + return true; + } + + return oldStartingQuality != StartingQuality; + } + + private void DrawIngredientHQEntry(int idx) + { + if (idx >= RecipeData.Ingredients.Count) + { + ImGui.Dummy(new(0, ImGui.GetFrameHeight())); + return; + } + + var ingredient = RecipeData.Ingredients[idx]; + var hqCount = HQIngredientCounts[idx]; + + var canHq = ingredient.Item.CanBeHq; + var icon = Service.IconManager.GetHqIcon(ingredient.Item.Icon, canHq); + var imageSize = ImGui.GetFrameHeight(); + + using (var d = ImRaii.Disabled(!canHq)) + ImGui.Image(icon.ImGuiHandle, new Vector2(imageSize)); + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + { + if (canHq) + { + var perItem = RecipeData.CalculateItemStartingQuality(idx, 1); + var total = RecipeData.CalculateItemStartingQuality(idx, hqCount); + ImGui.SetTooltip($"{ingredient.Item.Name.ToDalamudString()} {SeIconChar.HighQuality.ToIconString()}\n+{perItem} Quality/Item{(total > 0 ? $"\n+{total} Quality" : "")}"); + } + else + ImGui.SetTooltip($"{ingredient.Item.Name.ToDalamudString()}"); + } + ImGui.SameLine(0, 5); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - (5 + ImGui.CalcTextSize("/").X + 5 + ImGui.CalcTextSize($"99").X)); + using var d2 = ImRaii.Disabled(!canHq); + if (canHq) + { + var text = hqCount.ToString(); + if (ImGui.InputText($"##ingredient{idx}", ref text, 8, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CharsDecimal)) + { + HQIngredientCounts[idx] = + int.TryParse(text, out var newCount) + ? Math.Clamp(newCount, 0, ingredient.Amount) + : 0; + } + } + else + { + var text = ingredient.Amount.ToString(); + ImGui.InputText($"##ingredient{idx}", ref text, 8, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CharsDecimal); + } + ImGui.SameLine(0, 5); + ImGui.AlignTextToFramePadding(); + ImGui.Text("/"); + ImGui.SameLine(0, 5); + ImGui.AlignTextToFramePadding(); + ImGuiUtils.TextCentered($"{ingredient.Amount}"); + } + + private void DrawActionHotbars() + { + var sim = new Sim(State); + + var imageSize = ImGui.GetFrameHeight() * 2; + var spacing = ImGui.GetStyle().ItemSpacing.Y; + + using var _color = ImRaii.PushColor(ImGuiCol.Button, Vector4.Zero); + using var _color3 = ImRaii.PushColor(ImGuiCol.ButtonHovered, Vector4.Zero); + using var _color2 = ImRaii.PushColor(ImGuiCol.ButtonActive, Vector4.Zero); + using var _alpha = ImRaii.PushStyle(ImGuiStyleVar.DisabledAlpha, ImGui.GetStyle().DisabledAlpha * .5f); + foreach (var category in Enum.GetValues()) + { + if (category == ActionCategory.Combo) + continue; + + var actions = category.GetActions(); + using var panel = ImGuiUtils.GroupPanel(category.GetDisplayName(), -1, out var availSpace); + var itemsPerRow = (int)MathF.Floor((availSpace + spacing) / (imageSize + spacing)); + var itemCount = actions.Count; + var iterCount = (int)(Math.Ceiling((float)itemCount / itemsPerRow) * itemsPerRow); + for (var i = 0; i < iterCount; i++) + { + if (i % itemsPerRow != 0) + ImGui.SameLine(0, spacing); + if (i < itemCount) + { + var actionBase = actions[i].Base(); + var canUse = actionBase.CanUse(sim); + if (ImGui.ImageButton(actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(imageSize), default, Vector2.One, 0, default, !canUse ? new(1, 1, 1, ImGui.GetStyle().DisabledAlpha) : Vector4.One)) + AddStep(actions[i]); + if (!canUse && + (CharacterStats.Level < actionBase.Level || + (actions[i] == ActionType.Manipulation && !CharacterStats.CanUseManipulation) || + (actions[i] is ActionType.HeartAndSoul or ActionType.CarefulObservation && !CharacterStats.IsSpecialist) + ) + ) + { + Vector2 v1 = ImGui.GetItemRectMin(), v2 = ImGui.GetItemRectMax(); + ImGui.PushClipRect(v1, v2, true); + (v1.X, v2.X) = (v2.X, v1.X); + ImGui.GetWindowDrawList().AddLine(v1, v2, ImGui.GetColorU32(new Vector4(1, 0, 0, ImGui.GetStyle().DisabledAlpha / 2)), 5); + ImGui.PopClipRect(); + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip($"{actions[i].GetName(RecipeData!.ClassJob)}\n{actionBase.GetTooltip(sim, true)}"); + } + else + ImGui.Dummy(new(imageSize)); + } + } + + var minY = ImGui.GetCursorPosY() + ImGui.GetStyle().WindowPadding.Y - ImGui.GetStyle().CellPadding.Y; + if (SizeConstraints!.Value.MinimumSize.Y != minY) + SizeConstraints = SizeConstraints.Value with { MinimumSize = SizeConstraints.Value.MinimumSize with { Y = minY } }; + } + + private void DrawMacroInfo() + { + using (var barsTable = ImRaii.Table("simBars", 2, ImGuiTableFlags.SizingStretchSame)) + { + if (barsTable) + { + ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch, 1); + ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch, 2); + + ImGui.TableNextColumn(); + var datas = new List(3) + { + new("Durability", Colors.Durability, State.Durability, RecipeData.RecipeInfo.MaxDurability, null, null), + new("Condition", default, 0, 0, null, State.Condition) + }; + if (RecipeData.Recipe.ItemResult.Value!.IsCollectable) + datas.Add(new("Collectability", Colors.HQ, State.Collectability, State.MaxCollectability, $"{State.Collectability}", null)); + else if (RecipeData.Recipe.RequiredQuality > 0) + datas.Add(new("Quality %", Colors.HQ, State.Quality, RecipeData.Recipe.RequiredQuality, $"{(float)State.Quality / RecipeData.Recipe.RequiredQuality * 100:0}%", null)); + else if (RecipeData.RecipeInfo.MaxQuality > 0) + datas.Add(new("HQ %", Colors.HQ, State.HQPercent, 100, $"{State.HQPercent}%", null)); + DrawBars(datas); + + ImGui.TableNextColumn(); + datas = new List(3) + { + new("Progress", Colors.Progress, State.Progress, RecipeData.RecipeInfo.MaxProgress, null, null), + new("Quality", Colors.Quality, State.Quality, RecipeData.RecipeInfo.MaxQuality, null, null), + new("CP", Colors.CP, State.CP, CharacterStats.CP, null, null) + }; + if (RecipeData.RecipeInfo.MaxQuality <= 0) + datas.RemoveAt(1); + DrawBars(datas); + } + } + + using (var panel = ImGuiUtils.GroupPanel("Buffs", -1, out _)) + { + using var _font = ImRaii.PushFont(AxisFont.ImFont); + + var iconHeight = ImGui.GetFrameHeight() * 1.75f; + var durationShift = iconHeight * .2f; + + ImGui.Dummy(new(0, iconHeight + ImGui.GetStyle().ItemSpacing.Y + ImGui.GetTextLineHeight() - durationShift)); + ImGui.SameLine(0, 0); + + var effects = State.ActiveEffects; + foreach(var effect in Enum.GetValues()) + { + if (!effects.HasEffect(effect)) + continue; + + using (var group = ImRaii.Group()) + { + var icon = effect.GetIcon(effects.GetStrength(effect)); + var size = new Vector2(iconHeight * icon.Width / icon.Height, iconHeight); + + ImGui.Image(icon.ImGuiHandle, size); + if (!effect.IsIndefinite()) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - durationShift); + ImGuiUtils.TextCentered($"{effects.GetDuration(effect)}", size.X); + } + } + if (ImGui.IsItemHovered()) + { + var status = effect.Status(); + using var _reset = ImRaii.DefaultFont(); + ImGui.SetTooltip($"{status.Name.ToDalamudString()}\n{status.Description.ToDalamudString()}"); + } + ImGui.SameLine(); + } + } + } + + private readonly record struct BarData(string Name, Vector4 Color, float Value, float Max, string? Caption, Condition? Condition); + private void DrawBars(IEnumerable bars) + { + var spacing = ImGui.GetStyle().ItemSpacing.X; + var totalSize = ImGui.GetContentRegionAvail().X; + totalSize -= 2 * spacing; + var textSize = bars.Max(b => + { + if (b.Caption is { } caption) + return ImGui.CalcTextSize(caption).X; + // max (sp/2) "/" (sp/2) max + return Math.Max(ImGui.CalcTextSize($"{b.Value:0}").X, ImGui.CalcTextSize($"{b.Max:0}").X) * 2 + + spacing + + ImGui.CalcTextSize("/").X; + }); + var maxSize = (textSize - 2 * spacing - ImGui.CalcTextSize("/").X) / 2; + var barSize = totalSize - textSize - spacing; + foreach(var bar in bars) + { + using var panel = ImGuiUtils.GroupPanel(bar.Name, totalSize, out _); + if (bar.Condition is { } condition) + { + using (var g = ImRaii.Group()) + { + var size = ImGui.GetFrameHeight() + spacing + ImGui.CalcTextSize(condition.Name()).X; + ImGuiUtils.AlignCentered(size, totalSize); + ImGui.GetWindowDrawList().AddCircleFilled( + ImGui.GetCursorScreenPos() + new Vector2(ImGui.GetFrameHeight() / 2), + ImGui.GetFrameHeight() / 2, + ImGui.ColorConvertFloat4ToU32(new Vector4(.35f, .35f, .35f, 0) + condition.GetColor(DateTime.UtcNow.TimeOfDay))); + ImGui.Dummy(new(ImGui.GetFrameHeight())); + ImGui.SameLine(0, spacing); + ImGui.AlignTextToFramePadding(); + ImGui.Text(condition.Name()); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(condition.Description(CharacterStats.HasSplendorousBuff)); + } + else + { + using (var color = ImRaii.PushColor(ImGuiCol.PlotHistogram, bar.Color)) + ImGui.ProgressBar(Math.Clamp(bar.Value / bar.Max, 0, 1), new(barSize, ImGui.GetFrameHeight()), string.Empty); + ImGui.SameLine(0, spacing); + ImGui.AlignTextToFramePadding(); + if (bar.Caption is { } caption) + ImGuiUtils.TextRight(caption, textSize); + else + { + ImGuiUtils.TextRight($"{bar.Value:0}", maxSize); + ImGui.SameLine(0, spacing / 2); + ImGui.Text("/"); + ImGui.SameLine(0, spacing / 2); + ImGuiUtils.TextRight($"{bar.Max:0}", maxSize); + } + } + } + } + + private void DrawMacro() + { + var spacing = ImGui.GetStyle().ItemSpacing.X; + var imageSize = ImGui.GetFrameHeight() * 2; + var lastState = InitialState; + + using var panel = ImGuiUtils.GroupPanel("Macro", -1, out var availSpace); + ImGui.Dummy(new(0, imageSize)); + ImGui.SameLine(0, 0); + + var macroActionsHeight = ImGui.GetFrameHeightWithSpacing(); + var childHeight = ImGui.GetContentRegionAvail().Y - ImGui.GetStyle().ItemSpacing.Y * 2 - ImGui.GetStyle().CellPadding.Y - macroActionsHeight - ImGui.GetStyle().ItemSpacing.Y * 2; + + using (var child = ImRaii.Child("##macroActions", new(availSpace, childHeight))) + { + var itemsPerRow = (int)Math.Max(1, MathF.Floor((ImGui.GetContentRegionAvail().X + spacing) / (imageSize + spacing))); + using var _color = ImRaii.PushColor(ImGuiCol.Button, Vector4.Zero); + using var _color3 = ImRaii.PushColor(ImGuiCol.ButtonHovered, Vector4.Zero); + using var _color2 = ImRaii.PushColor(ImGuiCol.ButtonActive, Vector4.Zero); + for (var i = 0; i < Macro.Count; i++) + { + if (i % itemsPerRow != 0) + ImGui.SameLine(0, spacing); + var (action, response, state) = (Macro[i].Action, Macro[i].Response, Macro[i].State); + var actionBase = action.Base(); + var failedAction = response != ActionResponse.UsedAction; + using var id = ImRaii.PushId(i); + if (ImGui.ImageButton(action.GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(imageSize), default, Vector2.One, 0, default, failedAction ? new(1, 1, 1, ImGui.GetStyle().DisabledAlpha) : Vector4.One)) + RemoveStep(i); + if (response is ActionResponse.ActionNotUnlocked) + { + Vector2 v1 = ImGui.GetItemRectMin(), v2 = ImGui.GetItemRectMax(); + ImGui.PushClipRect(v1, v2, true); + (v1.X, v2.X) = (v2.X, v1.X); + ImGui.GetWindowDrawList().AddLine(v1, v2, ImGui.GetColorU32(new Vector4(1, 0, 0, ImGui.GetStyle().DisabledAlpha / 2)), 5); + ImGui.PopClipRect(); + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + { + var sim = new Sim(lastState); + ImGui.SetTooltip($"{action.GetName(RecipeData!.ClassJob)}\n{actionBase.GetTooltip(sim, true)}"); + } + lastState = state; + } + } + + var pos = ImGui.GetCursorScreenPos(); + ImGui.Dummy(default); + ImGui.GetWindowDrawList().AddLine(pos, pos + new Vector2(availSpace, 0), ImGui.GetColorU32(ImGuiCol.Border)); + ImGui.Dummy(default); + DrawMacroActions(availSpace); + } + + private void DrawMacroActions(float availWidth) + { + var height = ImGui.GetFrameHeight(); + var spacing = ImGui.GetStyle().ItemSpacing.X; + var width = availWidth - (spacing + height) * (DefaultActions.Length > 0 ? 3 : 2); // small buttons at the end + var halfWidth = (width - spacing) / 2f; + var quarterWidth = (halfWidth - spacing) / 2f; + + using (var _disabled = ImRaii.Disabled(SolverRunning)) + { + if (MacroSetter != null) + { + if (ImGui.Button("Save", new(quarterWidth, height))) + SaveMacro(); + ImGui.SameLine(); + if (ImGui.Button("Save As", new(quarterWidth, height))) + ShowSaveAsPopup(); + } + else + { + if (ImGui.Button("Save", new(halfWidth, height))) + ShowSaveAsPopup(); + } + } + DrawSaveAsPopup(); + ImGui.SameLine(); + if (SolverRunning) + { + if (SolverTokenSource?.IsCancellationRequested ?? false) + { + using var _disabled = ImRaii.Disabled(); + ImGui.Button("Stopping", new(halfWidth, height)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("This might could a while, sorry! Please report\n" + + "if this takes longer than a second."); + } + else + { + if (ImGui.Button("Stop", new(halfWidth, height))) + SolverTokenSource?.Cancel(); + } + } + else + { + if (ImGui.Button(SolverStartStepCount.HasValue ? "Regenerate" : "Generate", new(halfWidth, height))) + CalculateBestMacro(); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Suggest a way to finish the crafting recipe.\n" + + "Results aren't perfect, and levels of success\n" + + "can vary wildly depending on the solver's settings."); + } + ImGui.SameLine(); + if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Copy, new(height))) + Service.Plugin.CopyMacro(Macro.Select(s => s.Action).ToArray()); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Copy to Clipboard"); + ImGui.SameLine(); + if (DefaultActions.Length > 0) + { + using (var _disabled = ImRaii.Disabled(SolverRunning)) + { + if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Undo, new(height))) + { + SolverStartStepCount = null; + Macro.Clear(); + foreach (var action in DefaultActions) + AddStep(action); + } + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip("Reset"); + } + ImGui.SameLine(); + using (var _disabled = ImRaii.Disabled(SolverRunning)) + { + if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Trash, new(height))) + { + SolverStartStepCount = null; + Macro.Clear(); + } + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip("Clear"); + } + + private string popupMacroName = string.Empty; + private void ShowSaveAsPopup() + { + ImGui.OpenPopup($"##saveAsPopup"); + popupMacroName = string.Empty; + ImGui.SetNextWindowPos(ImGui.GetMousePos() - new Vector2(ImGui.CalcItemWidth() * .25f, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.Y * 2)); + } + + private void DrawSaveAsPopup() + { + using var popup = ImRaii.Popup($"##saveAsPopup"); + if (popup) + { + if (ImGui.IsWindowAppearing()) + ImGui.SetKeyboardFocusHere(); + ImGui.SetNextItemWidth(ImGui.CalcItemWidth()); + if (ImGui.InputTextWithHint($"##setName", "Name", ref popupMacroName, 100, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue)) + { + if (!string.IsNullOrWhiteSpace(popupMacroName)) + { + var newMacro = new Macro() { Name = popupMacroName, Actions = Macro.Select(s => s.Action).ToArray() }; + Service.Configuration.AddMacro(newMacro); + MacroSetter = actions => + { + newMacro.ActionEnumerable = actions; + Service.Configuration.Save(); + }; + ImGui.CloseCurrentPopup(); + } + } + } + } + + private void CalculateBestMacro() + { + SolverTokenSource?.Cancel(); + SolverTokenSource = new(); + SolverException = null; + + RevertPreviousMacro(); + SolverStartStepCount = Macro.Count; + + var token = SolverTokenSource.Token; + var state = State; + var task = Task.Run(() => CalculateBestMacroTask(state, token), token); + _ = task.ContinueWith(t => + { + if (token == SolverTokenSource.Token) + SolverTokenSource = null; + }); + _ = task.ContinueWith(t => + { + if (token.IsCancellationRequested) + return; + + try + { + t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException); + } + catch (AggregateException e) + { + SolverException = e; + Log.Error(e, "Calculating macro failed"); + } + }, TaskContinuationOptions.OnlyOnFaulted); + } + + private void CalculateBestMacroTask(SimulationState state, CancellationToken token) + { + var config = Service.Configuration.SimulatorSolverConfig; + + token.ThrowIfCancellationRequested(); + + var solver = new Solver.Solver(config, state) { Token = token }; + solver.OnLog += Log.Debug; + solver.OnNewAction += a => AddStep(a, isMacro: true); + solver.Start(); + _ = solver.GetTask().GetAwaiter().GetResult(); + + token.ThrowIfCancellationRequested(); + } + + private void RevertPreviousMacro() + { + if (SolverStartStepCount is { } stepCount && stepCount < Macro.Count) + Macro.RemoveRange(stepCount, Macro.Count - stepCount); + } + + private void SaveMacro() + { + MacroSetter?.Invoke(Macro.Select(s => s.Action)); + } + + private void RecalculateState() + { + InitialState = new SimulationState(new(CharacterStats, RecipeData.RecipeInfo, StartingQuality)); + var sim = new Sim(InitialState); + var lastState = InitialState; + foreach (var step in Macro) + lastState = ((step.Response, step.State) = sim.Execute(lastState, step.Action)).State; + } + + private void AddStep(ActionType action, int index = -1, bool isMacro = false) + { + if (index < -1 || index >= Macro.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + if (!isMacro && SolverRunning) + throw new InvalidOperationException("Cannot add steps while solver is running"); + if (!SolverRunning) + SolverStartStepCount = null; + + if (index == -1) + { + var sim = new Sim(State); + var resp = sim.Execute(State, action); + Macro.Add(new() { Action = action, Response = resp.Response, State = resp.NewState }); + } + else { + var state = index == 0 ? InitialState : Macro[index - 1].State; + var sim = new Sim(state); + var resp = sim.Execute(state, action); + Macro.Insert(index, new() { Action = action, Response = resp.Response, State = resp.NewState }); + state = resp.NewState; + for(var i = index + 1; i < Macro.Count; i++) + state = ((Macro[i].Response, Macro[i].State) = sim.Execute(state, Macro[i].Action)).State; + } + } + + private void RemoveStep(int index) + { + if (index < 0 || index >= Macro.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + if (SolverRunning) + throw new InvalidOperationException("Cannot remove steps while solver is running"); + SolverStartStepCount = null; + + Macro.RemoveAt(index); + + var state = index == 0 ? InitialState : Macro[index - 1].State; + var sim = new Sim(state); + for (var i = index; i < Macro.Count; i++) + state = ((Macro[i].Response, Macro[i].State) = sim.Execute(state, Macro[i].Action)).State; + } + + public void Dispose() + { + Service.WindowSystem.RemoveWindow(this); + + AxisFont.Dispose(); + } +} diff --git a/Craftimizer/Windows/MacroList.cs b/Craftimizer/Windows/MacroList.cs new file mode 100644 index 0000000..101e169 --- /dev/null +++ b/Craftimizer/Windows/MacroList.cs @@ -0,0 +1,361 @@ +using Craftimizer.Plugin; +using Craftimizer.Utils; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Interface; +using Dalamud.Interface.Windowing; +using ImGuiNET; +using System; +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Sim = Craftimizer.Simulator.SimulatorNoRandom; +using Dalamud.Interface.Utility; + +namespace Craftimizer.Windows; + +public sealed class MacroList : Window, IDisposable +{ + private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.None; + + public CharacterStats? CharacterStats { get; private set; } + public RecipeData? RecipeData { get; private set; } + private MacroEditor? EditorWindow { get; set; } + + private IReadOnlyList Macros => Service.Configuration.Macros; + private Dictionary MacroStateCache { get; } = new(); + + public MacroList() : base("Craftimizer Macro List", WindowFlags, false) + { + RefreshSearch(); + + Macro.OnMacroChanged += OnMacroChanged; + Configuration.OnMacroListChanged += OnMacroListChanged; + + CollapsedCondition = ImGuiCond.Appearing; + Collapsed = false; + + SizeConstraints = new() { MinimumSize = new(500, 520), MaximumSize = new(float.PositiveInfinity) }; + + Service.WindowSystem.AddWindow(this); + } + + public override bool DrawConditions() + { + return Service.ClientState.LocalPlayer != null; + } + + public override void PreDraw() + { + var oldCharacterStats = CharacterStats; + var oldRecipeData = RecipeData; + + EditorWindow = Service.Plugin.EditorWindow; + EditorWindow = (EditorWindow?.IsOpen ?? false) ? EditorWindow : null; + RecipeData = EditorWindow?.RecipeData ?? Service.Plugin.RecipeNoteWindow.RecipeData; + CharacterStats = EditorWindow?.CharacterStats ?? Service.Plugin.RecipeNoteWindow.CharacterStats; + + if (oldCharacterStats != CharacterStats || oldRecipeData != RecipeData) + RecalculateStats(); + } + + public override void Draw() + { + DrawSearchBar(); + using var group = ImRaii.Child("macros", new(-1, -1)); + if (sortedMacros.Count > 0) + { + var macros = new List(sortedMacros); + foreach (var macro in macros) + DrawMacro(macro); + } + else + { + var text1 = "You have no macros! Create one by opening"; + var text2 = "the Macro Editor here or from the Crafting Log."; + var text3 = "Open Crafting Log"; + var text4 = "Open Macro Editor"; + var buttonRowWidth = ImGui.CalcTextSize(text3).X + ImGui.CalcTextSize(text4).X + ImGui.GetStyle().ItemSpacing.X * 5; + var size = new Vector2( + Math.Max( + Math.Max(ImGui.CalcTextSize(text1).X, ImGui.CalcTextSize(text2).X), + buttonRowWidth + ), + ImGui.GetTextLineHeightWithSpacing() * 2 + ImGui.GetFrameHeight() + ); + ImGuiUtils.AlignMiddle(size); + using var child = ImRaii.Child("##macroMessage", size); + ImGuiUtils.TextCentered(text1); + ImGuiUtils.TextCentered(text2); + ImGuiUtils.AlignCentered(buttonRowWidth); + if (ImGui.Button(text3)) + Service.Plugin.OpenCraftingLog(); + ImGui.SameLine(); + if (ImGui.Button(text4)) + OpenEditor(null); + } + } + + private string searchText = string.Empty; + private List sortedMacros = null!; + private void DrawSearchBar() + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputTextWithHint("##search", "Search", ref searchText, 100)) + RefreshSearch(); + } + + private void DrawMacro(Macro macro) + { + var windowHeight = 2 * ImGui.GetFrameHeightWithSpacing(); + + if (macro.Actions.Any(a => a.Category() == ActionCategory.Combo)) + throw new InvalidOperationException("Combo actions should be sanitized away"); + + var stateNullable = GetMacroState(macro); + + using var panel = ImGuiUtils.GroupPanel(macro.Name, -1, out var availWidth); + var stepsAvailWidthOffset = ImGui.GetContentRegionAvail().X - availWidth; + var spacing = ImGui.GetStyle().ItemSpacing.Y; + var miniRowHeight = (windowHeight - spacing) / 2f; + + using var table = ImRaii.Table("table", stateNullable.HasValue ? 3 : 2, ImGuiTableFlags.BordersInnerV); + if (table) + { + if (stateNullable.HasValue) + ImGui.TableSetupColumn("stats", ImGuiTableColumnFlags.WidthFixed, 0); + ImGui.TableSetupColumn("actions", ImGuiTableColumnFlags.WidthFixed, 0); + ImGui.TableSetupColumn("steps", ImGuiTableColumnFlags.WidthStretch, 0); + + ImGui.TableNextRow(ImGuiTableRowFlags.None, windowHeight); + if (stateNullable is { } state) + { + ImGui.TableNextColumn(); + if (Service.Configuration.ShowOptimalMacroStat) + { + var progressHeight = windowHeight; + if (state.Progress >= state.Input.Recipe.MaxProgress && state.Input.Recipe.MaxQuality > 0) + { + ImGuiUtils.ArcProgress( + (float)state.Quality / state.Input.Recipe.MaxQuality, + progressHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Colors.Quality)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Quality: {state.Quality} / {state.Input.Recipe.MaxQuality}"); + } + else + { + ImGuiUtils.ArcProgress( + (float)state.Progress / state.Input.Recipe.MaxProgress, + progressHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Colors.Progress)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Progress: {state.Progress} / {state.Input.Recipe.MaxProgress}"); + } + } + else + { + ImGuiUtils.ArcProgress( + (float)state.Progress / state.Input.Recipe.MaxProgress, + miniRowHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Colors.Progress)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Progress: {state.Progress} / {state.Input.Recipe.MaxProgress}"); + + ImGui.SameLine(0, spacing); + ImGuiUtils.ArcProgress( + (float)state.Quality / state.Input.Recipe.MaxQuality, + miniRowHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Colors.Quality)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Quality: {state.Quality} / {state.Input.Recipe.MaxQuality}"); + + ImGuiUtils.ArcProgress((float)state.Durability / state.Input.Recipe.MaxDurability, + miniRowHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Colors.Durability)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Remaining Durability: {state.Durability} / {state.Input.Recipe.MaxDurability}"); + + ImGui.SameLine(0, spacing); + ImGuiUtils.ArcProgress( + (float)state.CP / state.Input.Stats.CP, + miniRowHeight / 2f, + .5f, + ImGui.GetColorU32(ImGuiCol.TableBorderLight), + ImGui.GetColorU32(Colors.CP)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"Remaining CP: {state.CP} / {state.Input.Stats.CP}"); + } + } + + ImGui.TableNextColumn(); + { + if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Copy, new(miniRowHeight))) + Service.Plugin.CopyMacro(macro.Actions); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Copy to Clipboard"); + ImGui.SameLine(); + if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Trash, new(miniRowHeight)) && ImGui.GetIO().KeyShift) + Service.Configuration.RemoveMacro(macro); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Delete (Hold Shift)"); + + if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.PencilAlt, new(miniRowHeight))) + ShowRenamePopup(macro); + DrawRenamePopup(macro); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Rename"); + ImGui.SameLine(); + if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Edit, new(miniRowHeight))) + OpenEditor(macro); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Open in Simulator"); + } + + ImGui.TableNextColumn(); + { + var itemsPerRow = (int)MathF.Floor((ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset + spacing) / (miniRowHeight + spacing)); + var itemCount = macro.Actions.Count; + for (var i = 0; i < itemsPerRow * 2; i++) + { + if (i % itemsPerRow != 0) + ImGui.SameLine(0, spacing); + if (i < itemCount) + { + var shouldShowMore = i + 1 == itemsPerRow * 2 && i + 1 < itemCount; + if (!shouldShowMore) + { + ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(macro.Actions[i].GetName(RecipeData!.ClassJob)); + } + else + { + var amtMore = itemCount - itemsPerRow * 2; + var pos = ImGui.GetCursorPos(); + ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight), default, Vector2.One, new(1, 1, 1, .5f)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"{macro.Actions[i].GetName(RecipeData!.ClassJob)}\nand {amtMore} more"); + ImGui.SetCursorPos(pos); + ImGui.GetWindowDrawList().AddRectFilled(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), ImGui.GetColorU32(ImGuiCol.FrameBg), miniRowHeight / 8f); + ImGui.GetWindowDrawList().AddTextClippedEx(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), $"+{amtMore}", null, new(.5f), null); + } + } + else + ImGui.Dummy(new(miniRowHeight)); + } + } + } + } + + private string popupMacroName = string.Empty; + private Macro? popupMacro; + private void ShowRenamePopup(Macro macro) + { + ImGui.OpenPopup($"##renamePopup-{macro.GetHashCode()}"); + popupMacro = macro; + popupMacroName = macro.Name; + ImGui.SetNextWindowPos(ImGui.GetMousePos() - new Vector2(ImGui.CalcItemWidth() * .25f, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.Y * 2)); + } + + private void DrawRenamePopup(Macro macro) + { + using var popup = ImRaii.Popup($"##renamePopup-{macro.GetHashCode()}"); + if (popup) + { + if (ImGui.IsWindowAppearing()) + ImGui.SetKeyboardFocusHere(); + ImGui.SetNextItemWidth(ImGui.CalcItemWidth()); + if (ImGui.InputTextWithHint($"##setName", "Name", ref popupMacroName, 100, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue)) + { + if (!string.IsNullOrWhiteSpace(popupMacroName)) + { + popupMacro!.Name = popupMacroName; + ImGui.CloseCurrentPopup(); + } + } + } + } + + private void RecalculateStats() + { + MacroStateCache.Clear(); + } + + private void RefreshSearch() + { + if (string.IsNullOrWhiteSpace(searchText)) + { + sortedMacros = new(Macros); + return; + } + var matcher = new FuzzyMatcher(searchText.ToLowerInvariant(), MatchMode.FuzzyParts); + var query = Macros.AsParallel().Select(i => (Item: i, Score: matcher.Matches(i.Name.ToLowerInvariant()))) + .Where(t => t.Score > 0) + .OrderByDescending(t => t.Score) + .Select(t => t.Item); + sortedMacros = query.ToList(); + } + + private void OpenEditor(Macro? macro) + { + var character = CharacterStats ?? new() + { + Craftsmanship = 100, + Control = 100, + CP = 200, + Level = 10, + CanUseManipulation = false, + HasSplendorousBuff = false, + IsSpecialist = false, + CLvl = 10, + }; + var recipe = RecipeData ?? new(1023); + + var buffs = EditorWindow?.Buffs ?? new(Service.Plugin.RecipeNoteWindow.CharacterStats != null ? Service.ClientState.LocalPlayer?.StatusList : null); + Service.Plugin.OpenMacroEditor(character, recipe, buffs, macro?.Actions ?? Enumerable.Empty(), macro != null ? (actions => { macro.ActionEnumerable = actions; Service.Configuration.Save(); }) : null); + } + + private void OnMacroChanged(Macro macro) + { + MacroStateCache.Remove(macro); + } + + private void OnMacroListChanged() + { + RefreshSearch(); + } + + private SimulationState? GetMacroState(Macro macro) + { + if (CharacterStats == null || RecipeData == null) + return null; + + if (MacroStateCache.TryGetValue(macro, out var state)) + return state; + + state = new SimulationState(new(CharacterStats, RecipeData.RecipeInfo)); + var sim = new Sim(state); + (_, state, _) = sim.ExecuteMultiple(state, macro.Actions); + return MacroStateCache[macro] = state; + } + + public void Dispose() + { + Macro.OnMacroChanged -= OnMacroChanged; + Configuration.OnMacroListChanged -= OnMacroListChanged; + + Service.WindowSystem.RemoveWindow(this); + } +} diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index 185c489..cbc2855 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -4,12 +4,14 @@ using Craftimizer.Simulator.Actions; using Craftimizer.Solver; using Craftimizer.Utils; +using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Utility; @@ -59,6 +61,7 @@ public enum CraftableStatus private CancellationTokenSource? BestMacroTokenSource { get; set; } private Exception? BestMacroException { get; set; } public (Macro, SimulationState)? BestSavedMacro { get; private set; } + public bool HasSavedMacro { get; private set; } public SolverSolution? BestSuggestedMacro { get; private set; } private IDalamudTextureWrap ExpertBadge { get; } @@ -82,7 +85,21 @@ public RecipeNote() : base("Craftimizer RecipeNote", WindowFlags, false) IsOpen = true; } + private bool wasOpen; public override bool DrawConditions() + { + var isOpen = ShouldDraw(); + if (isOpen != wasOpen) + { + if (wasOpen) + BestMacroTokenSource?.Cancel(); + } + + wasOpen = isOpen; + return isOpen; + } + + private bool ShouldDraw() { if (Service.ClientState.LocalPlayer == null) return false; @@ -146,7 +163,7 @@ public override bool DrawConditions() statsChanged = true; } - if (statsChanged && CraftStatus == CraftableStatus.OK) + if ((statsChanged || (BestMacroTokenSource?.IsCancellationRequested ?? false)) && CraftStatus == CraftableStatus.OK) CalculateBestMacros(); return true; @@ -172,16 +189,22 @@ public override void PreDraw() public override void Draw() { + var availWidth = ImGui.GetContentRegionAvail().X; + using (var table = ImRaii.Table("stats", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingFixedSame)) { - using var table = ImRaii.Table("stats", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame); if (table) { - ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthFixed, 0); + ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthFixed, 0); ImGui.TableNextColumn(); DrawCharacterStats(); ImGui.TableNextColumn(); DrawRecipeStats(); + + // Ensure that we know the window should be the same size as this table. Any more and it'll grow slowly and won't shrink when it could + ImGui.SameLine(0, 0); + // The -1 is to account for the extra vertical separator on the right that ImGui draws for some reason + availWidth = ImGui.GetCursorPosX() - ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().CellPadding.X - 1; } } @@ -190,35 +213,44 @@ public override void Draw() ImGui.Separator(); + using (var table = ImRaii.Table("macros", 1, ImGuiTableFlags.SizingStretchSame)) { - using var table = ImRaii.Table("macros", 1, ImGuiTableFlags.SizingStretchSame); if (table) { ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGuiUtils.TextCentered("Best Saved Macro"); - if (BestSavedMacro is { } savedMacro) + availWidth -= ImGui.GetStyle().ItemSpacing.X * 2; + using (var panel = ImGuiUtils.GroupPanel("Best Saved Macro", availWidth, out _)) { - ImGuiUtils.TextCentered(savedMacro.Item1.Name); - DrawMacro("savedMacro", (savedMacro.Item1.Actions, savedMacro.Item2)); + var stepsAvailWidthOffset = ImGui.GetContentRegionAvail().X - availWidth; + if (BestSavedMacro is { } savedMacro) + { + ImGuiUtils.TextCentered(savedMacro.Item1.Name, availWidth); + DrawMacro((savedMacro.Item1.Actions, savedMacro.Item2), a => { savedMacro.Item1.ActionEnumerable = a; Service.Configuration.Save(); }, stepsAvailWidthOffset, true); + } + else + { + ImGui.Text(""); + DrawMacro(null, null, stepsAvailWidthOffset, true); + } } - else + + using (var panel = ImGuiUtils.GroupPanel("Suggested Macro", availWidth, out _)) { - ImGui.Text(""); - DrawMacro("savedMacro", null); + var stepsAvailWidthOffset = ImGui.GetContentRegionAvail().X - availWidth; + if (BestSuggestedMacro is { } suggestedMacro) + DrawMacro((suggestedMacro.Actions, suggestedMacro.State), null, stepsAvailWidthOffset, false); + else + DrawMacro(null, null, stepsAvailWidthOffset, false); } - ImGui.Button("View Saved Macros", new(-1, 0)); - ImGui.Separator(); + ImGuiHelpers.ScaledDummy(5); - ImGui.AlignTextToFramePadding(); - ImGuiUtils.TextCentered("Suggested Macro"); - if (BestSuggestedMacro is { } suggestedMacro) - DrawMacro("suggestedMacro", (suggestedMacro.Actions, suggestedMacro.State)); - else - DrawMacro("suggestedMacro", null); - ImGui.Button("Open Simulator", new(-1, 0)); + if (ImGui.Button("View Saved Macros", new(-1, 0))) + Service.Plugin.OpenMacroListWindow(); + + if (ImGui.Button("Open in Simulator", new(-1, 0))) + Service.Plugin.OpenMacroEditor(CharacterStats!, RecipeData!, new(Service.ClientState.LocalPlayer!.StatusList), Enumerable.Empty(), null); } } } @@ -237,7 +269,7 @@ private void DrawCharacterStats() } var levelText = string.Empty; if (level != 0) - levelText = SqText.ToLevelString(level); + levelText = SqText.LevelPrefix.ToIconChar() + SqText.ToLevelString(level); var imageSize = ImGui.GetFrameHeight(); bool hasSplendorous = false, hasSpecialist = false, shouldHaveManip = false; if (CraftStatus is not (CraftableStatus.LockedClassJob or CraftableStatus.WrongClassJob)) @@ -338,6 +370,7 @@ private void DrawCharacterStats() } else ImGuiUtils.TextCentered($"You do not have any {RecipeData.ClassJob.GetName().ToLowerInvariant()} gearsets."); + ImGui.Dummy(default); } break; case CraftableStatus.SpecialistRequired: @@ -439,9 +472,9 @@ private void DrawRecipeStats() var textStarsSize = Vector2.Zero; if (!string.IsNullOrEmpty(textStars)) { var layout = AxisFont.LayoutBuilder(textStars).Build(); - textStarsSize = new(layout.Width + 3, layout.Height); + textStarsSize = new(layout.Width, layout.Height); } - var textLevel = SqText.ToLevelString(RecipeData.RecipeInfo.ClassJobLevel); + var textLevel = SqText.LevelPrefix.ToIconChar() + SqText.ToLevelString(RecipeData.RecipeInfo.ClassJobLevel); var isExpert = RecipeData.RecipeInfo.IsExpert; var isCollectable = RecipeData.Recipe.ItemResult.Value!.IsCollectable; var imageSize = ImGui.GetFrameHeight(); @@ -452,7 +485,7 @@ private void DrawRecipeStats() ImGuiUtils.AlignCentered( imageSize + 5 + ImGui.CalcTextSize(textLevel).X + - textStarsSize.X + + (textStarsSize != Vector2.Zero ? textStarsSize.X + 3 : 0) + (isCollectable ? badgeSize.X + 3 : 0) + (isExpert ? badgeSize.X + 3 : 0) ); @@ -516,16 +549,16 @@ private void DrawRecipeStats() } } - private void DrawMacro(string imGuiId, (List Actions, SimulationState State)? macroValue) + private void DrawMacro((IReadOnlyList Actions, SimulationState State)? macroValue, Action>? setter, float stepsAvailWidthOffset, bool isSavedMacro) { - //using var window = ImRaii.Child(imGuiId, new(-1, (name != null ? ImGui.GetTextLineHeightWithSpacing() : 0) + 2 * ImGui.GetFrameHeightWithSpacing()), false, ImGuiWindowFlags.AlwaysAutoResize); - var windowHeight = 2 * ImGui.GetFrameHeightWithSpacing(); - if (macroValue == null) + if (macroValue is not { } macro) { - if (BestMacroException == null) - ImGuiUtils.TextMiddleNewLine("Calculating...", new(ImGui.GetContentRegionAvail().X, windowHeight + 1 + ImGui.GetStyle().ItemSpacing.Y)); + if (isSavedMacro && !HasSavedMacro) + ImGuiUtils.TextMiddleNewLine("You have no macros!", new(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight + 1)); + else if (BestMacroException == null) + ImGuiUtils.TextMiddleNewLine("Calculating...", new(ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset, windowHeight + 1)); else { ImGui.AlignTextToFramePadding(); @@ -536,8 +569,8 @@ private void DrawMacro(string imGuiId, (List Actions, SimulationStat } return; } - - var macro = macroValue!.Value; + if (macro.Actions.Any(a => a.Category() == ActionCategory.Combo)) + throw new InvalidOperationException("Combo actions should be sanitized away"); using var table = ImRaii.Table("table", 3, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame); if (table) @@ -552,7 +585,6 @@ private void DrawMacro(string imGuiId, (List Actions, SimulationStat var spacing = ImGui.GetStyle().ItemSpacing.Y; var miniRowHeight = (windowHeight - spacing) / 2f; - //ImGui.Text($"{macro.Actions.Count}"); { if (Service.Configuration.ShowOptimalMacroStat) { @@ -564,7 +596,7 @@ private void DrawMacro(string imGuiId, (List Actions, SimulationStat progressHeight / 2f, .5f, ImGui.GetColorU32(ImGuiCol.TableBorderLight), - ImGui.GetColorU32(Plugin.Windows.Simulator.QualityColor)); + ImGui.GetColorU32(Colors.Quality)); if (ImGui.IsItemHovered()) ImGui.SetTooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}"); } @@ -575,7 +607,7 @@ private void DrawMacro(string imGuiId, (List Actions, SimulationStat progressHeight / 2f, .5f, ImGui.GetColorU32(ImGuiCol.TableBorderLight), - ImGui.GetColorU32(Plugin.Windows.Simulator.ProgressColor)); + ImGui.GetColorU32(Colors.Progress)); if (ImGui.IsItemHovered()) ImGui.SetTooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}"); } @@ -587,7 +619,7 @@ private void DrawMacro(string imGuiId, (List Actions, SimulationStat miniRowHeight / 2f, .5f, ImGui.GetColorU32(ImGuiCol.TableBorderLight), - ImGui.GetColorU32(Plugin.Windows.Simulator.ProgressColor)); + ImGui.GetColorU32(Colors.Progress)); if (ImGui.IsItemHovered()) ImGui.SetTooltip($"Progress: {macro.State.Progress} / {macro.State.Input.Recipe.MaxProgress}"); @@ -597,7 +629,7 @@ private void DrawMacro(string imGuiId, (List Actions, SimulationStat miniRowHeight / 2f, .5f, ImGui.GetColorU32(ImGuiCol.TableBorderLight), - ImGui.GetColorU32(Plugin.Windows.Simulator.QualityColor)); + ImGui.GetColorU32(Colors.Quality)); if (ImGui.IsItemHovered()) ImGui.SetTooltip($"Quality: {macro.State.Quality} / {macro.State.Input.Recipe.MaxQuality}"); @@ -605,7 +637,7 @@ private void DrawMacro(string imGuiId, (List Actions, SimulationStat miniRowHeight / 2f, .5f, ImGui.GetColorU32(ImGuiCol.TableBorderLight), - ImGui.GetColorU32(Plugin.Windows.Simulator.DurabilityColor)); + ImGui.GetColorU32(Colors.Durability)); if (ImGui.IsItemHovered()) ImGui.SetTooltip($"Remaining Durability: {macro.State.Durability} / {macro.State.Input.Recipe.MaxDurability}"); @@ -615,7 +647,7 @@ private void DrawMacro(string imGuiId, (List Actions, SimulationStat miniRowHeight / 2f, .5f, ImGui.GetColorU32(ImGuiCol.TableBorderLight), - ImGui.GetColorU32(Plugin.Windows.Simulator.CPColor)); + ImGui.GetColorU32(Colors.CP)); if (ImGui.IsItemHovered()) ImGui.SetTooltip($"Remaining CP: {macro.State.CP} / {macro.State.Input.Stats.CP}"); } @@ -623,31 +655,44 @@ private void DrawMacro(string imGuiId, (List Actions, SimulationStat ImGui.TableNextColumn(); { - ImGuiUtils.TextMiddleNewLine($"{macro.Actions.Count}", new(miniRowHeight)); + if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Edit, new(miniRowHeight))) + Service.Plugin.OpenMacroEditor(CharacterStats!, RecipeData!, new(Service.ClientState.LocalPlayer!.StatusList), macro.Actions, setter); if (ImGui.IsItemHovered()) - ImGui.SetTooltip($"{macro.Actions.Count} Step{(macro.Actions.Count != 1 ? "s" : "")}"); - using (var iconFont = ImRaii.PushFont(UiBuilder.IconFont)) - if (ImGuiUtils.ButtonCentered(FontAwesomeIcon.Copy.ToIconString(), new(miniRowHeight))) - { - throw new NotImplementedException(); - } + ImGui.SetTooltip("Open in Simulator"); + if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Copy, new(miniRowHeight))) + Service.Plugin.CopyMacro(macro.Actions); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Copy to Clipboard"); } ImGui.TableNextColumn(); { - var itemsPerRow = (int)MathF.Ceiling((ImGui.GetContentRegionAvail().X + spacing) / (miniRowHeight + spacing)); - var itemCount = Math.Min(macro.Actions.Count, itemsPerRow * 2); + var itemsPerRow = (int)MathF.Floor((ImGui.GetContentRegionAvail().X - stepsAvailWidthOffset + spacing) / (miniRowHeight + spacing)); + var itemCount = macro.Actions.Count; for (var i = 0; i < itemsPerRow * 2; i++) { if (i % itemsPerRow != 0) ImGui.SameLine(0, spacing); if (i < itemCount) { - ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight)); - if (ImGui.IsItemHovered()) - ImGui.SetTooltip(macro.Actions[i].GetName(RecipeData!.ClassJob)); + var shouldShowMore = i + 1 == itemsPerRow * 2 && i + 1 < itemCount; + if (!shouldShowMore) + { + ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(macro.Actions[i].GetName(RecipeData!.ClassJob)); + } + else + { + var amtMore = itemCount - itemsPerRow * 2; + var pos = ImGui.GetCursorPos(); + ImGui.Image(macro.Actions[i].GetIcon(RecipeData!.ClassJob).ImGuiHandle, new(miniRowHeight), default, Vector2.One, new(1, 1, 1, .5f)); + if (ImGui.IsItemHovered()) + ImGui.SetTooltip($"{macro.Actions[i].GetName(RecipeData!.ClassJob)}\nand {amtMore} more"); + ImGui.SetCursorPos(pos); + ImGui.GetWindowDrawList().AddRectFilled(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), ImGui.GetColorU32(ImGuiCol.FrameBg), miniRowHeight / 8f); + ImGui.GetWindowDrawList().AddTextClippedEx(ImGui.GetCursorScreenPos(), ImGui.GetCursorScreenPos() + new Vector2(miniRowHeight), $"+{amtMore}", null, new(.5f), null); + } } else ImGui.Dummy(new(miniRowHeight)); @@ -758,25 +803,31 @@ private void CalculateBestMacros() BestMacroTokenSource = new(); BestMacroException = null; BestSavedMacro = null; + HasSavedMacro = false; BestSuggestedMacro = null; var token = BestMacroTokenSource.Token; - _ = Task.Run(() => CalculateBestMacrosTask(token), token) - .ContinueWith(t => - { - if (token.IsCancellationRequested) - return; + var task = Task.Run(() => CalculateBestMacrosTask(token), token); + _ = task.ContinueWith(t => + { + if (token == BestMacroTokenSource.Token) + BestMacroTokenSource = null; + }); + _ = task.ContinueWith(t => + { + if (token.IsCancellationRequested) + return; - try - { - t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException); - } - catch (AggregateException e) - { - BestMacroException = e; - Log.Error(e, "Calculating macros failed"); - } - }, TaskContinuationOptions.OnlyOnFaulted); + try + { + t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException); + } + catch (AggregateException e) + { + BestMacroException = e; + Log.Error(e, "Calculating macros failed"); + } + }, TaskContinuationOptions.OnlyOnFaulted); } private void CalculateBestMacrosTask(CancellationToken token) @@ -790,26 +841,30 @@ private void CalculateBestMacrosTask(CancellationToken token) token.ThrowIfCancellationRequested(); - var bestSaved = macros - .Select(macro => - { - var (resp, outState, failedIdx) = simulator.ExecuteMultiple(state, macro.Actions); - outState.ActionCount = macro.Actions.Count; - var score = SimulationNode.CalculateScoreForState(outState, simulator.CompletionState, mctsConfig) ?? 0; - if (resp != ActionResponse.SimulationComplete) + HasSavedMacro = macros.Count > 0; + if (HasSavedMacro) + { + var bestSaved = macros + .Select(macro => { - if (failedIdx != -1) - score /= 2; - } - return (macro, outState, score); - }) - .MaxBy(m => m.score); - - token.ThrowIfCancellationRequested(); - - BestSavedMacro = (bestSaved.macro, bestSaved.outState); - - token.ThrowIfCancellationRequested(); + var (resp, outState, failedIdx) = simulator.ExecuteMultiple(state, macro.Actions); + outState.ActionCount = macro.Actions.Count; + var score = SimulationNode.CalculateScoreForState(outState, simulator.CompletionState, mctsConfig) ?? 0; + if (resp != ActionResponse.SimulationComplete) + { + if (failedIdx != -1) + score /= 2; + } + return (macro, outState, score); + }) + .MaxBy(m => m.score); + + token.ThrowIfCancellationRequested(); + + BestSavedMacro = (bestSaved.macro, bestSaved.outState); + + token.ThrowIfCancellationRequested(); + } var solver = new Solver.Solver(config, state) { Token = token }; solver.OnLog += Log.Debug; @@ -819,10 +874,13 @@ private void CalculateBestMacrosTask(CancellationToken token) token.ThrowIfCancellationRequested(); BestSuggestedMacro = solution; + + token.ThrowIfCancellationRequested(); } public void Dispose() { + Service.WindowSystem.RemoveWindow(this); AxisFont?.Dispose(); } } diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index a41261e..1f341b2 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -1,38 +1,34 @@ using Craftimizer.Solver; -using Dalamud.Interface; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; +using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; using System; using System.Numerics; namespace Craftimizer.Plugin.Windows; -public class Settings : Window +public sealed class Settings : Window, IDisposable { + private const ImGuiWindowFlags WindowFlags = ImGuiWindowFlags.None; + private static Configuration Config => Service.Configuration; private const int OptionWidth = 200; private static Vector2 OptionButtonSize => new(OptionWidth, ImGui.GetFrameHeight()); - public const string TabGeneral = "General"; - public const string TabSimulator = "Simulator"; - public const string TabSynthHelper = "Synthesis Helper"; - public const string TabAbout = "About"; - private string? SelectedTab { get; set; } - public Settings() : base("Craftimizer Settings") + public Settings() : base("Craftimizer Settings", WindowFlags) { Service.WindowSystem.AddWindow(this); SizeConstraints = new WindowSizeConstraints() { - MinimumSize = new Vector2(400, 400), - MaximumSize = new Vector2(float.MaxValue, float.MaxValue) + MinimumSize = new(450, 400), + MaximumSize = new(float.PositiveInfinity) }; - Size = SizeConstraints.Value.MinimumSize; - SizeCondition = ImGuiCond.Appearing; } public void SelectTab(string label) @@ -40,16 +36,16 @@ public void SelectTab(string label) SelectedTab = label; } - private bool BeginTabItem(string label) + private ImRaii.IEndObject TabItem(string label) { var isSelected = string.Equals(SelectedTab, label, StringComparison.Ordinal); if (isSelected) { SelectedTab = null; var open = true; - return ImGui.BeginTabItem(label, ref open, ImGuiTabItemFlags.SetSelected); + return ImRaii.TabItem(label, ref open, ImGuiTabItemFlags.SetSelected); } - return ImGui.BeginTabItem(label); + return ImRaii.TabItem(label); } private static void DrawOption(string label, string tooltip, bool val, Action setter, ref bool isDirty) @@ -63,25 +59,44 @@ private static void DrawOption(string label, string tooltip, bool val, Action setter, ref bool isDirty) + private static void DrawOption(string label, string tooltip, T value, T min, T max, Action setter, ref bool isDirty) where T : struct, INumber { ImGui.SetNextItemWidth(OptionWidth); - if (ImGui.InputInt(label, ref val)) + var text = value.ToString(); + if (ImGui.InputText(label, ref text, 8, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CharsDecimal)) { - setter(val); - isDirty = true; + if (T.TryParse(text, null, out var newValue)) + { + newValue = T.Clamp(newValue, min, max); + if (value != newValue) + { + setter(newValue); + isDirty = true; + } + } } if (ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); } - private static void DrawOption(string label, string tooltip, float val, Action setter, ref bool isDirty) + private static void DrawOption(string label, string tooltip, Func getName, Func getTooltip, T value, Action setter, ref bool isDirty) where T : struct, Enum { ImGui.SetNextItemWidth(OptionWidth); - if (ImGui.InputFloat(label, ref val)) + using (var combo = ImRaii.Combo(label, getName(value))) { - setter(val); - isDirty = true; + if (combo) + { + foreach (var type in Enum.GetValues()) + { + if (ImGui.Selectable(getName(type), value.Equals(type))) + { + setter(type); + isDirty = true; + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip(getTooltip(type)); + } + } } if (ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); @@ -112,14 +127,33 @@ private static string GetAlgorithmTooltip(SolverAlgorithm algorithm) => _ => "Unknown" }; + private static string GetCopyTypeName(MacroCopyConfiguration.CopyType type) => + type switch + { + MacroCopyConfiguration.CopyType.OpenWindow => "Open a Window", + MacroCopyConfiguration.CopyType.CopyToMacro => "Copy to Macros", + MacroCopyConfiguration.CopyType.CopyToClipboard => "Copy to Clipboard", + _ => "Unknown", + }; + + private static string GetCopyTypeTooltip(MacroCopyConfiguration.CopyType type) => + type switch + { + MacroCopyConfiguration.CopyType.OpenWindow => "Open a dedicated window with all macros being copied.\n" + + "Copy, view, and choose at your own leisure.", + MacroCopyConfiguration.CopyType.CopyToMacro => "Copy directly to the game's macro system.", + MacroCopyConfiguration.CopyType.CopyToClipboard => "Copy to your clipboard. Macros are separated by a blank line.", + _ => "Unknown" + }; + public override void Draw() { if (ImGui.BeginTabBar("settingsTabBar")) { DrawTabGeneral(); DrawTabSimulator(); - if (Config.EnableSynthHelper) - DrawTabSynthHelper(); + //if (Config.EnableSynthHelper) + // DrawTabSynthHelper(); DrawTabAbout(); ImGui.EndTabBar(); @@ -128,31 +162,30 @@ public override void Draw() private void DrawTabGeneral() { - if (!BeginTabItem("General")) + using var tab = TabItem("General"); + if (!tab) return; ImGuiHelpers.ScaledDummy(5); var isDirty = false; - DrawOption( - "Override Uncraftability Warning", - "Allow simulation for crafts that otherwise wouldn't\n" + - "be able to be crafted with your current gear", - Config.OverrideUncraftability, - v => Config.OverrideUncraftability = v, - ref isDirty - ); - DrawOption( - "Enable Synthesis Helper", - "Adds a helper next to your synthesis window to help solve for the best craft.\n" + - "Extremely useful for expert recipes, where the condition can greatly affect\n" + - "which actions you take.", - Config.EnableSynthHelper, - v => Config.EnableSynthHelper = v, - ref isDirty - ); + using (var g = ImRaii.Group()) + { + using var d = ImRaii.Disabled(); + DrawOption( + "Enable Synthesis Helper", + "Adds a helper next to your synthesis window to help solve for the best craft.\n" + + "Extremely useful for expert recipes, where the condition can greatly affect\n" + + "which actions you take.", + Config.EnableSynthHelper, + v => Config.EnableSynthHelper = v, + ref isDirty + ); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Disabled temporarily for testing"); DrawOption( "Show Only One Macro Stat", @@ -164,10 +197,151 @@ ref isDirty ref isDirty ); + ImGuiHelpers.ScaledDummy(5); + + using (var panel = ImGuiUtils.GroupPanel("Copying Settings", -1, out _)) + { + DrawOption( + "Macro Copy Method", + "The method to copy a macro with.", + GetCopyTypeName, + GetCopyTypeTooltip, + Config.MacroCopy.Type, + v => Config.MacroCopy.Type = v, + ref isDirty + ); + + if (Config.MacroCopy.Type == MacroCopyConfiguration.CopyType.CopyToMacro) + { + DrawOption( + "Copy Downwards", + "Copy subsequent macros downward (#1 -> #11) instead of to the right.", + Config.MacroCopy.CopyDown, + v => Config.MacroCopy.CopyDown = v, + ref isDirty + ); + + DrawOption( + "Copy to Shared Macros", + "Copy to the shared macros tab. Leaving this unchecked copies to the\n" + + "individual tab.", + Config.MacroCopy.SharedMacro, + v => Config.MacroCopy.SharedMacro = v, + ref isDirty + ); + + DrawOption( + "Macro Number", + "The # of the macro to being copying to. Subsequent macros will be\n" + + "copied relative to this macro.", + Config.MacroCopy.StartMacroIdx, + 0, 99, + v => Config.MacroCopy.StartMacroIdx = v, + ref isDirty + ); + + DrawOption( + "Max Macro Copy Count", + "The maximum number of macros to be copied. Any more and a window is\n" + + "displayed with the rest of them.", + Config.MacroCopy.MaxMacroCount, + 1, 99, + v => Config.MacroCopy.MaxMacroCount = v, + ref isDirty + ); + } + + DrawOption( + "Use MacroChain's /nextmacro", + "Replaces the last step with /nextmacro to run the next macro\n" + + "automatically. Overrides Add End Notification except for the\n" + + "last macro.", + Config.MacroCopy.UseNextMacro, + v => Config.MacroCopy.UseNextMacro = v, + ref isDirty + ); + + DrawOption( + "Add Macro Lock", + "Adds /mlock to the beginning of every macro. Prevents other\n" + + "macros from being run.", + Config.MacroCopy.UseMacroLock, + v => Config.MacroCopy.UseMacroLock = v, + ref isDirty + ); + + DrawOption( + "Add Notification", + "Replaces the last step of every macro with a /echo notification.", + Config.MacroCopy.AddNotification, + v => Config.MacroCopy.AddNotification = v, + ref isDirty + ); + + if (Config.MacroCopy.AddNotification) + { + DrawOption( + "Add Notification Sound", + "Adds a sound to the end of every macro.", + Config.MacroCopy.AddNotificationSound, + v => Config.MacroCopy.AddNotificationSound = v, + ref isDirty + ); + + if (Config.MacroCopy.AddNotificationSound) + { + DrawOption( + "Intermediate Notification Sound", + "Ending notification sound for an intermediary macro.\n" + + "Uses ", + Config.MacroCopy.IntermediateNotificationSound, + 1, 16, + v => + { + Config.MacroCopy.IntermediateNotificationSound = v; + UIModule.PlayChatSoundEffect((uint)v); + }, + ref isDirty + ); + + DrawOption( + "Final Notification Sound", + "Ending notification sound for the final macro.\n" + + "Uses ", + Config.MacroCopy.EndNotificationSound, + 1, 16, + v => + { + Config.MacroCopy.EndNotificationSound = v; + UIModule.PlayChatSoundEffect((uint)v); + }, + ref isDirty + ); + } + } + + if (Config.MacroCopy.Type != MacroCopyConfiguration.CopyType.CopyToMacro) + { + DrawOption( + "Remove Wait Times", + "Remove at the end of every action. Useful for SomethingNeedDoing.", + Config.MacroCopy.RemoveWaitTimes, + v => Config.MacroCopy.RemoveWaitTimes = v, + ref isDirty + ); + + DrawOption( + "Combine Macro", + "Doesn't split the macro into smaller macros. Useful for SomethingNeedDoing.", + Config.MacroCopy.CombineMacro, + v => Config.MacroCopy.CombineMacro = v, + ref isDirty + ); + } + } + if (isDirty) Config.Save(); - - ImGui.EndTabItem(); } private static void DrawSolverConfig(ref SolverConfig configRef, SolverConfig defaultConfig, out bool isDirty) @@ -176,228 +350,240 @@ private static void DrawSolverConfig(ref SolverConfig configRef, SolverConfig de var config = configRef; - ImGuiUtils.BeginGroupPanel("General"); - - if (ImGui.Button("Reset to Default", OptionButtonSize)) - { - config = defaultConfig; - isDirty = true; - } - - ImGui.SetNextItemWidth(OptionWidth); - if (ImGui.BeginCombo("Algorithm", GetAlgorithmName(config.Algorithm))) + using (var panel = ImGuiUtils.GroupPanel("General", -1, out _)) { - foreach (var alg in Enum.GetValues()) + if (ImGui.Button("Reset to Default", OptionButtonSize)) { - if (ImGui.Selectable(GetAlgorithmName(alg), config.Algorithm == alg)) - { - config = config with { Algorithm = alg }; - isDirty = true; - } - if (ImGui.IsItemHovered()) - ImGui.SetTooltip(GetAlgorithmTooltip(alg)); + config = defaultConfig; + isDirty = true; } - ImGui.EndCombo(); - } - if (ImGui.IsItemHovered()) - ImGui.SetTooltip( + + DrawOption( + "Algorithm", "The algorithm to use when solving for a macro. Different\n" + "algorithms provide different pros and cons for using them.\n" + "By far, the Stepwise Furcated algorithm provides the best\n" + - "results, especially for very difficult crafts." + "results, especially for very difficult crafts.", + GetAlgorithmName, + GetAlgorithmTooltip, + config.Algorithm, + v => config = config with { Algorithm = v }, + ref isDirty ); - DrawOption( - "Iterations", - "The total number of iterations to run per crafting step.\n" + - "Higher values require more computational power. Higher values\n" + - "also may decrease variance, so other values should be tweaked\n" + - "as necessary to get a more favorable outcome.", - config.Iterations, - v => config = config with { Iterations = v }, - ref isDirty - ); - - DrawOption( - "Max Step Count", - "The maximum number of crafting steps; this is generally the only\n" + - "setting you should change, and it should be set to around 5 steps\n" + - "more than what you'd expect. If this value is too low, the solver\n" + - "won't learn much per iteration; too high and it will waste time\n" + - "on useless extra steps.", - config.MaxStepCount, - v => config = config with { MaxStepCount = v }, - ref isDirty - ); - - DrawOption( - "Exploration Constant", - "A constant that decides how often the solver will explore new,\n" + - "possibly good paths. If this value is too high,\n" + - "moves will mostly be decided at random.", - config.ExplorationConstant, - v => config = config with { ExplorationConstant = v }, - ref isDirty - ); - - DrawOption( - "Score Weighting Constant", - "A constant ranging from 0 to 1 that configures how the solver\n" + - "scores and picks paths to travel to next. A value of 0 means\n" + - "actions will be chosen based on their average outcome, whereas\n" + - "1 uses their best outcome achieved so far.", - config.MaxScoreWeightingConstant, - v => config = config with { MaxScoreWeightingConstant = v }, - ref isDirty - ); - - ImGui.BeginDisabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated)); - DrawOption( - "Max Core Count", - "The number of cores to use when solving. You should use as many\n" + - "as you can. If it's too high, it will have an effect on your gameplay\n" + - $"experience. A good estimate would be 1 or 2 cores less than your\n" + - $"system (FYI, you have {Environment.ProcessorCount} cores,) but\n" + - $"make sure to accomodate for any other tasks you have in the\n" + - $"background, if you have any.\n" + - "(Only used in the Forked and Furcated algorithms)", - config.MaxThreadCount, - v => config = config with { MaxThreadCount = v }, - ref isDirty - ); - ImGui.EndDisabled(); - - ImGui.BeginDisabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated)); - DrawOption( - "Fork Count", - "Split the number of iterations across different solvers. In general,\n" + - "you should increase this value to at least the number of cores in\n" + - $"your system (FYI, you have {Environment.ProcessorCount} cores) to attain the most speedup.\n" + - "The higher the number, the more chance you have of finding a\n" + - "better local maximum; this concept similar but not equivalent\n" + - "to the exploration constant.\n" + - "(Only used in the Forked and Furcated algorithms)", - config.ForkCount, - v => config = config with { ForkCount = v }, - ref isDirty - ); - ImGui.EndDisabled(); - - ImGui.BeginDisabled(config.Algorithm is not SolverAlgorithm.StepwiseFurcated); - DrawOption( - "Furcated Action Count", - "On every craft step, pick this many top solutions and use them as\n" + - "the input for the next craft step. For best results, use Fork Count / 2\n" + - "and add about 1 or 2 more if needed.\n" + - "(Only used in the Stepwise Furcated algorithm)", - config.FurcatedActionCount, - v => config = config with { FurcatedActionCount = v }, - ref isDirty - ); - ImGui.EndDisabled(); + DrawOption( + "Iterations", + "The total number of iterations to run per crafting step.\n" + + "Higher values require more computational power. Higher values\n" + + "also may decrease variance, so other values should be tweaked\n" + + "as necessary to get a more favorable outcome.", + config.Iterations, + 1000, + 500000, + v => config = config with { Iterations = v }, + ref isDirty + ); - ImGuiUtils.EndGroupPanel(); + DrawOption( + "Max Step Count", + "The maximum number of crafting steps; this is generally the only\n" + + "setting you should change, and it should be set to around 5 steps\n" + + "more than what you'd expect. If this value is too low, the solver\n" + + "won't learn much per iteration; too high and it will waste time\n" + + "on useless extra steps.", + config.MaxStepCount, + 1, + 100, + v => config = config with { MaxStepCount = v }, + ref isDirty + ); - ImGuiUtils.BeginGroupPanel("Advanced"); + DrawOption( + "Exploration Constant", + "A constant that decides how often the solver will explore new,\n" + + "possibly good paths. If this value is too high,\n" + + "moves will mostly be decided at random.", + config.ExplorationConstant, + 0, + 10, + v => config = config with { ExplorationConstant = v }, + ref isDirty + ); - DrawOption( - "Score Storage Threshold", - "If a craft achieves this certain arbitrary score, the solver will\n" + - "throw away all other possible combinations in favor of that one.\n" + - "Only change this value if you absolutely know what you're doing.", - config.ScoreStorageThreshold, - v => config = config with { ScoreStorageThreshold = v }, - ref isDirty - ); + DrawOption( + "Score Weighting Constant", + "A constant ranging from 0 to 1 that configures how the solver\n" + + "scores and picks paths to travel to next. A value of 0 means\n" + + "actions will be chosen based on their average outcome, whereas\n" + + "1 uses their best outcome achieved so far.", + config.MaxScoreWeightingConstant, + 0, + 1, + v => config = config with { MaxScoreWeightingConstant = v }, + ref isDirty + ); - DrawOption( - "Max Rollout Step Count", - "The maximum number of crafting steps every iteration can consider.\n" + - "Decreasing this value can have unintended side effects. Only change\n" + - "this value if you absolutely know what you're doing.", - config.MaxRolloutStepCount, - v => config = config with { MaxRolloutStepCount = v }, - ref isDirty - ); + using (var d = ImRaii.Disabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated))) + DrawOption( + "Max Core Count", + "The number of cores to use when solving. You should use as many\n" + + "as you can. If it's too high, it will have an effect on your gameplay\n" + + $"experience. A good estimate would be 1 or 2 cores less than your\n" + + $"system (FYI, you have {Environment.ProcessorCount} cores), but make sure to accomodate\n" + + $"for any other tasks you have in the background, if you have any.\n" + + "(Only used in the Forked and Furcated algorithms)", + config.MaxThreadCount, + 1, + Environment.ProcessorCount, + v => config = config with { MaxThreadCount = v }, + ref isDirty + ); + + using (var d = ImRaii.Disabled(config.Algorithm is not (SolverAlgorithm.OneshotForked or SolverAlgorithm.StepwiseForked or SolverAlgorithm.StepwiseFurcated))) + DrawOption( + "Fork Count", + "Split the number of iterations across different solvers. In general,\n" + + "you should increase this value to at least the number of cores in\n" + + $"your system (FYI, you have {Environment.ProcessorCount} cores) to attain the most speedup.\n" + + "The higher the number, the more chance you have of finding a\n" + + "better local maximum; this concept similar but not equivalent\n" + + "to the exploration constant.\n" + + "(Only used in the Forked and Furcated algorithms)", + config.ForkCount, + 1, + 500, + v => config = config with { ForkCount = v }, + ref isDirty + ); + + using (var d = ImRaii.Disabled(config.Algorithm is not SolverAlgorithm.StepwiseFurcated)) + DrawOption( + "Furcated Action Count", + "On every craft step, pick this many top solutions and use them as\n" + + "the input for the next craft step. For best results, use Fork Count / 2\n" + + "and add about 1 or 2 more if needed.\n" + + "(Only used in the Stepwise Furcated algorithm)", + config.FurcatedActionCount, + 1, + 500, + v => config = config with { FurcatedActionCount = v }, + ref isDirty + ); + } - DrawOption( - "Strict Actions", - "When finding the next possible actions to execute, use a heuristic\n" + - "to restrict which actions to attempt taking. This results in a much\n" + - "better macro at the cost of not finding an extremely creative one.", - config.StrictActions, - v => config = config with { StrictActions = v }, - ref isDirty - ); + using (var panel = ImGuiUtils.GroupPanel("Advanced", -1, out _)) + { + DrawOption( + "Score Storage Threshold", + "If a craft achieves this certain arbitrary score, the solver will\n" + + "throw away all other possible combinations in favor of that one.\n" + + "Only change this value if you absolutely know what you're doing.", + config.ScoreStorageThreshold, + 0, + 1, + v => config = config with { ScoreStorageThreshold = v }, + ref isDirty + ); - ImGuiUtils.EndGroupPanel(); + DrawOption( + "Max Rollout Step Count", + "The maximum number of crafting steps every iteration can consider.\n" + + "Decreasing this value can have unintended side effects. Only change\n" + + "this value if you absolutely know what you're doing.", + config.MaxRolloutStepCount, + 1, + 50, + v => config = config with { MaxRolloutStepCount = v }, + ref isDirty + ); - ImGuiUtils.BeginGroupPanel("Score Weights (Advanced)"); - ImGui.TextWrapped("All values should add up to 1. Otherwise, the Score Storage Threshold should be changed."); - ImGuiHelpers.ScaledDummy(10); + DrawOption( + "Strict Actions", + "When finding the next possible actions to execute, use a heuristic\n" + + "to restrict which actions to attempt taking. This results in a much\n" + + "better macro at the cost of not finding an extremely creative one.", + config.StrictActions, + v => config = config with { StrictActions = v }, + ref isDirty + ); + } - DrawOption( - "Progress", - "Amount of weight to give to the craft's progress.", - config.ScoreProgress, - v => config = config with { ScoreProgress = v }, - ref isDirty - ); + using (var panel = ImGuiUtils.GroupPanel("Score Weights (Advanced)", -1, out _)) + { + ImGui.TextWrapped("All values should add up to 1. Otherwise, the Score Storage Threshold should be changed."); + ImGuiHelpers.ScaledDummy(10); + + DrawOption( + "Progress", + "Amount of weight to give to the craft's progress.", + config.ScoreProgress, + 0, + 1, + v => config = config with { ScoreProgress = v }, + ref isDirty + ); - DrawOption( - "Quality", - "Amount of weight to give to the craft's quality.", - config.ScoreQuality, - v => config = config with { ScoreQuality = v }, - ref isDirty - ); + DrawOption( + "Quality", + "Amount of weight to give to the craft's quality.", + config.ScoreQuality, + 0, + 1, + v => config = config with { ScoreQuality = v }, + ref isDirty + ); - DrawOption( - "Durability", - "Amount of weight to give to the craft's remaining durability.", - config.ScoreDurability, - v => config = config with { ScoreDurability = v }, - ref isDirty - ); + DrawOption( + "Durability", + "Amount of weight to give to the craft's remaining durability.", + config.ScoreDurability, + 0, + 1, + v => config = config with { ScoreDurability = v }, + ref isDirty + ); - DrawOption( - "CP", - "Amount of weight to give to the craft's remaining CP.", - config.ScoreCP, - v => config = config with { ScoreCP = v }, - ref isDirty - ); + DrawOption( + "CP", + "Amount of weight to give to the craft's remaining CP.", + config.ScoreCP, + 0, + 1, + v => config = config with { ScoreCP = v }, + ref isDirty + ); - DrawOption( - "Steps", - "Amount of weight to give to the craft's number of steps. The lower\n" + - "the step count, the higher the score.", - config.ScoreSteps, - v => config = config with { ScoreSteps = v }, - ref isDirty - ); + DrawOption( + "Steps", + "Amount of weight to give to the craft's number of steps. The lower\n" + + "the step count, the higher the score.", + config.ScoreSteps, + 0, + 1, + v => config = config with { ScoreSteps = v }, + ref isDirty + ); - if (ImGui.Button("Normalize Weights", OptionButtonSize)) - { - var total = config.ScoreProgress + - config.ScoreQuality + - config.ScoreDurability + - config.ScoreCP + - config.ScoreSteps; - config = config with + if (ImGui.Button("Normalize Weights", OptionButtonSize)) { - ScoreProgress = config.ScoreProgress / total, - ScoreQuality = config.ScoreQuality / total, - ScoreDurability = config.ScoreDurability / total, - ScoreCP = config.ScoreCP / total, - ScoreSteps = config.ScoreSteps / total - }; - isDirty = true; + var total = config.ScoreProgress + + config.ScoreQuality + + config.ScoreDurability + + config.ScoreCP + + config.ScoreSteps; + config = config with + { + ScoreProgress = config.ScoreProgress / total, + ScoreQuality = config.ScoreQuality / total, + ScoreDurability = config.ScoreDurability / total, + ScoreCP = config.ScoreCP / total, + ScoreSteps = config.ScoreSteps / total + }; + isDirty = true; + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Normalize all weights to sum up to 1"); } - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Normalize all weights to sum up to 1"); - - ImGuiUtils.EndGroupPanel(); if (isDirty) configRef = config; @@ -405,30 +591,28 @@ ref isDirty private void DrawTabSimulator() { - if (!BeginTabItem("Simulator")) + using var tab = TabItem("Simulator"); + if (!tab) return; ImGuiHelpers.ScaledDummy(5); var isDirty = false; - DrawOption( - "Show Only Learned Actions", - "Don't show crafting actions that haven't been\n" + - "learned yet with your current job on the simulator sidebar", - Config.HideUnlearnedActions, - v => Config.HideUnlearnedActions = v, - ref isDirty - ); - - DrawOption( - "Condition Randomness", - "Allows the simulator condition to fluctuate randomly like a real craft.\n" + - "Turns off when generating a macro.", - Config.ConditionRandomness, - v => Config.ConditionRandomness = v, - ref isDirty - ); + using (var g = ImRaii.Group()) + { + using var d = ImRaii.Disabled(); + DrawOption( + "Condition Randomness", + "Allows the simulator condition to fluctuate randomly like a real craft.\n" + + "Turns off when generating a macro.", + Config.ConditionRandomness, + v => Config.ConditionRandomness = v, + ref isDirty + ); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Disabled temporarily for testing"); ImGuiHelpers.ScaledDummy(5); ImGui.Separator(); @@ -444,13 +628,12 @@ ref isDirty if (isDirty) Config.Save(); - - ImGui.EndTabItem(); } private void DrawTabSynthHelper() { - if (!BeginTabItem("Synthesis Helper")) + using var tab = TabItem("Synthesis Helper"); + if (!tab) return; ImGuiHelpers.ScaledDummy(5); @@ -461,6 +644,8 @@ private void DrawTabSynthHelper() "Step Count", "The number of future actions to solve for during an in-game craft.", Config.SynthHelperStepCount, + 1, + 100, v => Config.SynthHelperStepCount = v, ref isDirty ); @@ -479,13 +664,12 @@ ref isDirty if (isDirty) Config.Save(); - - ImGui.EndTabItem(); } private void DrawTabAbout() { - if (!BeginTabItem("About")) + using var tab = TabItem("About"); + if (!tab) return; ImGuiHelpers.ScaledDummy(5); @@ -493,28 +677,34 @@ private void DrawTabAbout() var plugin = Service.Plugin; var icon = plugin.Icon; - ImGui.BeginTable("settingsAboutTable", 2); - ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, icon.Width); - - ImGui.TableNextColumn(); - ImGui.Image(icon.ImGuiHandle, new(icon.Width, icon.Height)); - - ImGui.TableNextColumn(); - ImGui.Text($"{plugin.Name} v{plugin.Version} {plugin.BuildConfiguration}"); - ImGui.Text($"By {plugin.Author} ("); - ImGui.SameLine(0, 0); - ImGuiUtils.Hyperlink("WorkingRobot", "https://github.com/WorkingRobot"); - ImGui.SameLine(0, 0); - ImGui.Text(")"); - - ImGui.EndTable(); + using (var table = ImRaii.Table("settingsAboutTable", 2)) + { + if (table) + { + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, icon.Width); + + ImGui.TableNextColumn(); + ImGui.Image(icon.ImGuiHandle, new(icon.Width, icon.Height)); + + ImGui.TableNextColumn(); + ImGui.Text($"Craftimizer v{plugin.Version} {plugin.BuildConfiguration}"); + ImGui.Text($"By {plugin.Author} ("); + ImGui.SameLine(0, 0); + ImGuiUtils.Hyperlink("WorkingRobot", "https://github.com/WorkingRobot"); + ImGui.SameLine(0, 0); + ImGui.Text(")"); + } + } ImGui.Text("Credit to altosock's "); ImGui.SameLine(0, 0); ImGuiUtils.Hyperlink("Craftingway", "https://craftingway.app"); ImGui.SameLine(0, 0); ImGui.Text(" for the original solver algorithm"); + } - ImGui.EndTabItem(); + public void Dispose() + { + Service.WindowSystem.RemoveWindow(this); } } diff --git a/Simulator/ActionCategory.cs b/Simulator/ActionCategory.cs index 24d671b..56ce764 100644 --- a/Simulator/ActionCategory.cs +++ b/Simulator/ActionCategory.cs @@ -1,3 +1,6 @@ +using Craftimizer.Simulator.Actions; +using System.Collections.ObjectModel; + namespace Craftimizer.Simulator; public enum ActionCategory @@ -13,6 +16,25 @@ public enum ActionCategory public static class ActionCategoryUtils { + private static readonly ReadOnlyDictionary SortedActions; + + static ActionCategoryUtils() + { + SortedActions = new( + Enum.GetValues() + .Where(a => a.Category() != ActionCategory.Combo) + .GroupBy(a => a.Category()) + .ToDictionary(g => g.Key, g => g.OrderBy(a => a.Level()).ToArray())); + } + + public static IReadOnlyList GetActions(this ActionCategory me) + { + if (SortedActions.TryGetValue(me, out var actions)) + return actions; + + throw new ArgumentException($"Unknown action category {me}", nameof(me)); + } + public static string GetDisplayName(this ActionCategory category) => category switch { diff --git a/Simulator/Actions/BaseAction.cs b/Simulator/Actions/BaseAction.cs index 47e736f..8b3edb1 100644 --- a/Simulator/Actions/BaseAction.cs +++ b/Simulator/Actions/BaseAction.cs @@ -48,7 +48,8 @@ public virtual void Use(Simulator s) s.ActionStates.MutateState(this); s.ActionCount++; - s.ActiveEffects.DecrementDuration(); + if (IncreasesStepCount) + s.ActiveEffects.DecrementDuration(); } public virtual void UseSuccess(Simulator s) diff --git a/Simulator/Actions/BaseBuffAction.cs b/Simulator/Actions/BaseBuffAction.cs index 09458b6..dbabd50 100644 --- a/Simulator/Actions/BaseBuffAction.cs +++ b/Simulator/Actions/BaseBuffAction.cs @@ -14,10 +14,13 @@ internal abstract class BaseBuffAction : BaseAction public override void UseSuccess(Simulator s) => s.AddEffect(Effect, Duration); - public sealed override string GetTooltip(Simulator s, bool addUsability) + public override string GetTooltip(Simulator s, bool addUsability) { var builder = new StringBuilder(base.GetTooltip(s, addUsability)); builder.AppendLine($"{Duration} Steps"); return builder.ToString(); } + + protected string GetBaseTooltip(Simulator s, bool addUsability) => + base.GetTooltip(s, addUsability); } diff --git a/Simulator/Actions/CarefulObservation.cs b/Simulator/Actions/CarefulObservation.cs index 67ee00f..ebee287 100644 --- a/Simulator/Actions/CarefulObservation.cs +++ b/Simulator/Actions/CarefulObservation.cs @@ -15,4 +15,7 @@ internal sealed class CarefulObservation : BaseAction public override bool CanUse(Simulator s) => s.Input.Stats.IsSpecialist && s.ActionStates.CarefulObservationCount < 3; public override void UseSuccess(Simulator s) => s.StepCondition(); + + public override string GetTooltip(Simulator s, bool addUsability) => + $"{base.GetTooltip(s, addUsability)}Specialist Only"; } diff --git a/Simulator/Actions/HeartAndSoul.cs b/Simulator/Actions/HeartAndSoul.cs index 9e662ca..16e7b1a 100644 --- a/Simulator/Actions/HeartAndSoul.cs +++ b/Simulator/Actions/HeartAndSoul.cs @@ -14,4 +14,7 @@ internal sealed class HeartAndSoul : BaseBuffAction public override int CPCost(Simulator s) => 0; public override bool CanUse(Simulator s) => s.Input.Stats.IsSpecialist && !s.ActionStates.UsedHeartAndSoul; + + public override string GetTooltip(Simulator s, bool addUsability) => + $"{GetBaseTooltip(s, addUsability)}Specialist Only"; } diff --git a/Simulator/Actions/Manipulation.cs b/Simulator/Actions/Manipulation.cs index 4b0e6ba..804ee65 100644 --- a/Simulator/Actions/Manipulation.cs +++ b/Simulator/Actions/Manipulation.cs @@ -14,14 +14,16 @@ internal sealed class Manipulation : BaseBuffAction public override void Use(Simulator s) { - if (s.HasEffect(EffectType.Manipulation)) - s.RestoreDurability(5); + UseSuccess(s); s.ReduceCP(CPCost(s)); - s.ReduceDurability(DurabilityCost); - - UseSuccess(s); s.IncreaseStepCount(); + + s.ActionStates.MutateState(this); + s.ActionCount++; + + if (IncreasesStepCount) + s.ActiveEffects.DecrementDuration(); } } diff --git a/Simulator/Actions/TrainedEye.cs b/Simulator/Actions/TrainedEye.cs index 94c9b08..494fb0c 100644 --- a/Simulator/Actions/TrainedEye.cs +++ b/Simulator/Actions/TrainedEye.cs @@ -18,4 +18,7 @@ public override bool CanUse(Simulator s) => public override void UseSuccess(Simulator s) => s.IncreaseQualityRaw(s.Input.Recipe.MaxQuality - s.Quality); + + public override string GetTooltip(Simulator s, bool addUsability) => + $"{base.GetTooltip(s, addUsability)}+{s.Input.Recipe.MaxQuality - s.Quality} Quality"; } diff --git a/Simulator/Effects.cs b/Simulator/Effects.cs index 49cf0b4..6740e16 100644 --- a/Simulator/Effects.cs +++ b/Simulator/Effects.cs @@ -82,6 +82,11 @@ public readonly byte GetDuration(EffectType effect) => _ => 0 }; + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsIndefinite(EffectType effect) => + effect is EffectType.InnerQuiet or EffectType.HeartAndSoul; + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly byte GetStrength(EffectType effect) => diff --git a/Simulator/SimulationState.cs b/Simulator/SimulationState.cs index 6b9182f..e743ade 100644 --- a/Simulator/SimulationState.cs +++ b/Simulator/SimulationState.cs @@ -25,6 +25,8 @@ public record struct SimulationState 74, 76, 78, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 94, 96, 98, 100 }; public readonly int HQPercent => HQPercentTable[(int)Math.Clamp((float)Quality / Input.Recipe.MaxQuality * 100, 0, 100)]; + public readonly int Collectability => Math.Max(Quality / 10, 1); + public readonly int MaxCollectability => Math.Max(Input.Recipe.MaxQuality / 10, 1); public readonly bool IsFirstStep => StepCount == 0; diff --git a/Simulator/Simulator.cs b/Simulator/Simulator.cs index 849c288..d108e4c 100644 --- a/Simulator/Simulator.cs +++ b/Simulator/Simulator.cs @@ -63,6 +63,8 @@ private ActionResponse Execute(ActionType action) return ActionResponse.ActionNotUnlocked; if (action == ActionType.Manipulation && !Input.Stats.CanUseManipulation) return ActionResponse.ActionNotUnlocked; + if (action is ActionType.CarefulObservation or ActionType.HeartAndSoul && !Input.Stats.IsSpecialist) + return ActionResponse.ActionNotUnlocked; if (baseAction.CPCost(this) > CP) return ActionResponse.NotEnoughCP; return ActionResponse.CannotUseAction; diff --git a/Solver/Solver.cs b/Solver/Solver.cs index 3dd5e26..0cb8cec 100644 --- a/Solver/Solver.cs +++ b/Solver/Solver.cs @@ -35,9 +35,6 @@ public sealed class Solver : IDisposable // Always called when a new step is generated. public event NewActionDelegate? OnNewAction; - // Always called when the solver is fully complete. - public event SolutionDelegate? OnSolution; - public Solver(SolverConfig config, SimulationState state) { Config = config; @@ -107,6 +104,12 @@ public void Dispose() CompletionTask?.Dispose(); } + private void InvokeNewAction(ActionType action) + { + foreach (var sanitizedAction in SolverSolution.SanitizeCombo(action)) + OnNewAction?.Invoke(sanitizedAction); + } + private async Task SearchStepwiseFurcated() { var definiteActionCount = 0; @@ -115,7 +118,7 @@ private async Task SearchStepwiseFurcated() var state = State; var sim = new Simulator(state, Config.MaxStepCount); - var activeStates = new List() { new(new(), state) }; + var activeStates = new List() { new(Array.Empty(), state) }; while (activeStates.Count != 0) { @@ -162,12 +165,12 @@ private async Task SearchStepwiseFurcated() if (bestAction.MaxScore >= Config.ScoreStorageThreshold) { var (_, furcatedActionIdx, solution) = bestAction; - var (activeActions, _) = activeStates[furcatedActionIdx]; + (IEnumerable activeActions, _) = activeStates[furcatedActionIdx]; - activeActions.AddRange(solution.Actions); + activeActions = activeActions.Concat(solution.Actions); foreach (var action in activeActions.Skip(definiteActionCount)) - OnNewAction?.Invoke(action); - return solution with { Actions = activeActions }; + InvokeNewAction(action); + return solution with { ActionEnumerable = activeActions }; } var newStates = new List(Config.FurcatedActionCount); @@ -214,7 +217,7 @@ private async Task SearchStepwiseFurcated() if (definiteCount != equalCount) { foreach(var action in refActions.Take(equalCount).Skip(definiteCount)) - OnNewAction?.Invoke(action); + InvokeNewAction(action); definiteActionCount = equalCount; } @@ -224,11 +227,11 @@ private async Task SearchStepwiseFurcated() } if (bestSims.Count == 0) - return new(new(), state); + return new(Array.Empty(), state); var result = bestSims.MaxBy(s => s.Score).Result; foreach (var action in result.Actions.Skip(definiteActionCount)) - OnNewAction?.Invoke(action); + InvokeNewAction(action); return result; } @@ -282,12 +285,12 @@ private async Task SearchStepwiseForked() { actions.AddRange(solution.Actions); foreach (var action in solution.Actions) - OnNewAction?.Invoke(action); + InvokeNewAction(action); return solution with { Actions = actions }; } var chosenAction = solution.Actions[0]; - OnNewAction?.Invoke(chosenAction); + InvokeNewAction(chosenAction); (_, state) = sim.Execute(state, chosenAction); actions.Add(chosenAction); @@ -321,12 +324,12 @@ private Task SearchStepwise() { actions.AddRange(solution.Actions); foreach (var action in solution.Actions) - OnNewAction?.Invoke(action); + InvokeNewAction(action); return Task.FromResult(solution with { Actions = actions }); } var chosenAction = solution.Actions[0]; - OnNewAction?.Invoke(chosenAction); + InvokeNewAction(chosenAction); (_, state) = sim.Execute(state, chosenAction); actions.Add(chosenAction); @@ -365,7 +368,7 @@ private async Task SearchOneshotForked() var solution = tasks.Select(t => t.Result).MaxBy(r => r.MaxScore).Solution; foreach (var action in solution.Actions) - OnNewAction?.Invoke(action); + InvokeNewAction(action); return solution; } @@ -376,7 +379,7 @@ private Task SearchOneshot() solver.Search(Config.Iterations, Token); var solution = solver.Solution(); foreach (var action in solution.Actions) - OnNewAction?.Invoke(action); + InvokeNewAction(action); return Task.FromResult(solution); } diff --git a/Solver/SolverSolution.cs b/Solver/SolverSolution.cs index 8a5ac54..f512479 100644 --- a/Solver/SolverSolution.cs +++ b/Solver/SolverSolution.cs @@ -3,4 +3,43 @@ namespace Craftimizer.Solver; -public readonly record struct SolverSolution(List Actions, SimulationState State); +public readonly record struct SolverSolution { + private readonly List actions = null!; + public readonly IReadOnlyList Actions { get => actions; init => ActionEnumerable = value; } + public readonly IEnumerable ActionEnumerable { init => actions = SanitizeCombos(value).ToList(); } + public readonly SimulationState State { get; init; } + + public SolverSolution(IEnumerable actions, SimulationState state) + { + ActionEnumerable = actions; + State = state; + } + + public void Deconstruct(out IReadOnlyList actions, out SimulationState state) + { + actions = Actions; + state = State; + } + + internal static IEnumerable SanitizeCombo(ActionType action) + { + if (action.Base() is BaseComboAction combo) + { + foreach (var a in SanitizeCombo(combo.ActionTypeA)) + yield return a; + foreach (var b in SanitizeCombo(combo.ActionTypeB)) + yield return b; + } + else + yield return action; + } + + internal static IEnumerable SanitizeCombos(IEnumerable actions) + { + foreach (var action in actions) + { + foreach (var sanitizedAction in SanitizeCombo(action)) + yield return sanitizedAction; + } + } +} From 6f5c6b66fb145e908d6a7760ee36013f29876c12 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 17 Oct 2023 04:33:44 -0700 Subject: [PATCH 18/58] Fix internal name --- Craftimizer/Craftimizer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Craftimizer/Craftimizer.json b/Craftimizer/Craftimizer.json index 6acd17f..15d1e92 100644 --- a/Craftimizer/Craftimizer.json +++ b/Craftimizer/Craftimizer.json @@ -4,7 +4,7 @@ "Punchline": "Simulate crafts and create computer-assisted macros from the comfort of your own game", "Description": "Allows you to generate macros and simulate all sorts of crafts without having to open another app. Open your crafting log to get started.", "RepoUrl": "https://github.com/WorkingRobot/craftimizer", - "InternalName": "craftimizer", + "InternalName": "Craftimizer", "ApplicableVersion": "any", "Tags": [ "crafting", From 8db4007ed9c93b3259f9656075f8a05eefe7fc7a Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 17 Oct 2023 04:34:03 -0700 Subject: [PATCH 19/58] Fix /crafteditor command --- Craftimizer/Plugin.cs | 39 +++++++++++++++++++++++++++++++- Craftimizer/Windows/MacroList.cs | 23 +++---------------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/Craftimizer/Plugin.cs b/Craftimizer/Plugin.cs index dea4338..414f2b6 100644 --- a/Craftimizer/Plugin.cs +++ b/Craftimizer/Plugin.cs @@ -11,6 +11,7 @@ using Dalamud.Plugin; using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; namespace Craftimizer.Plugin; @@ -69,12 +70,48 @@ public Plugin([RequiredVersion("1.0")] DalamudPluginInterface pluginInterface) { HelpMessage = "Open the crafting macros window.", }); - Service.CommandManager.AddHandler("/crafteditor", new CommandInfo((_, _) => OpenSettingsWindow()) + Service.CommandManager.AddHandler("/crafteditor", new CommandInfo((_, _) => OpenEmptyMacroEditor()) { HelpMessage = "Open the crafting macro editor.", }); } + public (CharacterStats? Character, RecipeData? Recipe, MacroEditor.CrafterBuffs? Buffs) GetOpenedStats() + { + var editorWindow = (EditorWindow?.IsOpen ?? false) ? EditorWindow : null; + var recipeData = editorWindow?.RecipeData ?? Service.Plugin.RecipeNoteWindow.RecipeData; + var characterStats = editorWindow?.CharacterStats ?? Service.Plugin.RecipeNoteWindow.CharacterStats; + var buffs = editorWindow?.Buffs ?? (RecipeNoteWindow.CharacterStats != null ? new(Service.ClientState.LocalPlayer?.StatusList) : null); + + return (characterStats, recipeData, buffs); + } + + public (CharacterStats Character, RecipeData Recipe, MacroEditor.CrafterBuffs Buffs) GetDefaultStats() + { + var stats = GetOpenedStats(); + return ( + stats.Character ?? new() + { + Craftsmanship = 100, + Control = 100, + CP = 200, + Level = 10, + CanUseManipulation = false, + HasSplendorousBuff = false, + IsSpecialist = false, + CLvl = 10, + }, + stats.Recipe ?? new(1023), + stats.Buffs ?? new(null) + ); + } + + public void OpenEmptyMacroEditor() + { + var stats = GetDefaultStats(); + OpenMacroEditor(stats.Character, stats.Recipe, stats.Buffs, Enumerable.Empty(), null); + } + public void OpenMacroEditor(CharacterStats characterStats, RecipeData recipeData, MacroEditor.CrafterBuffs buffs, IEnumerable actions, Action>? setter) { EditorWindow?.Dispose(); diff --git a/Craftimizer/Windows/MacroList.cs b/Craftimizer/Windows/MacroList.cs index 101e169..a63d503 100644 --- a/Craftimizer/Windows/MacroList.cs +++ b/Craftimizer/Windows/MacroList.cs @@ -21,7 +21,6 @@ public sealed class MacroList : Window, IDisposable public CharacterStats? CharacterStats { get; private set; } public RecipeData? RecipeData { get; private set; } - private MacroEditor? EditorWindow { get; set; } private IReadOnlyList Macros => Service.Configuration.Macros; private Dictionary MacroStateCache { get; } = new(); @@ -51,10 +50,7 @@ public override void PreDraw() var oldCharacterStats = CharacterStats; var oldRecipeData = RecipeData; - EditorWindow = Service.Plugin.EditorWindow; - EditorWindow = (EditorWindow?.IsOpen ?? false) ? EditorWindow : null; - RecipeData = EditorWindow?.RecipeData ?? Service.Plugin.RecipeNoteWindow.RecipeData; - CharacterStats = EditorWindow?.CharacterStats ?? Service.Plugin.RecipeNoteWindow.CharacterStats; + (CharacterStats, RecipeData, _) = Service.Plugin.GetOpenedStats(); if (oldCharacterStats != CharacterStats || oldRecipeData != RecipeData) RecalculateStats(); @@ -310,21 +306,8 @@ private void RefreshSearch() private void OpenEditor(Macro? macro) { - var character = CharacterStats ?? new() - { - Craftsmanship = 100, - Control = 100, - CP = 200, - Level = 10, - CanUseManipulation = false, - HasSplendorousBuff = false, - IsSpecialist = false, - CLvl = 10, - }; - var recipe = RecipeData ?? new(1023); - - var buffs = EditorWindow?.Buffs ?? new(Service.Plugin.RecipeNoteWindow.CharacterStats != null ? Service.ClientState.LocalPlayer?.StatusList : null); - Service.Plugin.OpenMacroEditor(character, recipe, buffs, macro?.Actions ?? Enumerable.Empty(), macro != null ? (actions => { macro.ActionEnumerable = actions; Service.Configuration.Save(); }) : null); + var stats = Service.Plugin.GetDefaultStats(); + Service.Plugin.OpenMacroEditor(stats.Character, stats.Recipe, stats.Buffs, macro?.Actions ?? Enumerable.Empty(), macro != null ? (actions => { macro.ActionEnumerable = actions; Service.Configuration.Save(); }) : null); } private void OnMacroChanged(Macro macro) From 39e95305af9808bf9b667d1c8e81ac0e888f8f7f Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 17 Oct 2023 04:34:13 -0700 Subject: [PATCH 20/58] 1.9.0.1 --- Craftimizer/Craftimizer.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Craftimizer/Craftimizer.csproj b/Craftimizer/Craftimizer.csproj index 3397a43..0fe00c2 100644 --- a/Craftimizer/Craftimizer.csproj +++ b/Craftimizer/Craftimizer.csproj @@ -2,7 +2,7 @@ Asriel Camora - 1.9.0.0 + 1.9.0.1 https://github.com/WorkingRobot/craftimizer.git From 3836fbef1017e3bbb5c2120a37909646df147485 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 24 Oct 2023 00:21:14 -0700 Subject: [PATCH 21/58] Disable esc key on RecipeNote window --- Craftimizer/Windows/RecipeNote.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index cbc2855..b33e598 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -80,9 +80,12 @@ public RecipeNote() : base("Craftimizer RecipeNote", WindowFlags, false) NoManipulationBadge = Service.IconManager.GetAssemblyTexture("Graphics.no_manip.png"); AxisFont = Service.PluginInterface.UiBuilder.GetGameFontHandle(new(GameFontFamilyAndSize.Axis14)); - Service.WindowSystem.AddWindow(this); - + RespectCloseHotkey = false; + DisableWindowSounds = true; + ShowCloseButton = false; IsOpen = true; + + Service.WindowSystem.AddWindow(this); } private bool wasOpen; From 889d07433cacc7396a05441c64db9b0972a604e9 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 24 Oct 2023 01:02:21 -0700 Subject: [PATCH 22/58] Fix copying macro to clipboard, and using /nextmacro --- Craftimizer/Utils/MacroCopy.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Craftimizer/Utils/MacroCopy.cs b/Craftimizer/Utils/MacroCopy.cs index 849a3ef..85ed9fe 100644 --- a/Craftimizer/Utils/MacroCopy.cs +++ b/Craftimizer/Utils/MacroCopy.cs @@ -27,7 +27,7 @@ public static void Copy(IReadOnlyList actions) var config = Service.Configuration.MacroCopy; var macros = new List(); var s = new List(); - foreach(var action in actions) + for (var i = 0; i < actions.Count; ++i) { if (s.Count == 0) { @@ -35,18 +35,18 @@ public static void Copy(IReadOnlyList actions) s.Add("/mlock"); } - s.Add(GetActionCommand(action, config)); + s.Add(GetActionCommand(actions[i], config)); - if (config.Type == MacroCopyConfiguration.CopyType.CopyToMacro || !config.CombineMacro) + if (i != actions.Count - 1 && (config.Type == MacroCopyConfiguration.CopyType.CopyToMacro || !config.CombineMacro)) { if (s.Count == MacroSize - 1) { - if (GetEndCommand(macros.Count, true, config) is { } endCommand) + if (GetEndCommand(macros.Count, false, config) is { } endCommand) s.Add(endCommand); } if (s.Count == MacroSize) { - macros.Add(string.Join("\n", s)); + macros.Add(string.Join(Environment.NewLine, s)); s.Clear(); } } @@ -55,7 +55,7 @@ public static void Copy(IReadOnlyList actions) { if (GetEndCommand(macros.Count, true, config) is { } endCommand) s.Add(endCommand); - macros.Add(string.Join("\n", s)); + macros.Add(string.Join(Environment.NewLine, s)); } switch (config.Type) @@ -146,7 +146,7 @@ private static unsafe void SetMacro(int idx, bool isShared, string macroText) private static void CopyToClipboard(List macros, MacroCopyConfiguration config) { - ImGui.SetClipboardText(string.Join("\n\n", macros)); + ImGui.SetClipboardText(string.Join(Environment.NewLine + Environment.NewLine, macros)); Service.PluginInterface.UiBuilder.AddNotification(macros.Count > 1 ? "Copied macro to clipboard." : $"Copied {macros.Count} macros to clipboard.", "Craftimizer Macro Copied", NotificationType.Success); } } From cd0fbacbbc3adb543cdc0bdcb79e488d5320f9cc Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 24 Oct 2023 01:07:26 -0700 Subject: [PATCH 23/58] Don't move to another macro if it's only going to have one step --- Craftimizer/Configuration.cs | 1 + Craftimizer/Utils/MacroCopy.cs | 28 +++++++++++++++++----------- Craftimizer/Windows/Settings.cs | 9 +++++++++ 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/Craftimizer/Configuration.cs b/Craftimizer/Configuration.cs index f03b0bc..d88d579 100644 --- a/Craftimizer/Configuration.cs +++ b/Craftimizer/Configuration.cs @@ -59,6 +59,7 @@ public enum CopyType public bool AddNotification { get; set; } = true; // Requires AddNotification + public bool ForceNotification { get; set; } public bool AddNotificationSound { get; set; } = true; public int IntermediateNotificationSound { get; set; } = 10; public int EndNotificationSound { get; set; } = 6; diff --git a/Craftimizer/Utils/MacroCopy.cs b/Craftimizer/Utils/MacroCopy.cs index 85ed9fe..e9f7df8 100644 --- a/Craftimizer/Utils/MacroCopy.cs +++ b/Craftimizer/Utils/MacroCopy.cs @@ -37,24 +37,30 @@ public static void Copy(IReadOnlyList actions) s.Add(GetActionCommand(actions[i], config)); - if (i != actions.Count - 1 && (config.Type == MacroCopyConfiguration.CopyType.CopyToMacro || !config.CombineMacro)) + if (config.Type == MacroCopyConfiguration.CopyType.CopyToMacro || !config.CombineMacro) { - if (s.Count == MacroSize - 1) + if (i != actions.Count - 1 && (i != actions.Count - 2 || config.ForceNotification)) { - if (GetEndCommand(macros.Count, false, config) is { } endCommand) - s.Add(endCommand); - } - if (s.Count == MacroSize) - { - macros.Add(string.Join(Environment.NewLine, s)); - s.Clear(); + if (s.Count == MacroSize - 1) + { + if (GetEndCommand(macros.Count, false, config) is { } endCommand) + s.Add(endCommand); + } + if (s.Count == MacroSize) + { + macros.Add(string.Join(Environment.NewLine, s)); + s.Clear(); + } } } } if (s.Count > 0) { - if (GetEndCommand(macros.Count, true, config) is { } endCommand) - s.Add(endCommand); + if (s.Count < MacroSize) + { + if (GetEndCommand(macros.Count, true, config) is { } endCommand) + s.Add(endCommand); + } macros.Add(string.Join(Environment.NewLine, s)); } diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index 1f341b2..03deb24 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -280,6 +280,15 @@ ref isDirty if (Config.MacroCopy.AddNotification) { + DrawOption( + "Force Notification", + "Prioritize always having a notification sound at the end of\n" + + "every macro. Keeping this off prevents macros with only 1 action.", + Config.MacroCopy.ForceNotification, + v => Config.MacroCopy.ForceNotification = v, + ref isDirty + ); + DrawOption( "Add Notification Sound", "Adds a sound to the end of every macro.", From 56383d4e6e84a68ff1f9867a06237305d5e12345 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 24 Oct 2023 01:38:59 -0700 Subject: [PATCH 24/58] Add macro chain is missing error --- Craftimizer/ImGuiUtils.cs | 3 +-- Craftimizer/Windows/Settings.cs | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Craftimizer/ImGuiUtils.cs b/Craftimizer/ImGuiUtils.cs index 88afbb8..1a98d3b 100644 --- a/Craftimizer/ImGuiUtils.cs +++ b/Craftimizer/ImGuiUtils.cs @@ -446,9 +446,8 @@ private static bool ListClip(IReadOnlyList data, float lineHeight, Predica public static bool IconButtonSized(FontAwesomeIcon icon, Vector2 size) { - ImGui.PushFont(UiBuilder.IconFont); + using var font = ImRaii.PushFont(UiBuilder.IconFont); var ret = ImGui.Button(icon.ToIconString(), size); - ImGui.PopFont(); return ret; } diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index 03deb24..ff16a8e 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -1,10 +1,13 @@ using Craftimizer.Solver; +using Dalamud.Interface; +using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; using System; +using System.Linq; using System.Numerics; namespace Craftimizer.Plugin.Windows; @@ -252,7 +255,7 @@ ref isDirty } DrawOption( - "Use MacroChain's /nextmacro", + "Use Macro Chain's /nextmacro", "Replaces the last step with /nextmacro to run the next macro\n" + "automatically. Overrides Add End Notification except for the\n" + "last macro.", @@ -261,6 +264,18 @@ ref isDirty ref isDirty ); + if (Config.MacroCopy.UseNextMacro && !Service.PluginInterface.InstalledPlugins.Any(p => p.IsLoaded && string.Equals(p.InternalName, "MacroChain", StringComparison.Ordinal))) + { + ImGui.SameLine(); + using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudOrange)) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImGui.Text(FontAwesomeIcon.ExclamationCircle.ToIconString()); + } + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Macro Chain is not installed"); + } + DrawOption( "Add Macro Lock", "Adds /mlock to the beginning of every macro. Prevents other\n" + From c0ba4407b536f57ec4e39defb8ac03d63d5a59dd Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 24 Oct 2023 01:39:13 -0700 Subject: [PATCH 25/58] Add force notification warning --- Craftimizer/Utils/MacroCopy.cs | 2 +- Craftimizer/Windows/Settings.cs | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Craftimizer/Utils/MacroCopy.cs b/Craftimizer/Utils/MacroCopy.cs index e9f7df8..d8b382a 100644 --- a/Craftimizer/Utils/MacroCopy.cs +++ b/Craftimizer/Utils/MacroCopy.cs @@ -56,7 +56,7 @@ public static void Copy(IReadOnlyList actions) } if (s.Count > 0) { - if (s.Count < MacroSize) + if (s.Count < MacroSize || (config.Type != MacroCopyConfiguration.CopyType.CopyToMacro && config.CombineMacro)) { if (GetEndCommand(macros.Count, true, config) is { } endCommand) s.Add(endCommand); diff --git a/Craftimizer/Windows/Settings.cs b/Craftimizer/Windows/Settings.cs index ff16a8e..a11ca3c 100644 --- a/Craftimizer/Windows/Settings.cs +++ b/Craftimizer/Windows/Settings.cs @@ -295,14 +295,20 @@ ref isDirty if (Config.MacroCopy.AddNotification) { - DrawOption( - "Force Notification", - "Prioritize always having a notification sound at the end of\n" + - "every macro. Keeping this off prevents macros with only 1 action.", - Config.MacroCopy.ForceNotification, - v => Config.MacroCopy.ForceNotification = v, - ref isDirty - ); + var isForceUseful = Config.MacroCopy.Type == MacroCopyConfiguration.CopyType.CopyToMacro || !Config.MacroCopy.CombineMacro; + using (var d = ImRaii.Disabled(!isForceUseful)) + { + DrawOption( + "Force Notification", + "Prioritize always having a notification sound at the end of\n" + + "every macro. Keeping this off prevents macros with only 1 action.", + Config.MacroCopy.ForceNotification, + v => Config.MacroCopy.ForceNotification = v, + ref isDirty + ); + } + if (!isForceUseful && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip("Only useful when Combine Macro is off"); DrawOption( "Add Notification Sound", From 60a939e023f312dbd5af9c6751b60c0a737e739d Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 24 Oct 2023 01:42:28 -0700 Subject: [PATCH 26/58] 1.9.0.2 Bump --- Craftimizer/Craftimizer.csproj | 2 +- Craftimizer/Craftimizer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Craftimizer/Craftimizer.csproj b/Craftimizer/Craftimizer.csproj index 0fe00c2..02fefc8 100644 --- a/Craftimizer/Craftimizer.csproj +++ b/Craftimizer/Craftimizer.csproj @@ -2,7 +2,7 @@ Asriel Camora - 1.9.0.1 + 1.9.0.2 https://github.com/WorkingRobot/craftimizer.git diff --git a/Craftimizer/Craftimizer.json b/Craftimizer/Craftimizer.json index 15d1e92..956fa6d 100644 --- a/Craftimizer/Craftimizer.json +++ b/Craftimizer/Craftimizer.json @@ -1,5 +1,5 @@ { - "Author": "Asriel Camora", + "Author": "Asriel", "Name": "Craftimizer", "Punchline": "Simulate crafts and create computer-assisted macros from the comfort of your own game", "Description": "Allows you to generate macros and simulate all sorts of crafts without having to open another app. Open your crafting log to get started.", From a81b6b3b1cd5c9a4694cd3de74a3b847938d00c6 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 24 Oct 2023 02:49:43 -0700 Subject: [PATCH 27/58] (1.9.0.3) Fix blank line when copying to macros --- Craftimizer/Craftimizer.csproj | 2 +- Craftimizer/Utils/MacroCopy.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Craftimizer/Craftimizer.csproj b/Craftimizer/Craftimizer.csproj index 02fefc8..685b50d 100644 --- a/Craftimizer/Craftimizer.csproj +++ b/Craftimizer/Craftimizer.csproj @@ -2,7 +2,7 @@ Asriel Camora - 1.9.0.2 + 1.9.0.3 https://github.com/WorkingRobot/craftimizer.git diff --git a/Craftimizer/Utils/MacroCopy.cs b/Craftimizer/Utils/MacroCopy.cs index d8b382a..10ecb50 100644 --- a/Craftimizer/Utils/MacroCopy.cs +++ b/Craftimizer/Utils/MacroCopy.cs @@ -144,7 +144,7 @@ private static unsafe void SetMacro(int idx, bool isShared, string macroText) var module = RaptureMacroModule.Instance(); var macro = module->GetMacro(isShared ? 1u : 0u, (uint)idx); - var text = Utf8String.FromString(macroText); + var text = Utf8String.FromString(macroText.Replace(Environment.NewLine, "\n")); module->ReplaceMacroLines(macro, text); text->Dtor(); IMemorySpace.Free(text); From 281b0d27a2759e00952a8354f5c52429a5359708 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Wed, 25 Oct 2023 12:43:09 -0700 Subject: [PATCH 28/58] Allow empty text and hashing for GroupPanel --- Craftimizer/ImGuiUtils.cs | 61 +++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 19 deletions(-) diff --git a/Craftimizer/ImGuiUtils.cs b/Craftimizer/ImGuiUtils.cs index 1a98d3b..68b83f1 100644 --- a/Craftimizer/ImGuiUtils.cs +++ b/Craftimizer/ImGuiUtils.cs @@ -24,6 +24,8 @@ internal static class ImGuiUtils // ^ only useful if width = -1 public static float BeginGroupPanel(string name, float width) { + ImGui.PushID(name); + // container group ImGui.BeginGroup(); @@ -43,14 +45,22 @@ public static float BeginGroupPanel(string name, float width) // label group ImGui.BeginGroup(); - ImGui.Dummy(new Vector2(frameHeight / 2, 0)); // shifts text by fh/2 - ImGui.SameLine(0, 0); - var textFrameHeight = ImGui.GetFrameHeight(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(name); - GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), textFrameHeight / 2f)); // push rect to stack - ImGui.SameLine(0, 0); - ImGui.Dummy(new Vector2(0f, textFrameHeight + itemSpacing.Y)); // shifts content by fh + is.y + if (ImGui.CalcTextSize(name, true).X == 0) + { + GroupPanelLabelStack.Push(default); + ImGui.Dummy(new Vector2(0f, itemSpacing.Y)); // shifts content by is.y + } + else + { + ImGui.Dummy(new Vector2(frameHeight / 2, 0)); // shifts text by fh/2 + ImGui.SameLine(0, 0); + var textFrameHeight = ImGui.GetFrameHeight(); + ImGui.AlignTextToFramePadding(); + ImGui.Text(name); + GroupPanelLabelStack.Push((ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), textFrameHeight / 2f)); // push rect to stack + ImGui.SameLine(0, 0); + ImGui.Dummy(new Vector2(0f, textFrameHeight + itemSpacing.Y)); // shifts content by fh + is.y + } // content group ImGui.BeginGroup(); @@ -80,28 +90,33 @@ public static void EndGroupPanel() // inner group ImGui.EndGroup(); - var labelRect = GroupPanelLabelStack.Pop(); - var innerMin = ImGui.GetItemRectMin() + new Vector2(0, labelRect.TopPadding); + var (labelMin, labelMax, labelPadding) = GroupPanelLabelStack.Pop(); + + var innerMin = ImGui.GetItemRectMin(); var innerMax = ImGui.GetItemRectMax(); + // If there was actual text + if (labelMax.X != labelMin.X) + { + innerMin += new Vector2(0, labelPadding); - (Vector2 Min, Vector2 Max) frameRect = (innerMin, innerMax); - // add itemspacing padding on the label's sides - labelRect.Min.X -= itemSpacing.X / 2; - labelRect.Max.X += itemSpacing.X / 2; + // add itemspacing padding on the label's sides + labelMin.X -= itemSpacing.X / 2; + labelMax.X += itemSpacing.X / 2; + } for (var i = 0; i < 4; ++i) { var (minClip, maxClip) = i switch { - 0 => (new Vector2(float.NegativeInfinity), new Vector2(labelRect.Min.X, float.PositiveInfinity)), - 1 => (new Vector2(labelRect.Max.X, float.NegativeInfinity), new Vector2(float.PositiveInfinity)), - 2 => (new Vector2(labelRect.Min.X, float.NegativeInfinity), new Vector2(labelRect.Max.X, labelRect.Min.Y)), - 3 => (new Vector2(labelRect.Min.X, labelRect.Max.Y), new Vector2(labelRect.Max.X, float.PositiveInfinity)), + 0 => (new Vector2(float.NegativeInfinity), new Vector2(labelMin.X, float.PositiveInfinity)), + 1 => (new Vector2(labelMax.X, float.NegativeInfinity), new Vector2(float.PositiveInfinity)), + 2 => (new Vector2(labelMin.X, float.NegativeInfinity), new Vector2(labelMax.X, labelMin.Y)), + 3 => (new Vector2(labelMin.X, labelMax.Y), new Vector2(labelMax.X, float.PositiveInfinity)), _ => (Vector2.Zero, Vector2.Zero) }; ImGui.PushClipRect(minClip, maxClip, true); ImGui.GetWindowDrawList().AddRect( - frameRect.Min, frameRect.Max, + innerMin, innerMax, ImGui.GetColorU32(ImGuiCol.Border), itemSpacing.X); ImGui.PopClipRect(); @@ -111,6 +126,8 @@ public static void EndGroupPanel() } ImGui.EndGroup(); + + ImGui.PopID(); } private struct EndUnconditionally : ImRaii.IEndObject, IDisposable @@ -444,6 +461,12 @@ private static bool ListClip(IReadOnlyList data, float lineHeight, Predica } } + public static bool InputTextMultilineWithHint(string label, string hint, ref string input, int maxLength, Vector2 size, ImGuiInputTextFlags flags = ImGuiInputTextFlags.None, ImGuiInputTextCallback? callback = null, IntPtr user_data = default) + { + const ImGuiInputTextFlags Multiline = (ImGuiInputTextFlags)(1 << 26); + return ImGuiExtras.InputTextEx(label, hint, ref input, maxLength, size, flags | Multiline, callback, user_data); + } + public static bool IconButtonSized(FontAwesomeIcon icon, Vector2 size) { using var font = ImRaii.PushFont(UiBuilder.IconFont); From c6c950dc51743dbd5c7b8233521c5ea9779b7a25 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Wed, 25 Oct 2023 12:44:03 -0700 Subject: [PATCH 29/58] Add internal InputTextEx --- Craftimizer/ImGuiExtras.cs | 139 +++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 Craftimizer/ImGuiExtras.cs diff --git a/Craftimizer/ImGuiExtras.cs b/Craftimizer/ImGuiExtras.cs new file mode 100644 index 0000000..bc60e1a --- /dev/null +++ b/Craftimizer/ImGuiExtras.cs @@ -0,0 +1,139 @@ +using ImGuiNET; +using System; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace Craftimizer; + +internal static unsafe class ImGuiExtras +{ + // https://github.com/ImGuiNET/ImGui.NET/blob/069363672fed940ebdaa02f9b032c282b66467c7/src/CodeGenerator/definitions/cimgui/definitions.json#L25394 + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + private static extern unsafe byte igInputTextEx(byte* label, byte* hint, byte* buf, int buf_size, Vector2 size, ImGuiInputTextFlags flags, ImGuiInputTextCallback? callback, void* user_data); + + // https://github.com/ImGuiNET/ImGui.NET/blob/069363672fed940ebdaa02f9b032c282b66467c7/src/ImGui.NET/Util.cs + #region Util + + private const int StackAllocationSizeLimit = 2048; + + private static unsafe byte* Allocate(int byteCount) => (byte*)Marshal.AllocHGlobal(byteCount); + + private static unsafe void Free(byte* ptr) => Marshal.FreeHGlobal((IntPtr)ptr); + + private static unsafe int GetUtf8(ReadOnlySpan s, byte* utf8Bytes, int utf8ByteCount) + { + if (s.IsEmpty) + return 0; + + fixed (char* utf16Ptr = s) + return Encoding.UTF8.GetBytes(utf16Ptr, s.Length, utf8Bytes, utf8ByteCount); + } + + private static unsafe bool AreStringsEqual(byte* a, int aLength, byte* b) + { + for (var i = 0; i < aLength; i++) + { + if (a[i] != b[i]) + return false; + } + + return b[aLength] == 0; + } + + private static unsafe string StringFromPtr(byte* ptr) + { + var characters = 0; + while (ptr[characters] != 0) + { + characters++; + } + + return Encoding.UTF8.GetString(ptr, characters); + } + + #endregion + + // Based off of code from InputTextWithHint: https://github.com/ImGuiNET/ImGui.NET/blob/069363672fed940ebdaa02f9b032c282b66467c7/src/ImGui.NET/ImGui.Manual.cs#L271 + public static unsafe bool InputTextEx(string label, string hint, ref string input, int maxLength, Vector2 size, ImGuiInputTextFlags flags = ImGuiInputTextFlags.None, ImGuiInputTextCallback? callback = null, IntPtr user_data = default) + { + var utf8LabelByteCount = Encoding.UTF8.GetByteCount(label); + byte* utf8LabelBytes; + if (utf8LabelByteCount > StackAllocationSizeLimit) + { + utf8LabelBytes = Allocate(utf8LabelByteCount + 1); + } + else + { + var stackPtr = stackalloc byte[utf8LabelByteCount + 1]; + utf8LabelBytes = stackPtr; + } + GetUtf8(label, utf8LabelBytes, utf8LabelByteCount); + + var utf8HintByteCount = Encoding.UTF8.GetByteCount(hint); + byte* utf8HintBytes; + if (utf8HintByteCount > StackAllocationSizeLimit) + { + utf8HintBytes = Allocate(utf8HintByteCount + 1); + } + else + { + var stackPtr = stackalloc byte[utf8HintByteCount + 1]; + utf8HintBytes = stackPtr; + } + GetUtf8(hint, utf8HintBytes, utf8HintByteCount); + + var utf8InputByteCount = Encoding.UTF8.GetByteCount(input); + var inputBufSize = Math.Max(maxLength + 1, utf8InputByteCount + 1); + + byte* utf8InputBytes; + byte* originalUtf8InputBytes; + if (inputBufSize > StackAllocationSizeLimit) + { + utf8InputBytes = Allocate(inputBufSize); + originalUtf8InputBytes = Allocate(inputBufSize); + } + else + { + var inputStackBytes = stackalloc byte[inputBufSize]; + utf8InputBytes = inputStackBytes; + var originalInputStackBytes = stackalloc byte[inputBufSize]; + originalUtf8InputBytes = originalInputStackBytes; + } + GetUtf8(input, utf8InputBytes, inputBufSize); + var clearBytesCount = (uint)(inputBufSize - utf8InputByteCount); + Unsafe.InitBlockUnaligned(utf8InputBytes + utf8InputByteCount, 0, clearBytesCount); + Unsafe.CopyBlock(originalUtf8InputBytes, utf8InputBytes, (uint)inputBufSize); + + var result = igInputTextEx( + utf8LabelBytes, + utf8HintBytes, + utf8InputBytes, + inputBufSize, + size, + flags, + callback, + user_data.ToPointer()); + if (!AreStringsEqual(originalUtf8InputBytes, inputBufSize, utf8InputBytes)) + { + input = StringFromPtr(utf8InputBytes); + } + + if (utf8LabelByteCount > StackAllocationSizeLimit) + { + Free(utf8LabelBytes); + } + if (utf8HintByteCount > StackAllocationSizeLimit) + { + Free(utf8HintBytes); + } + if (inputBufSize > StackAllocationSizeLimit) + { + Free(utf8InputBytes); + Free(originalUtf8InputBytes); + } + + return result != 0; + } +} From 246234620a3e373c867a98645413b1360483ad2c Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Wed, 25 Oct 2023 12:44:47 -0700 Subject: [PATCH 30/58] Change copy icon to paste Looks better imo --- Craftimizer/Windows/MacroClipboard.cs | 2 +- Craftimizer/Windows/MacroEditor.cs | 2 +- Craftimizer/Windows/MacroList.cs | 2 +- Craftimizer/Windows/RecipeNote.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Craftimizer/Windows/MacroClipboard.cs b/Craftimizer/Windows/MacroClipboard.cs index 54f7cb3..a320b93 100644 --- a/Craftimizer/Windows/MacroClipboard.cs +++ b/Craftimizer/Windows/MacroClipboard.cs @@ -49,7 +49,7 @@ private void DrawMacro(int idx, string macro) ImGui.SetCursorPos(buttonCursor); { using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(buttonActive ? ImGuiCol.ButtonActive : ImGuiCol.ButtonHovered), buttonHovered); - ImGuiUtils.IconButtonSized(FontAwesomeIcon.Copy, new(ImGui.GetFrameHeight())); + ImGuiUtils.IconButtonSized(FontAwesomeIcon.Paste, new(ImGui.GetFrameHeight())); if (buttonClicked) { ImGui.SetClipboardText(macro); diff --git a/Craftimizer/Windows/MacroEditor.cs b/Craftimizer/Windows/MacroEditor.cs index 91aafd2..69a91af 100644 --- a/Craftimizer/Windows/MacroEditor.cs +++ b/Craftimizer/Windows/MacroEditor.cs @@ -1186,7 +1186,7 @@ private void DrawMacroActions(float availWidth) "can vary wildly depending on the solver's settings."); } ImGui.SameLine(); - if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Copy, new(height))) + if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Paste, new(height))) Service.Plugin.CopyMacro(Macro.Select(s => s.Action).ToArray()); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Copy to Clipboard"); diff --git a/Craftimizer/Windows/MacroList.cs b/Craftimizer/Windows/MacroList.cs index a63d503..3601af6 100644 --- a/Craftimizer/Windows/MacroList.cs +++ b/Craftimizer/Windows/MacroList.cs @@ -197,7 +197,7 @@ private void DrawMacro(Macro macro) ImGui.TableNextColumn(); { - if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Copy, new(miniRowHeight))) + if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Paste, new(miniRowHeight))) Service.Plugin.CopyMacro(macro.Actions); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Copy to Clipboard"); diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index b33e598..ee3d3c9 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -662,7 +662,7 @@ private void DrawMacro((IReadOnlyList Actions, SimulationState State Service.Plugin.OpenMacroEditor(CharacterStats!, RecipeData!, new(Service.ClientState.LocalPlayer!.StatusList), macro.Actions, setter); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Open in Simulator"); - if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Copy, new(miniRowHeight))) + if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.Paste, new(miniRowHeight))) Service.Plugin.CopyMacro(macro.Actions); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Copy to Clipboard"); From f364b09af9cb655ac62170aeec95f3bab9816089 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Wed, 25 Oct 2023 12:45:02 -0700 Subject: [PATCH 31/58] Add importing of ffxivteamcraft macros --- Craftimizer/Utils/MacroCopy.cs | 2 +- Craftimizer/Utils/MacroImport.cs | 269 +++++++++++++++++++++++++++++ Craftimizer/Windows/MacroEditor.cs | 198 +++++++++++++++++++-- 3 files changed, 452 insertions(+), 17 deletions(-) create mode 100644 Craftimizer/Utils/MacroImport.cs diff --git a/Craftimizer/Utils/MacroCopy.cs b/Craftimizer/Utils/MacroCopy.cs index 10ecb50..abdc02f 100644 --- a/Craftimizer/Utils/MacroCopy.cs +++ b/Craftimizer/Utils/MacroCopy.cs @@ -144,7 +144,7 @@ private static unsafe void SetMacro(int idx, bool isShared, string macroText) var module = RaptureMacroModule.Instance(); var macro = module->GetMacro(isShared ? 1u : 0u, (uint)idx); - var text = Utf8String.FromString(macroText.Replace(Environment.NewLine, "\n")); + var text = Utf8String.FromString(macroText.ReplaceLineEndings("\n")); module->ReplaceMacroLines(macro, text); text->Dtor(); IMemorySpace.Free(text); diff --git a/Craftimizer/Utils/MacroImport.cs b/Craftimizer/Utils/MacroImport.cs new file mode 100644 index 0000000..7ef72c4 --- /dev/null +++ b/Craftimizer/Utils/MacroImport.cs @@ -0,0 +1,269 @@ +using Craftimizer.Plugin; +using Craftimizer.Simulator; +using Craftimizer.Simulator.Actions; +using Dalamud.Networking.Http; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Threading.Tasks; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace Craftimizer.Utils; + +public static class MacroImport +{ + public static IReadOnlyList? TryParseMacro(string inputMacro) + { + var actions = new List(); + foreach(var line in inputMacro.ReplaceLineEndings("\n").Split("\n")) + { + if (TryParseLine(line) is { } action) + actions.Add(action); + } + return actions.Count > 0 ? actions : null; + } + + private static ActionType? TryParseLine(string line) + { + if (line.StartsWith("/ac", StringComparison.OrdinalIgnoreCase)) + line = line[3..]; + else if (line.StartsWith("/action", StringComparison.OrdinalIgnoreCase)) + line = line[7..]; + else + return null; + + line = line.TrimStart(); + + // get first word + if (line.StartsWith('"')) + { + line = line[1..]; + + var end = line.IndexOf('"', 1); + if (end != -1) + line = line[..end]; + } + else + { + var end = line.IndexOf(' ', 1); + if (end != -1) + line = line[..end]; + } + + foreach(var action in Enum.GetValues()) + { + if (line.Equals(action.GetName(ClassJob.Carpenter), StringComparison.OrdinalIgnoreCase)) + return action; + } + return null; + } + + public static bool TryParseUrl(string url, out Uri uri) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out uri!)) + return false; + + if (!string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && !string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) + return false; + + if (!uri.IsDefaultPort) + return false; + + return uri.DnsSafeHost is "ffxivteamcraft.com" or "craftingway.app"; + } + + public static Task RetrieveUrl(string url, CancellationToken token) + { + if (!TryParseUrl(url, out var uri)) + throw new ArgumentException("Unsupported url", nameof(url)); + + switch (uri.DnsSafeHost) + { + case "ffxivteamcraft.com": + return RetrieveTeamcraftUrl(uri, token); + case "craftingway.app": + return RetrieveCraftingwayUrl(uri, token); + default: + throw new UnreachableException("TryParseUrl should handle miscellaneous edge cases"); + } + } + + private sealed record TeamcraftMacro + { + public sealed record StringValue + { + [JsonPropertyName("stringValue")] + [JsonRequired] + public required string Value { get; set; } + + public static implicit operator string(StringValue v) => v.Value; + } + + public sealed record IntegerValue + { + [JsonPropertyName("integerValue")] + [JsonRequired] + public required int Value { get; set; } + + public static implicit operator int(IntegerValue v) => v.Value; + } + + public sealed record MapValue + { + public sealed record ValueData + { + [JsonRequired] + public required T Fields { get; set; } + } + + [JsonPropertyName("mapValue")] + [JsonRequired] + public required ValueData Data { get; set; } + + public T Value => Data.Fields; + public static implicit operator T(MapValue v) => v.Value; + } + + public sealed record ArrayValue + { + public sealed record ValueData + { + [JsonRequired] + public required T[] Values { get; set; } + } + + [JsonPropertyName("arrayValue")] + [JsonRequired] + public required ValueData Data { get; set; } + + public T[] Value => Data.Values; + public static implicit operator T[](ArrayValue v) => v.Value; + } + + public sealed record RecipeFieldData + { + [JsonRequired] + public required IntegerValue RLvl { get; set; } + [JsonRequired] + public required IntegerValue Durability { get; set; } + } + + public sealed record FieldData + { + public StringValue? Name { get; set; } + [JsonRequired] + public required ArrayValue Rotation { get; set; } + public MapValue? Recipe { get; set; } + } + + public sealed record ErrorData + { + public required int Code { get; set; } + public required string Message { get; set; } + public required string Status { get; set; } + } + + public FieldData? Fields { get; set; } + + public ErrorData? Error { get; set; } + } + + public readonly record struct RetrievedMacro(string Name, IReadOnlyList Actions); + + private static async Task RetrieveTeamcraftUrl(Uri uri, CancellationToken token) + { + using var heCallback = new HappyEyeballsCallback(); + using var client = new HttpClient(new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + ConnectCallback = heCallback.ConnectCallback, + }); + + var path = uri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped); + if (!path.StartsWith("simulator/", StringComparison.Ordinal)) + throw new ArgumentException("Teamcraft macro url should start with /simulator", nameof(uri)); + path = path[10..]; + + var lastSlash = path.LastIndexOf('/'); + if (lastSlash == -1) + throw new ArgumentException("Teamcraft macro url is not in the right format", nameof(uri)); + + var id = path[(lastSlash + 1)..]; + + var resp = await client.GetFromJsonAsync($"https://firestore.googleapis.com/v1beta1/projects/ffxivteamcraft/databases/(default)/documents/rotations/{id}", token).ConfigureAwait(false); + if (resp is null) + throw new Exception("Internal error; failed to retrieve macro"); + if (resp.Error is { } error) + throw new Exception($"Internal server error ({error.Status}); {error.Message}"); + if (resp.Fields is not { } rotation) + throw new Exception($"Internal error; No fields or error was returned"); + // https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/67f453041c6b2b31d32fcf6e1fd53aa38ed7a12b/apps/client/src/app/model/other/crafting-rotation.ts#L49 + var name = rotation.Name?.Value ?? + (rotation.Recipe is { Value: var recipe } ? + $"rlvl{recipe.RLvl.Value} - {rotation.Rotation.Value.Length} steps, {recipe.Durability.Value} dur" : + "New Teamcraft Rotation"); + var actions = new List(); + foreach(var action in rotation.Rotation.Value) + { + ActionType? actionType = action.Value switch + { + "BasicSynthesis" => ActionType.BasicSynthesis, + "CarefulSynthesis" => ActionType.CarefulSynthesis, + "PrudentSynthesis" => ActionType.PrudentSynthesis, + "RapidSynthesis" => ActionType.RapidSynthesis, + "Groundwork" => ActionType.Groundwork, + "FocusedSynthesis" => ActionType.FocusedSynthesis, + "MuscleMemory" => ActionType.MuscleMemory, + "IntensiveSynthesis" => ActionType.IntensiveSynthesis, + "BasicTouch" => ActionType.BasicTouch, + "StandardTouch" => ActionType.StandardTouch, + "AdvancedTouch" => ActionType.AdvancedTouch, + "HastyTouch" => ActionType.HastyTouch, + "ByregotsBlessing" => ActionType.ByregotsBlessing, + "PreciseTouch" => ActionType.PreciseTouch, + "FocusedTouch" => ActionType.FocusedTouch, + "PrudentTouch" => ActionType.PrudentTouch, + "TrainedEye" => ActionType.TrainedEye, + "PreparatoryTouch" => ActionType.PreparatoryTouch, + "Reflect" => ActionType.Reflect, + "TrainedFinesse" => ActionType.TrainedFinesse, + "TricksOfTheTrade" => ActionType.TricksOfTheTrade, + "MastersMend" => ActionType.MastersMend, + "Manipulation" => ActionType.Manipulation, + "WasteNot" => ActionType.WasteNot, + "WasteNotII" => ActionType.WasteNot2, + "GreatStrides" => ActionType.GreatStrides, + "Innovation" => ActionType.Innovation, + "Veneration" => ActionType.Veneration, + "FinalAppraisal" => ActionType.FinalAppraisal, + "Observe" => ActionType.Observe, + "HeartAndSoul" => ActionType.HeartAndSoul, + "CarefulObservation" => ActionType.CarefulObservation, + "DelicateSynthesis" => ActionType.DelicateSynthesis, + "RemoveFinalAppraisal" => throw new Exception("Removing Final Appraisal is an unsupported action"), + null => null, + { } actionValue => throw new Exception($"Unknown action {actionValue}"), + }; + if (actionType.HasValue) + actions.Add(actionType.Value); + } + return new(name, actions); + } + + private static async Task RetrieveCraftingwayUrl(Uri uri, CancellationToken token) + { + using var heCallback = new HappyEyeballsCallback(); + using var client = new HttpClient(new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + ConnectCallback = heCallback.ConnectCallback, + }); + + throw new NotImplementedException(); + } +} diff --git a/Craftimizer/Windows/MacroEditor.cs b/Craftimizer/Windows/MacroEditor.cs index 69a91af..9d4f9e1 100644 --- a/Craftimizer/Windows/MacroEditor.cs +++ b/Craftimizer/Windows/MacroEditor.cs @@ -6,8 +6,10 @@ using Dalamud.Game.ClientState.Statuses; using Dalamud.Game.Text; using Dalamud.Interface; +using Dalamud.Interface.Colors; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Utility; @@ -104,6 +106,14 @@ private sealed record SimulatedActionStep private IDalamudTextureWrap EatFromTheHandBadge { get; } private GameFontHandle AxisFont { get; } + private string popupSaveAsMacroName = string.Empty; + + private string popupImportText = string.Empty; + private string popupImportUrl = string.Empty; + private string popupImportError = string.Empty; + private CancellationTokenSource? popupImportUrlTokenSource; + private MacroImport.RetrievedMacro? popupImportUrlMacro; + public MacroEditor(CharacterStats characterStats, RecipeData recipeData, CrafterBuffs buffs, IEnumerable actions, Action>? setter) : base("Craftimizer Macro Editor", WindowFlags, false) { CharacterStats = characterStats; @@ -168,7 +178,8 @@ public override void Draw() ImGui.Separator(); - using (var table = ImRaii.Table("macroInfo", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame)) { + using (var table = ImRaii.Table("macroInfo", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingStretchSame)) + { if (table) { ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthStretch, 2); @@ -691,7 +702,7 @@ private bool DrawRecipeParams() ImGui.AlignTextToFramePadding(); ImGui.Image(Service.IconManager.GetIcon(RecipeData.Recipe.ItemResult.Value!.Icon).ImGuiHandle, new Vector2(imageSize)); - + ImGui.SameLine(0, 5); ushort? newRecipe = null; @@ -983,7 +994,7 @@ private void DrawMacroInfo() DrawBars(datas); } } - + using (var panel = ImGuiUtils.GroupPanel("Buffs", -1, out _)) { using var _font = ImRaii.PushFont(AxisFont.ImFont); @@ -995,7 +1006,7 @@ private void DrawMacroInfo() ImGui.SameLine(0, 0); var effects = State.ActiveEffects; - foreach(var effect in Enum.GetValues()) + foreach (var effect in Enum.GetValues()) { if (!effects.HasEffect(effect)) continue; @@ -1040,7 +1051,7 @@ private void DrawBars(IEnumerable bars) }); var maxSize = (textSize - 2 * spacing - ImGui.CalcTextSize("/").X) / 2; var barSize = totalSize - textSize - spacing; - foreach(var bar in bars) + foreach (var bar in bars) { using var panel = ImGuiUtils.GroupPanel(bar.Name, totalSize, out _); if (bar.Condition is { } condition) @@ -1138,7 +1149,7 @@ private void DrawMacroActions(float availWidth) { var height = ImGui.GetFrameHeight(); var spacing = ImGui.GetStyle().ItemSpacing.X; - var width = availWidth - (spacing + height) * (DefaultActions.Length > 0 ? 3 : 2); // small buttons at the end + var width = availWidth - ((spacing + height) * (3 + (DefaultActions.Length > 0 ? 1 : 0))); // small buttons at the end var halfWidth = (width - spacing) / 2f; var quarterWidth = (halfWidth - spacing) / 2f; @@ -1191,6 +1202,15 @@ private void DrawMacroActions(float availWidth) if (ImGui.IsItemHovered()) ImGui.SetTooltip("Copy to Clipboard"); ImGui.SameLine(); + using (var _disabled = ImRaii.Disabled(SolverRunning)) + { + if (ImGuiUtils.IconButtonSized(FontAwesomeIcon.FileImport, new(height))) + ShowImportPopup(); + } + if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) + ImGui.SetTooltip("Import Macro"); + DrawImportPopup(); + ImGui.SameLine(); if (DefaultActions.Length > 0) { using (var _disabled = ImRaii.Disabled(SolverRunning)) @@ -1219,11 +1239,10 @@ private void DrawMacroActions(float availWidth) ImGui.SetTooltip("Clear"); } - private string popupMacroName = string.Empty; private void ShowSaveAsPopup() { ImGui.OpenPopup($"##saveAsPopup"); - popupMacroName = string.Empty; + popupSaveAsMacroName = string.Empty; ImGui.SetNextWindowPos(ImGui.GetMousePos() - new Vector2(ImGui.CalcItemWidth() * .25f, ImGui.GetFrameHeight() + ImGui.GetStyle().WindowPadding.Y * 2)); } @@ -1235,11 +1254,11 @@ private void DrawSaveAsPopup() if (ImGui.IsWindowAppearing()) ImGui.SetKeyboardFocusHere(); ImGui.SetNextItemWidth(ImGui.CalcItemWidth()); - if (ImGui.InputTextWithHint($"##setName", "Name", ref popupMacroName, 100, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue)) + if (ImGui.InputTextWithHint($"##setName", "Name", ref popupSaveAsMacroName, 100, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue)) { - if (!string.IsNullOrWhiteSpace(popupMacroName)) + if (!string.IsNullOrWhiteSpace(popupSaveAsMacroName)) { - var newMacro = new Macro() { Name = popupMacroName, Actions = Macro.Select(s => s.Action).ToArray() }; + var newMacro = new Macro() { Name = popupSaveAsMacroName, Actions = Macro.Select(s => s.Action).ToArray() }; Service.Configuration.AddMacro(newMacro); MacroSetter = actions => { @@ -1252,6 +1271,152 @@ private void DrawSaveAsPopup() } } + private void ShowImportPopup() + { + ImGui.OpenPopup($"##importPopup"); + popupImportText = string.Empty; + popupImportUrl = string.Empty; + popupImportError = string.Empty; + popupImportUrlMacro = null; + popupImportUrlTokenSource = null; + } + + private void DrawImportPopup() + { + const string ExampleMacro = "/mlock\n/ac \"Muscle Memory\" \n/ac Manipulation \n/ac Veneration \n/ac \"Waste Not II\" \n/ac Groundwork \n/ac Innovation \n/ac \"Preparatory Touch\" \n/ac \"Preparatory Touch\" \n/ac \"Preparatory Touch\" \n/ac \"Preparatory Touch\" \n/ac \"Great Strides\" \n/ac \"Byregot's Blessing\" \n/ac \"Careful Synthesis\" "; + const string ExampleUrl = "https://ffxivteamcraft.com/simulator/39630/35499/9XOZDZKhbVXJUIPXjM63"; + + ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, new Vector2(0.5f)); + ImGui.SetNextWindowSizeConstraints(new(400, 0), new(float.PositiveInfinity)); + using var popup = ImRaii.Popup($"##importPopup", ImGuiWindowFlags.Modal | ImGuiWindowFlags.NoMove); + if (popup) + { + bool submittedText, submittedUrl; + + using (var panel = ImGuiUtils.GroupPanel("##text", -1, out var availWidth)) + { + ImGui.AlignTextToFramePadding(); + ImGuiUtils.TextCentered("Paste your macro here"); + { + using var font = ImRaii.PushFont(UiBuilder.MonoFont); + ImGuiUtils.InputTextMultilineWithHint("", ExampleMacro, ref popupImportText, 2048, new(availWidth, ImGui.GetTextLineHeight() * 15 + ImGui.GetStyle().FramePadding.Y), ImGuiInputTextFlags.AutoSelectAll); + } + using (var _disabled = ImRaii.Disabled(popupImportUrlTokenSource != null)) + submittedText = ImGui.Button("Import", new(availWidth, 0)); + } + + using (var panel = ImGuiUtils.GroupPanel("##url", -1, out var availWidth)) + { + var availOffset = ImGui.GetContentRegionAvail().X - availWidth; + + ImGui.AlignTextToFramePadding(); + ImGuiUtils.TextCentered("or provide a url to it"); + ImGui.SameLine(); + using (var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudGrey)) + { + using var font = ImRaii.PushFont(UiBuilder.IconFont); + ImGuiUtils.TextRight(FontAwesomeIcon.InfoCircle.ToIconString(), ImGui.GetContentRegionAvail().X - availOffset); + } + if (ImGui.IsItemHovered()) + { + using var t = ImRaii.Tooltip(); + ImGui.Text("Supported sites:"); + ImGui.BulletText("ffxivteamcraft.com"); + ImGui.BulletText("craftingway.app"); + ImGui.Text("More suggestions are appreciated!"); + } + ImGui.SetNextItemWidth(availWidth); + submittedUrl = ImGui.InputTextWithHint("", ExampleUrl, ref popupImportUrl, 2048, ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue); + using (var _disabled = ImRaii.Disabled(popupImportUrlTokenSource != null)) + submittedUrl = ImGui.Button("Import", new(availWidth, 0)) || submittedUrl; + } + + ImGui.Dummy(default); + + if (!string.IsNullOrWhiteSpace(popupImportError)) + { + using (var c = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed)) + ImGui.TextWrapped(popupImportError); + ImGui.Dummy(default); + } + + if (ImGuiUtils.ButtonCentered("Nevermind", new(ImGui.GetContentRegionAvail().X / 2f, 0))) + { + popupImportUrlTokenSource?.Cancel(); + ImGui.CloseCurrentPopup(); + } + + if (popupImportUrlTokenSource == null) + { + if (submittedText) + { + if (MacroImport.TryParseMacro(popupImportText) is { } parsedActions) + { + popupImportUrlTokenSource?.Cancel(); + Macro.Clear(); + foreach (var action in parsedActions) + AddStep(action); + + Service.PluginInterface.UiBuilder.AddNotification($"Imported macro with {parsedActions.Count} step{(parsedActions.Count != 1 ? "s" : "")}", "Craftimizer Macro Imported", NotificationType.Success); + popupImportUrlTokenSource?.Cancel(); + ImGui.CloseCurrentPopup(); + } + else + popupImportError = "Could not find any actions to import. Is it a valid macro?"; + } + if (submittedUrl) + { + if (MacroImport.TryParseUrl(popupImportUrl, out _)) + { + popupImportUrlTokenSource = new(); + popupImportUrlMacro = null; + var token = popupImportUrlTokenSource.Token; + var url = popupImportUrl; + + var task = Task.Run(() => MacroImport.RetrieveUrl(url, token), token); + _ = task.ContinueWith(t => + { + if (token == popupImportUrlTokenSource.Token) + popupImportUrlTokenSource = null; + }); + _ = task.ContinueWith(t => + { + if (token.IsCancellationRequested) + return; + + try + { + t.Exception!.Flatten().Handle(ex => ex is TaskCanceledException or OperationCanceledException); + } + catch (AggregateException e) + { + popupImportError = e.Message; + Log.Error(e, "Retrieving macro failed"); + } + }, TaskContinuationOptions.OnlyOnFaulted); + _ = task.ContinueWith(t => popupImportUrlMacro = t.Result, TaskContinuationOptions.OnlyOnRanToCompletion); + } + else + popupImportError = "The url is not in the right format for any supported sites."; + } + if (popupImportUrlMacro is { Name: var name, Actions: var actions }) + { + Macro.Clear(); + foreach(var action in actions) + AddStep(action); + Service.PluginInterface.UiBuilder.AddNotification($"Imported macro \"{name}\"", "Craftimizer Macro Imported", NotificationType.Success); + + popupImportUrlTokenSource?.Cancel(); + ImGui.CloseCurrentPopup(); + } + } + } + else + { + popupImportUrlTokenSource?.Cancel(); + popupImportUrlTokenSource = null; + } + } private void CalculateBestMacro() { SolverTokenSource?.Cancel(); @@ -1294,7 +1459,7 @@ private void CalculateBestMacroTask(SimulationState state, CancellationToken tok var solver = new Solver.Solver(config, state) { Token = token }; solver.OnLog += Log.Debug; - solver.OnNewAction += a => AddStep(a, isMacro: true); + solver.OnNewAction += a => AddStep(a, isSolver: true); solver.Start(); _ = solver.GetTask().GetAwaiter().GetResult(); @@ -1321,11 +1486,11 @@ private void RecalculateState() lastState = ((step.Response, step.State) = sim.Execute(lastState, step.Action)).State; } - private void AddStep(ActionType action, int index = -1, bool isMacro = false) + private void AddStep(ActionType action, int index = -1, bool isSolver = false) { if (index < -1 || index >= Macro.Count) throw new ArgumentOutOfRangeException(nameof(index)); - if (!isMacro && SolverRunning) + if (!isSolver && SolverRunning) throw new InvalidOperationException("Cannot add steps while solver is running"); if (!SolverRunning) SolverStartStepCount = null; @@ -1336,13 +1501,14 @@ private void AddStep(ActionType action, int index = -1, bool isMacro = false) var resp = sim.Execute(State, action); Macro.Add(new() { Action = action, Response = resp.Response, State = resp.NewState }); } - else { + else + { var state = index == 0 ? InitialState : Macro[index - 1].State; var sim = new Sim(state); var resp = sim.Execute(state, action); Macro.Insert(index, new() { Action = action, Response = resp.Response, State = resp.NewState }); state = resp.NewState; - for(var i = index + 1; i < Macro.Count; i++) + for (var i = index + 1; i < Macro.Count; i++) state = ((Macro[i].Response, Macro[i].State) = sim.Execute(state, Macro[i].Action)).State; } } From 2f36093453995aaa395cfd5eb847c3f5a267f334 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Wed, 25 Oct 2023 13:10:37 -0700 Subject: [PATCH 32/58] Add craftingway macro support --- Craftimizer/Utils/MacroImport.cs | 169 +++++++++++++++++++++++-------- 1 file changed, 129 insertions(+), 40 deletions(-) diff --git a/Craftimizer/Utils/MacroImport.cs b/Craftimizer/Utils/MacroImport.cs index 7ef72c4..6b9d5b8 100644 --- a/Craftimizer/Utils/MacroImport.cs +++ b/Craftimizer/Utils/MacroImport.cs @@ -8,10 +8,10 @@ using System.Net; using System.Net.Http; using System.Net.Http.Json; +using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; -using static System.Runtime.InteropServices.JavaScript.JSType; namespace Craftimizer.Utils; @@ -20,7 +20,7 @@ public static class MacroImport public static IReadOnlyList? TryParseMacro(string inputMacro) { var actions = new List(); - foreach(var line in inputMacro.ReplaceLineEndings("\n").Split("\n")) + foreach (var line in inputMacro.ReplaceLineEndings("\n").Split("\n")) { if (TryParseLine(line) is { } action) actions.Add(action); @@ -55,7 +55,7 @@ public static class MacroImport line = line[..end]; } - foreach(var action in Enum.GetValues()) + foreach (var action in Enum.GetValues()) { if (line.Equals(action.GetName(ClassJob.Carpenter), StringComparison.OrdinalIgnoreCase)) return action; @@ -173,6 +173,29 @@ public sealed record ErrorData public ErrorData? Error { get; set; } } + private sealed record CraftingwayMacro + { + public int Id { get; set; } + public string? Slug { get; set; } + public string? Version { get; set; } + public string? Job { get; set; } + [JsonPropertyName("job_level")] + public int JobLevel { get; set; } + public int Craftsmanship { get; set; } + public int Control { get; set; } + public int CP { get; set; } + public string? Food { get; set; } + public string? Potion { get; set; } + [JsonPropertyName("recipe_job_level")] + public int RecipeJobLevel { get; set; } + public string? Recipe { get; set; } + // HqIngredients + public string? Actions { get; set; } + [JsonPropertyName("created_at")] + public long CreatedAt { get; set; } + public string? Error { get; set; } + } + public readonly record struct RetrievedMacro(string Name, IReadOnlyList Actions); private static async Task RetrieveTeamcraftUrl(Uri uri, CancellationToken token) @@ -195,7 +218,10 @@ private static async Task RetrieveTeamcraftUrl(Uri uri, Cancella var id = path[(lastSlash + 1)..]; - var resp = await client.GetFromJsonAsync($"https://firestore.googleapis.com/v1beta1/projects/ffxivteamcraft/databases/(default)/documents/rotations/{id}", token).ConfigureAwait(false); + var resp = await client.GetFromJsonAsync( + $"https://firestore.googleapis.com/v1beta1/projects/ffxivteamcraft/databases/(default)/documents/rotations/{id}", + token). + ConfigureAwait(false); if (resp is null) throw new Exception("Internal error; failed to retrieve macro"); if (resp.Error is { } error) @@ -208,44 +234,44 @@ private static async Task RetrieveTeamcraftUrl(Uri uri, Cancella $"rlvl{recipe.RLvl.Value} - {rotation.Rotation.Value.Length} steps, {recipe.Durability.Value} dur" : "New Teamcraft Rotation"); var actions = new List(); - foreach(var action in rotation.Rotation.Value) + foreach (var action in rotation.Rotation.Value) { ActionType? actionType = action.Value switch { - "BasicSynthesis" => ActionType.BasicSynthesis, - "CarefulSynthesis" => ActionType.CarefulSynthesis, - "PrudentSynthesis" => ActionType.PrudentSynthesis, - "RapidSynthesis" => ActionType.RapidSynthesis, - "Groundwork" => ActionType.Groundwork, - "FocusedSynthesis" => ActionType.FocusedSynthesis, - "MuscleMemory" => ActionType.MuscleMemory, - "IntensiveSynthesis" => ActionType.IntensiveSynthesis, - "BasicTouch" => ActionType.BasicTouch, - "StandardTouch" => ActionType.StandardTouch, - "AdvancedTouch" => ActionType.AdvancedTouch, - "HastyTouch" => ActionType.HastyTouch, - "ByregotsBlessing" => ActionType.ByregotsBlessing, - "PreciseTouch" => ActionType.PreciseTouch, - "FocusedTouch" => ActionType.FocusedTouch, - "PrudentTouch" => ActionType.PrudentTouch, - "TrainedEye" => ActionType.TrainedEye, - "PreparatoryTouch" => ActionType.PreparatoryTouch, - "Reflect" => ActionType.Reflect, - "TrainedFinesse" => ActionType.TrainedFinesse, - "TricksOfTheTrade" => ActionType.TricksOfTheTrade, - "MastersMend" => ActionType.MastersMend, - "Manipulation" => ActionType.Manipulation, - "WasteNot" => ActionType.WasteNot, - "WasteNotII" => ActionType.WasteNot2, - "GreatStrides" => ActionType.GreatStrides, - "Innovation" => ActionType.Innovation, - "Veneration" => ActionType.Veneration, - "FinalAppraisal" => ActionType.FinalAppraisal, - "Observe" => ActionType.Observe, - "HeartAndSoul" => ActionType.HeartAndSoul, - "CarefulObservation" => ActionType.CarefulObservation, - "DelicateSynthesis" => ActionType.DelicateSynthesis, - "RemoveFinalAppraisal" => throw new Exception("Removing Final Appraisal is an unsupported action"), + "BasicSynthesis" => ActionType.BasicSynthesis, + "CarefulSynthesis" => ActionType.CarefulSynthesis, + "PrudentSynthesis" => ActionType.PrudentSynthesis, + "RapidSynthesis" => ActionType.RapidSynthesis, + "Groundwork" => ActionType.Groundwork, + "FocusedSynthesis" => ActionType.FocusedSynthesis, + "MuscleMemory" => ActionType.MuscleMemory, + "IntensiveSynthesis" => ActionType.IntensiveSynthesis, + "BasicTouch" => ActionType.BasicTouch, + "StandardTouch" => ActionType.StandardTouch, + "AdvancedTouch" => ActionType.AdvancedTouch, + "HastyTouch" => ActionType.HastyTouch, + "ByregotsBlessing" => ActionType.ByregotsBlessing, + "PreciseTouch" => ActionType.PreciseTouch, + "FocusedTouch" => ActionType.FocusedTouch, + "PrudentTouch" => ActionType.PrudentTouch, + "TrainedEye" => ActionType.TrainedEye, + "PreparatoryTouch" => ActionType.PreparatoryTouch, + "Reflect" => ActionType.Reflect, + "TrainedFinesse" => ActionType.TrainedFinesse, + "TricksOfTheTrade" => ActionType.TricksOfTheTrade, + "MastersMend" => ActionType.MastersMend, + "Manipulation" => ActionType.Manipulation, + "WasteNot" => ActionType.WasteNot, + "WasteNotII" => ActionType.WasteNot2, + "GreatStrides" => ActionType.GreatStrides, + "Innovation" => ActionType.Innovation, + "Veneration" => ActionType.Veneration, + "FinalAppraisal" => ActionType.FinalAppraisal, + "Observe" => ActionType.Observe, + "HeartAndSoul" => ActionType.HeartAndSoul, + "CarefulObservation" => ActionType.CarefulObservation, + "DelicateSynthesis" => ActionType.DelicateSynthesis, + "RemoveFinalAppraisal" => throw new Exception("Removing Final Appraisal is an unsupported action"), null => null, { } actionValue => throw new Exception($"Unknown action {actionValue}"), }; @@ -264,6 +290,69 @@ private static async Task RetrieveCraftingwayUrl(Uri uri, Cancel ConnectCallback = heCallback.ConnectCallback, }); - throw new NotImplementedException(); + // https://craftingway.app/rotation/variable-blueprint-KmrvS + + var path = uri.GetComponents(UriComponents.Path, UriFormat.SafeUnescaped); + if (!path.StartsWith("rotation/", StringComparison.Ordinal)) + throw new ArgumentException("Craftingway macro url should start with /rotation", nameof(uri)); + path = path[9..]; + + var lastSlash = path.LastIndexOf('/'); + if (lastSlash != -1) + throw new ArgumentException("Craftingway macro url is not in the right format", nameof(uri)); + + var id = path; + + var resp = await client.GetFromJsonAsync( + $"https://servingway.fly.dev/rotation/{id}", + token) + .ConfigureAwait(false); + if (resp is null) + throw new Exception("Internal error; failed to retrieve macro"); + if (resp.Error is { } error) + throw new Exception($"Internal server error; {error}"); + if (resp.Actions is not { } rotation) + throw new Exception($"Internal error; No actions or error was returned"); + // https://github.com/ffxiv-teamcraft/ffxiv-teamcraft/blob/67f453041c6b2b31d32fcf6e1fd53aa38ed7a12b/apps/client/src/app/model/other/crafting-rotation.ts#L49 + var name = resp.Slug ?? "New Craftinway Rotation"; + var actions = new List(); + foreach (var action in resp.Actions.Split(',')) + { + ActionType? actionType = action switch + { + "BasicSynthesis" => ActionType.BasicSynthesis, + "BasicTouch" => ActionType.BasicTouch, + "MastersMend" => ActionType.MastersMend, + "Observe" => ActionType.Observe, + "WasteNot" => ActionType.WasteNot, + "Veneration" => ActionType.Veneration, + "StandardTouch" => ActionType.StandardTouch, + "GreatStrides" => ActionType.GreatStrides, + "Innovation" => ActionType.Innovation, + "BasicSynthesisTraited" => ActionType.BasicSynthesis, + "WasteNotII" => ActionType.WasteNot2, + "ByregotsBlessing" => ActionType.ByregotsBlessing, + "MuscleMemory" => ActionType.MuscleMemory, + "CarefulSynthesis" => ActionType.CarefulSynthesis, + "Manipulation" => ActionType.Manipulation, + "PrudentTouch" => ActionType.PrudentTouch, + "FocusedSynthesis" => ActionType.FocusedSynthesis, + "FocusedTouch" => ActionType.FocusedTouch, + "Reflect" => ActionType.Reflect, + "PreparatoryTouch" => ActionType.PreparatoryTouch, + "Groundwork" => ActionType.Groundwork, + "DelicateSynthesis" => ActionType.DelicateSynthesis, + "TrainedEye" => ActionType.TrainedEye, + "CarefulSynthesisTraited" => ActionType.CarefulSynthesis, + "AdvancedTouch" => ActionType.AdvancedTouch, + "GroundworkTraited" => ActionType.Groundwork, + "PrudentSynthesis" => ActionType.PrudentSynthesis, + "TrainedFinesse" => ActionType.TrainedFinesse, + { } actionValue => throw new Exception($"Unknown action {actionValue}"), + }; + if (actionType.HasValue) + actions.Add(actionType.Value); + } + return new(name, actions); } } From fe187d9ae13269f235ea17cbefb1a2db77edb12b Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Wed, 25 Oct 2023 13:11:11 -0700 Subject: [PATCH 33/58] Clean up aggregate exception handling --- Craftimizer/Windows/MacroEditor.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Craftimizer/Windows/MacroEditor.cs b/Craftimizer/Windows/MacroEditor.cs index 9d4f9e1..0114ec7 100644 --- a/Craftimizer/Windows/MacroEditor.cs +++ b/Craftimizer/Windows/MacroEditor.cs @@ -1390,7 +1390,10 @@ private void DrawImportPopup() } catch (AggregateException e) { - popupImportError = e.Message; + if (e.InnerExceptions.Count == 1) + popupImportError = e.InnerExceptions[0].Message; + else + popupImportError = e.Message; Log.Error(e, "Retrieving macro failed"); } }, TaskContinuationOptions.OnlyOnFaulted); From c7f809767e76736f9a5f74fa7a6da3346bd34f58 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Wed, 25 Oct 2023 13:12:17 -0700 Subject: [PATCH 34/58] 1.9.1.0 --- Craftimizer/Craftimizer.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Craftimizer/Craftimizer.csproj b/Craftimizer/Craftimizer.csproj index 685b50d..9fd555d 100644 --- a/Craftimizer/Craftimizer.csproj +++ b/Craftimizer/Craftimizer.csproj @@ -2,7 +2,7 @@ Asriel Camora - 1.9.0.3 + 1.9.1.0 https://github.com/WorkingRobot/craftimizer.git From a8dcc9f6260fccc877a13b2dbcfb23c5ccca2d03 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 31 Oct 2023 12:45:26 -0700 Subject: [PATCH 35/58] Add test project --- Craftimizer.Test/Craftimizer.Test.csproj | 24 ++++++ Craftimizer.Test/Solver/ActionSet.cs | 104 +++++++++++++++++++++++ Craftimizer.Test/Usings.cs | 3 + Craftimizer.sln | 10 +++ 4 files changed, 141 insertions(+) create mode 100644 Craftimizer.Test/Craftimizer.Test.csproj create mode 100644 Craftimizer.Test/Solver/ActionSet.cs create mode 100644 Craftimizer.Test/Usings.cs diff --git a/Craftimizer.Test/Craftimizer.Test.csproj b/Craftimizer.Test/Craftimizer.Test.csproj new file mode 100644 index 0000000..5344fb4 --- /dev/null +++ b/Craftimizer.Test/Craftimizer.Test.csproj @@ -0,0 +1,24 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + + + + + + + + diff --git a/Craftimizer.Test/Solver/ActionSet.cs b/Craftimizer.Test/Solver/ActionSet.cs new file mode 100644 index 0000000..27061a0 --- /dev/null +++ b/Craftimizer.Test/Solver/ActionSet.cs @@ -0,0 +1,104 @@ +using Craftimizer.Simulator.Actions; + +namespace Craftimizer.Test.Solver; + +[TestClass] +public class ActionSetTests +{ + [TestMethod] + public void TestAcceptedActions() + { + var actions = Craftimizer.Solver.Simulator.AcceptedActions; + var lut = Craftimizer.Solver.Simulator.AcceptedActionsLUT; + + Assert.IsTrue(actions.Length <= 32); + foreach(var i in Enum.GetValues()) + { + var idx = lut[(byte)i]; + if (idx != 0) + Assert.AreEqual(i, actions[idx]); + } + } + + [TestMethod] + public void TestSize() + { + var set = new ActionSet(); + Assert.IsTrue(set.IsEmpty); + Assert.AreEqual(0, set.Count); + + set.AddAction(ActionType.BasicSynthesis); + set.AddAction(ActionType.WasteNot2); + + Assert.AreEqual(2, set.Count); + Assert.IsFalse(set.IsEmpty); + + set.RemoveAction(ActionType.BasicSynthesis); + set.RemoveAction(ActionType.WasteNot2); + + Assert.IsTrue(set.IsEmpty); + Assert.AreEqual(0, set.Count); + } + + [TestMethod] + public void TestAddRemove() + { + var set = new ActionSet(); + + Assert.IsTrue(set.AddAction(ActionType.BasicSynthesis)); + Assert.IsFalse(set.AddAction(ActionType.BasicSynthesis)); + + Assert.IsTrue(set.RemoveAction(ActionType.BasicSynthesis)); + Assert.IsFalse(set.RemoveAction(ActionType.BasicSynthesis)); + + Assert.IsTrue(set.AddAction(ActionType.BasicSynthesis)); + Assert.IsTrue(set.AddAction(ActionType.WasteNot2)); + + Assert.IsTrue(set.RemoveAction(ActionType.BasicSynthesis)); + Assert.IsTrue(set.RemoveAction(ActionType.WasteNot2)); + } + + [TestMethod] + public void TestHasAction() + { + var set = new ActionSet(); + + set.AddAction(ActionType.BasicSynthesis); + + Assert.IsTrue(set.HasAction(ActionType.BasicSynthesis)); + Assert.IsFalse(set.HasAction(ActionType.WasteNot2)); + + set.AddAction(ActionType.WasteNot2); + Assert.IsTrue(set.HasAction(ActionType.BasicSynthesis)); + Assert.IsTrue(set.HasAction(ActionType.WasteNot2)); + + set.RemoveAction(ActionType.BasicSynthesis); + Assert.IsFalse(set.HasAction(ActionType.BasicSynthesis)); + Assert.IsTrue(set.HasAction(ActionType.WasteNot2)); + } + + [TestMethod] + public void TestElementAt() + { + var set = new ActionSet(); + + set.AddAction(ActionType.BasicSynthesis); + set.AddAction(ActionType.ByregotsBlessing); + set.AddAction(ActionType.DelicateSynthesis); + set.AddAction(ActionType.FocusedTouch); + + Assert.AreEqual(3, set.Count); + + Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(0)); + Assert.AreEqual(ActionType.FocusedTouch, set.ElementAt(1)); + Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(2)); + Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(3)); + + set.RemoveAction(ActionType.FocusedTouch); + + Assert.AreEqual(3, set.Count); + Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(0)); + Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(1)); + Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(2)); + } +} diff --git a/Craftimizer.Test/Usings.cs b/Craftimizer.Test/Usings.cs new file mode 100644 index 0000000..da624f8 --- /dev/null +++ b/Craftimizer.Test/Usings.cs @@ -0,0 +1,3 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Craftimizer.Solver; +global using Craftimizer.Simulator; diff --git a/Craftimizer.sln b/Craftimizer.sln index 358354c..9b6d4a8 100644 --- a/Craftimizer.sln +++ b/Craftimizer.sln @@ -17,6 +17,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Solver", "Solve {172EE849-AC7E-4F2A-ACAB-EF9D065523B3} = {172EE849-AC7E-4F2A-ACAB-EF9D065523B3} EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Craftimizer.Test", "Craftimizer.Test\Craftimizer.Test.csproj", "{C3AEA981-9DA8-405C-995B-86528493891B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -57,6 +59,14 @@ Global {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|Any CPU.Build.0 = Release|Any CPU {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.ActiveCfg = Release|Any CPU {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.Build.0 = Release|Any CPU + {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.ActiveCfg = Debug|Any CPU + {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.Build.0 = Debug|Any CPU + {C3AEA981-9DA8-405C-995B-86528493891B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3AEA981-9DA8-405C-995B-86528493891B}.Release|Any CPU.Build.0 = Release|Any CPU + {C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.ActiveCfg = Release|Any CPU + {C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From a382da066eadaa8e1717778979b58e933a1d8692 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 31 Oct 2023 12:46:56 -0700 Subject: [PATCH 36/58] Add tests CI/CD --- .github/workflows/build.yml | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 21087eb..519081e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,7 @@ name: Build on: - push: - tags: - - '*' + push env: PLUGIN_REPO: WorkingRobot/MyDalamudPlugins @@ -12,6 +10,8 @@ env: jobs: build: runs-on: windows-latest + env: + DOTNET_CLI_TELEMETRY_OPTOUT: true steps: - name: Checkout @@ -20,34 +20,49 @@ jobs: submodules: recursive - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1.0.3 + uses: microsoft/setup-msbuild@v1 - name: Download Dalamud run: | Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev\" - - name: Build + - name: Restore run: | dotnet restore -r win ${{env.PROJECT_NAME}}.sln + + - name: Build + run: | dotnet build --configuration Release - env: - DOTNET_CLI_TELEMETRY_OUTPUT: true - - name: Upload Artifact - uses: actions/upload-artifact@v2.2.1 + - name: Test + run: | + dotnet test --configuration Release --logger "trx;logfilename=results.trx" --logger "html;logfilename=results.html" --logger "console;verbosity=detailed" --results-directory="TestResults" + + - name: Upload Artifacts + uses: actions/upload-artifact@v3 with: name: ${{env.PROJECT_NAME}} path: ${{env.PROJECT_NAME}}/bin/x64/Release/${{env.PROJECT_NAME}} if-no-files-found: error + - name: Upload Test Results + uses: actions/upload-artifact@v3 + if: ${{ always() }} + with: + name: ${{env.PROJECT_NAME}} + path: TestResults + - name: Create Release uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + id: release with: files: ${{env.PROJECT_NAME}}/bin/x64/Release/${{env.PROJECT_NAME}}/* - name: Trigger Plugin Repo Update - uses: peter-evans/repository-dispatch@v1 + uses: peter-evans/repository-dispatch@v2 + if: ${{ steps.release.conclusion == 'success' }} with: token: ${{secrets.PAT}} repository: ${{env.PLUGIN_REPO}} From 439a174d3401e280e3d68c0a2c98b24062203826 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 31 Oct 2023 12:47:30 -0700 Subject: [PATCH 37/58] Change ActionSet to use pragmas --- Solver/ActionSet.cs | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/Solver/ActionSet.cs b/Solver/ActionSet.cs index 81de8c4..c7aaf34 100644 --- a/Solver/ActionSet.cs +++ b/Solver/ActionSet.cs @@ -5,21 +5,27 @@ namespace Craftimizer.Solver; +// #define IS_DETERMINISTIC + public struct ActionSet { - private const bool IsDeterministic = false; - private uint bits; [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int FromAction(ActionType action) => Simulator.AcceptedActionsLUT[(byte)action]; + private static int FromAction(ActionType action) + { + var ret = Simulator.AcceptedActionsLUT[(byte)action]; + if (ret == 0) + throw new ArgumentOutOfRangeException(nameof(action), action, $"Action {action} is unsupported in {nameof(ActionSet)}."); + return ret; + } [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ActionType ToAction(int index) => Simulator.AcceptedActions[index]; [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint ToMask(ActionType action) => 1u << FromAction(action) + 1; + private static uint ToMask(ActionType action) => 1u << (FromAction(action) + 1); // Return true if action was newly added and not there before. [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -57,25 +63,28 @@ public bool RemoveAction(ActionType action) [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly ActionType SelectRandom(Random random) { - if (IsDeterministic) - return First(); - +#if IS_DETERMINISTIC + return First(); +#else return ElementAt(random.Next(Count)); +#endif } [MethodImpl(MethodImplOptions.AggressiveInlining)] public ActionType PopRandom(Random random) { - if (IsDeterministic) - return PopFirst(); - +#if IS_DETERMINISTIC + return PopFirst(); +#else var action = ElementAt(random.Next(Count)); RemoveAction(action); return action; +#endif } +#if IS_DETERMINISTIC [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ActionType PopFirst() + private ActionType PopFirst() { var action = First(); RemoveAction(action); @@ -84,5 +93,6 @@ public ActionType PopFirst() [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly ActionType First() => ElementAt(0); + private readonly ActionType First() => ElementAt(0); +#endif } From a4dfcc448860eaa23a83f5f8367fc268aa117744 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 31 Oct 2023 12:55:00 -0700 Subject: [PATCH 38/58] Fix typo --- Craftimizer.Test/Solver/ActionSet.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Craftimizer.Test/Solver/ActionSet.cs b/Craftimizer.Test/Solver/ActionSet.cs index 27061a0..d76f372 100644 --- a/Craftimizer.Test/Solver/ActionSet.cs +++ b/Craftimizer.Test/Solver/ActionSet.cs @@ -87,7 +87,7 @@ public void TestElementAt() set.AddAction(ActionType.DelicateSynthesis); set.AddAction(ActionType.FocusedTouch); - Assert.AreEqual(3, set.Count); + Assert.AreEqual(4, set.Count); Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(0)); Assert.AreEqual(ActionType.FocusedTouch, set.ElementAt(1)); From 9f46eff516f31953a22b963b2496c56517a0161d Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 31 Oct 2023 14:09:16 -0700 Subject: [PATCH 39/58] Change test artifact name --- .github/workflows/build.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 519081e..96b8af1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: submodules: recursive @@ -50,7 +50,6 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ always() }} with: - name: ${{env.PROJECT_NAME}} path: TestResults - name: Create Release From 706f5c98cab67c324d73d74b3f3ced5319b03054 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 31 Oct 2023 14:10:00 -0700 Subject: [PATCH 40/58] Switch to Ubuntu --- .github/workflows/build.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 96b8af1..5936f2f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ env: jobs: build: - runs-on: windows-latest + runs-on: ubuntu-latest env: DOTNET_CLI_TELEMETRY_OPTOUT: true @@ -19,13 +19,16 @@ jobs: with: submodules: recursive - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '7.0' - name: Download Dalamud run: | - Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip - Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev\" + wget https://goatcorp.github.io/dalamud-distrib/stg/latest.zip + unzip latest.zip -d dalamud/ + echo "DALAMUD_HOME=$PWD/dalamud" >> $GITHUB_ENV - name: Restore run: | From 23c49d44531456d504a9ab971e48d75ef54eb4c9 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 31 Oct 2023 14:13:43 -0700 Subject: [PATCH 41/58] Fix test artifact name --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5936f2f..2a5f9e7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,6 +53,7 @@ jobs: uses: actions/upload-artifact@v3 if: ${{ always() }} with: + name: TestResults path: TestResults - name: Create Release From 256a72db8892318b68cb2e97bd1cbf221ce8afbe Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 31 Oct 2023 15:41:09 -0700 Subject: [PATCH 42/58] Add more actionset tests --- .github/workflows/build.yml | 4 ++-- Craftimizer.Test/Solver/ActionSet.cs | 36 +++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2a5f9e7..848adc5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,11 +36,11 @@ jobs: - name: Build run: | - dotnet build --configuration Release + dotnet build --configuration Release --no-restore - name: Test run: | - dotnet test --configuration Release --logger "trx;logfilename=results.trx" --logger "html;logfilename=results.html" --logger "console;verbosity=detailed" --results-directory="TestResults" + dotnet test --configuration Release --logger "trx;logfilename=results.trx" --logger "html;logfilename=results.html" --logger "console;verbosity=detailed" --no-build --results-directory="TestResults" - name: Upload Artifacts uses: actions/upload-artifact@v3 diff --git a/Craftimizer.Test/Solver/ActionSet.cs b/Craftimizer.Test/Solver/ActionSet.cs index d76f372..87a00ee 100644 --- a/Craftimizer.Test/Solver/ActionSet.cs +++ b/Craftimizer.Test/Solver/ActionSet.cs @@ -12,7 +12,7 @@ public void TestAcceptedActions() var lut = Craftimizer.Solver.Simulator.AcceptedActionsLUT; Assert.IsTrue(actions.Length <= 32); - foreach(var i in Enum.GetValues()) + foreach (var i in Enum.GetValues()) { var idx = lut[(byte)i]; if (idx != 0) @@ -29,7 +29,7 @@ public void TestSize() set.AddAction(ActionType.BasicSynthesis); set.AddAction(ActionType.WasteNot2); - + Assert.AreEqual(2, set.Count); Assert.IsFalse(set.IsEmpty); @@ -50,7 +50,7 @@ public void TestAddRemove() Assert.IsTrue(set.RemoveAction(ActionType.BasicSynthesis)); Assert.IsFalse(set.RemoveAction(ActionType.BasicSynthesis)); - + Assert.IsTrue(set.AddAction(ActionType.BasicSynthesis)); Assert.IsTrue(set.AddAction(ActionType.WasteNot2)); @@ -101,4 +101,34 @@ public void TestElementAt() Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(1)); Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(2)); } + + [TestMethod] + public void TestRandomIndex() + { + var actions = new[] + { + ActionType.BasicTouch, + ActionType.BasicSynthesis, + ActionType.GreatStrides, + ActionType.TrainedFinesse, + }; + + var set = new ActionSet(); + foreach(var action in actions) + set.AddAction(action); + + var counts = new Dictionary(); + var rng = new Random(0); + for (var i = 0; i < 100; i++) + { + var action = set.SelectRandom(rng); + + Assert.IsTrue(actions.Contains(action)); + + counts[action] = counts.GetValueOrDefault(action) + 1; + } + + foreach (var action in actions) + Assert.IsTrue(counts.GetValueOrDefault(action) > 0); + } } From 239f71db58c4a37bcf96f686d4905f9f3f9077a9 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Wed, 1 Nov 2023 20:07:07 -0700 Subject: [PATCH 43/58] Add unofficial builds --- .github/create_unofficial.py | 27 +++++++++++++++++++++++++++ .github/workflows/build.yml | 9 +++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 .github/create_unofficial.py diff --git a/.github/create_unofficial.py b/.github/create_unofficial.py new file mode 100644 index 0000000..2497469 --- /dev/null +++ b/.github/create_unofficial.py @@ -0,0 +1,27 @@ +import shutil, os, subprocess, zipfile, json, sys + +from itertools import chain + +PROJECT_NAME = sys.argv[1] +OFFICIAL_ZIP = f"{PROJECT_NAME}/bin/x64/Release/{PROJECT_NAME}/latest.zip" +UNOFFICIAL_ZIP = f"{PROJECT_NAME}/bin/x64/Release/{PROJECT_NAME}/latestUnofficial.zip" + +shutil.copy(OFFICIAL_ZIP, UNOFFICIAL_ZIP) + +subprocess.check_call(['7z', 'd', UNOFFICIAL_ZIP, f"{PROJECT_NAME}.json"]) + +with zipfile.ZipFile(UNOFFICIAL_ZIP) as file: + members = file.namelist() + +subprocess.check_call(['7z', 'rn', UNOFFICIAL_ZIP] + list(chain.from_iterable((m, m.replace(PROJECT_NAME, f"{PROJECT_NAME}Unofficial")) for m in members))) + +with open(f"{PROJECT_NAME}/bin/x64/Release/{PROJECT_NAME}/{PROJECT_NAME}.json") as file: + manifest = json.load(file) + +manifest['Punchline'] = f"Unofficial/uncertified build of {manifest['Name']}. {manifest['Punchline']}" +manifest['InternalName'] += 'Unofficial' +manifest['Name'] += ' (Unofficial)' +manifest['IconUrl'] = f"https://raw.githubusercontent.com/WorkingRobot/MyDalamudPlugins/main/icons/{manifest['InternalName']}.png" + +with zipfile.ZipFile(UNOFFICIAL_ZIP, "a", zipfile.ZIP_DEFLATED, compresslevel = 7) as file: + file.writestr(f"{PROJECT_NAME}Unofficial.json", json.dumps(manifest, indent = 2)) \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 848adc5..8613830 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,7 @@ on: env: PLUGIN_REPO: WorkingRobot/MyDalamudPlugins PROJECT_NAME: Craftimizer + IS_OFFICIAL: ${{true}} jobs: build: @@ -32,7 +33,7 @@ jobs: - name: Restore run: | - dotnet restore -r win ${{env.PROJECT_NAME}}.sln + dotnet restore -r win - name: Build run: | @@ -42,6 +43,10 @@ jobs: run: | dotnet test --configuration Release --logger "trx;logfilename=results.trx" --logger "html;logfilename=results.html" --logger "console;verbosity=detailed" --no-build --results-directory="TestResults" + - name: Create Unofficial Builds + if: ${{env.IS_OFFICIAL}} + run: python ./.github/create_unofficial.py ${{env.PROJECT_NAME}} + - name: Upload Artifacts uses: actions/upload-artifact@v3 with: @@ -51,7 +56,7 @@ jobs: - name: Upload Test Results uses: actions/upload-artifact@v3 - if: ${{ always() }} + if: ${{ !cancelled() }} with: name: TestResults path: TestResults From 74195b59ebf39957098259519879d6fe7b6ceb1e Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Thu, 2 Nov 2023 00:36:49 -0700 Subject: [PATCH 44/58] Fix failed to install --- .github/create_unofficial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/create_unofficial.py b/.github/create_unofficial.py index 2497469..778fa47 100644 --- a/.github/create_unofficial.py +++ b/.github/create_unofficial.py @@ -11,7 +11,7 @@ subprocess.check_call(['7z', 'd', UNOFFICIAL_ZIP, f"{PROJECT_NAME}.json"]) with zipfile.ZipFile(UNOFFICIAL_ZIP) as file: - members = file.namelist() + members = [member for member in file.namelist() if member in (f"{PROJECT_NAME}.dll", f"{PROJECT_NAME}.deps.json", f"{PROJECT_NAME}.json", f"{PROJECT_NAME}.pdb")] subprocess.check_call(['7z', 'rn', UNOFFICIAL_ZIP] + list(chain.from_iterable((m, m.replace(PROJECT_NAME, f"{PROJECT_NAME}Unofficial")) for m in members))) From ac109ae7119d385e1d15a43ee225d1c44fe592fd Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Thu, 2 Nov 2023 01:01:03 -0700 Subject: [PATCH 45/58] Fix ActionSet --- Craftimizer.Test/Solver/ActionSet.cs | 2 +- Solver/ActionSet.cs | 2 +- Solver/Simulator.cs | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Craftimizer.Test/Solver/ActionSet.cs b/Craftimizer.Test/Solver/ActionSet.cs index 87a00ee..e6f07d0 100644 --- a/Craftimizer.Test/Solver/ActionSet.cs +++ b/Craftimizer.Test/Solver/ActionSet.cs @@ -15,7 +15,7 @@ public void TestAcceptedActions() foreach (var i in Enum.GetValues()) { var idx = lut[(byte)i]; - if (idx != 0) + if (idx != -1) Assert.AreEqual(i, actions[idx]); } } diff --git a/Solver/ActionSet.cs b/Solver/ActionSet.cs index c7aaf34..248ba13 100644 --- a/Solver/ActionSet.cs +++ b/Solver/ActionSet.cs @@ -16,7 +16,7 @@ public struct ActionSet private static int FromAction(ActionType action) { var ret = Simulator.AcceptedActionsLUT[(byte)action]; - if (ret == 0) + if (ret == -1) throw new ArgumentOutOfRangeException(nameof(action), action, $"Action {action} is unsupported in {nameof(ActionSet)}."); return ret; } diff --git a/Solver/Simulator.cs b/Solver/Simulator.cs index b3f6f60..23ab77c 100644 --- a/Solver/Simulator.cs +++ b/Solver/Simulator.cs @@ -63,6 +63,8 @@ public Simulator(SimulationState state, int maxStepCount) : base(state) static Simulator() { AcceptedActionsLUT = new int[Enum.GetValues().Length]; + for (var i = 0; i < AcceptedActionsLUT.Length; i++) + AcceptedActionsLUT[i] = -1; for (var i = 0; i < AcceptedActions.Length; i++) AcceptedActionsLUT[(byte)AcceptedActions[i]] = i; } From 2f727f76cd9418c6030bfdf83d4b1e3995a5d618 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sat, 4 Nov 2023 11:01:15 -0700 Subject: [PATCH 46/58] Move accepted actions to ActionSet --- Craftimizer.Test/Solver/ActionSet.cs | 6 ++-- Craftimizer/Windows/RecipeNote.cs | 2 +- Solver/ActionSet.cs | 44 ++++++++++++++++++++++++- Solver/Simulator.cs | 48 ++-------------------------- 4 files changed, 49 insertions(+), 51 deletions(-) diff --git a/Craftimizer.Test/Solver/ActionSet.cs b/Craftimizer.Test/Solver/ActionSet.cs index e6f07d0..30e4bef 100644 --- a/Craftimizer.Test/Solver/ActionSet.cs +++ b/Craftimizer.Test/Solver/ActionSet.cs @@ -8,8 +8,8 @@ public class ActionSetTests [TestMethod] public void TestAcceptedActions() { - var actions = Craftimizer.Solver.Simulator.AcceptedActions; - var lut = Craftimizer.Solver.Simulator.AcceptedActionsLUT; + var actions = ActionSet.AcceptedActions; + var lut = ActionSet.AcceptedActionsLUT; Assert.IsTrue(actions.Length <= 32); foreach (var i in Enum.GetValues()) @@ -123,7 +123,7 @@ public void TestRandomIndex() { var action = set.SelectRandom(rng); - Assert.IsTrue(actions.Contains(action)); + CollectionAssert.Contains(actions, action); counts[action] = counts.GetValueOrDefault(action) + 1; } diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index ee3d3c9..2562e9c 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -839,7 +839,7 @@ private void CalculateBestMacrosTask(CancellationToken token) var state = new SimulationState(input); var config = Service.Configuration.SimulatorSolverConfig; var mctsConfig = new MCTSConfig(config); - var simulator = new Solver.Simulator(state, mctsConfig.MaxStepCount); + var simulator = new SimulatorNoRandom(state); List macros = new(Service.Configuration.Macros); token.ThrowIfCancellationRequested(); diff --git a/Solver/ActionSet.cs b/Solver/ActionSet.cs index 248ba13..43b5874 100644 --- a/Solver/ActionSet.cs +++ b/Solver/ActionSet.cs @@ -11,6 +11,48 @@ public struct ActionSet { private uint bits; + public static readonly ActionType[] AcceptedActions = new[] + { + ActionType.StandardTouchCombo, + ActionType.AdvancedTouchCombo, + ActionType.FocusedTouchCombo, + ActionType.FocusedSynthesisCombo, + ActionType.TrainedFinesse, + ActionType.PrudentSynthesis, + ActionType.Groundwork, + ActionType.AdvancedTouch, + ActionType.CarefulSynthesis, + ActionType.TrainedEye, + ActionType.DelicateSynthesis, + ActionType.PreparatoryTouch, + ActionType.Reflect, + ActionType.FocusedTouch, + ActionType.FocusedSynthesis, + ActionType.PrudentTouch, + ActionType.Manipulation, + ActionType.MuscleMemory, + ActionType.ByregotsBlessing, + ActionType.WasteNot2, + ActionType.BasicSynthesis, + ActionType.Innovation, + ActionType.GreatStrides, + ActionType.StandardTouch, + ActionType.Veneration, + ActionType.WasteNot, + ActionType.Observe, + ActionType.MastersMend, + ActionType.BasicTouch, + }; + + public static readonly int[] AcceptedActionsLUT; + + static ActionSet() + { + AcceptedActionsLUT = new int[Enum.GetValues().Length]; + for (var i = 0; i < AcceptedActions.Length; i++) + AcceptedActionsLUT[(byte)AcceptedActions[i]] = i; + } + [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int FromAction(ActionType action) @@ -22,7 +64,7 @@ private static int FromAction(ActionType action) } [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ActionType ToAction(int index) => Simulator.AcceptedActions[index]; + private static ActionType ToAction(int index) => AcceptedActions[index]; [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint ToMask(ActionType action) => 1u << (FromAction(action) + 1); diff --git a/Solver/Simulator.cs b/Solver/Simulator.cs index 23ab77c..8edf3e4 100644 --- a/Solver/Simulator.cs +++ b/Solver/Simulator.cs @@ -5,7 +5,7 @@ namespace Craftimizer.Solver; -public sealed class Simulator : SimulatorNoRandom +internal sealed class Simulator : SimulatorNoRandom { private readonly int maxStepCount; @@ -25,50 +25,6 @@ public Simulator(SimulationState state, int maxStepCount) : base(state) this.maxStepCount = maxStepCount; } - public static readonly ActionType[] AcceptedActions = new[] - { - ActionType.StandardTouchCombo, - ActionType.AdvancedTouchCombo, - ActionType.FocusedTouchCombo, - ActionType.FocusedSynthesisCombo, - ActionType.TrainedFinesse, - ActionType.PrudentSynthesis, - ActionType.Groundwork, - ActionType.AdvancedTouch, - ActionType.CarefulSynthesis, - ActionType.TrainedEye, - ActionType.DelicateSynthesis, - ActionType.PreparatoryTouch, - ActionType.Reflect, - ActionType.FocusedTouch, - ActionType.FocusedSynthesis, - ActionType.PrudentTouch, - ActionType.Manipulation, - ActionType.MuscleMemory, - ActionType.ByregotsBlessing, - ActionType.WasteNot2, - ActionType.BasicSynthesis, - ActionType.Innovation, - ActionType.GreatStrides, - ActionType.StandardTouch, - ActionType.Veneration, - ActionType.WasteNot, - ActionType.Observe, - ActionType.MastersMend, - ActionType.BasicTouch, - }; - - public static readonly int[] AcceptedActionsLUT; - - static Simulator() - { - AcceptedActionsLUT = new int[Enum.GetValues().Length]; - for (var i = 0; i < AcceptedActionsLUT.Length; i++) - AcceptedActionsLUT[i] = -1; - for (var i = 0; i < AcceptedActions.Length; i++) - AcceptedActionsLUT[(byte)AcceptedActions[i]] = i; - } - // https://github.com/alostsock/crafty/blob/cffbd0cad8bab3cef9f52a3e3d5da4f5e3781842/crafty/src/craft_state.rs#L146 [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -191,7 +147,7 @@ public ActionSet AvailableActionsHeuristic(bool strict) return new(); var ret = new ActionSet(); - foreach (var action in AcceptedActions) + foreach (var action in ActionSet.AcceptedActions) if (CanUseAction(action, strict)) ret.AddAction(action); return ret; From 9ca83bfe23c2a18d517016a0a66836dd588aa98b Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sat, 4 Nov 2023 11:01:39 -0700 Subject: [PATCH 47/58] Simulator tests (will fail!) --- Craftimizer.Test/Simulator/Simulator.cs | 235 ++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 Craftimizer.Test/Simulator/Simulator.cs diff --git a/Craftimizer.Test/Simulator/Simulator.cs b/Craftimizer.Test/Simulator/Simulator.cs new file mode 100644 index 0000000..f628654 --- /dev/null +++ b/Craftimizer.Test/Simulator/Simulator.cs @@ -0,0 +1,235 @@ +using Craftimizer.Simulator.Actions; +using System; +using System.Reflection; + +namespace Craftimizer.Test.Simulator; + +[TestClass] +public class SimulatorTests +{ + // https://craftingway.app/rotation/loud-namazu-jVe9Y + // Chondrite Saw + private static SimulationInput Input1 { get; } = + new(new() + { + Craftsmanship = 3304, + Control = 3374, + CP = 575, + Level = 90, + CanUseManipulation = true, + HasSplendorousBuff = false, + IsSpecialist = false, + CLvl = 560, + }, + new() + { + IsExpert = false, + ClassJobLevel = 90, + RLvl = 560, + ConditionsFlag = 0b1111, + MaxDurability = 80, + MaxQuality = 7200, + MaxProgress = 3500, + QualityModifier = 80, + QualityDivider = 115, + ProgressModifier = 90, + ProgressDivider = 130 + }); + + // Conflicting Info: + // https://craftingway.app/rotation/sandy-fafnir-doVCs + // Classical Longsword + private static SimulationInput Input2 { get; } = + new(new() + { + Craftsmanship = 3290, + Control = 3541, + CP = 649, + Level = 90, + CanUseManipulation = true, + HasSplendorousBuff = false, + IsSpecialist = false, + CLvl = 560, + }, + new() + { + IsExpert = false, + ClassJobLevel = 90, + RLvl = 580, + ConditionsFlag = 0b1111, + MaxDurability = 70, + MaxQuality = 10920, + MaxProgress = 3900, + QualityModifier = 70, + QualityDivider = 115, + ProgressModifier = 80, + ProgressDivider = 130 + }); + + private static SimulationState AssertCraft(SimulationInput input, IEnumerable actions, + int progress, int quality, + int durability, int cp) + { + var simulator = new SimulatorNoRandom(new(input)); + var (_, state, _) = simulator.ExecuteMultiple(new(input), actions); + Assert.AreEqual(progress, state.Progress); + Assert.AreEqual(quality, state.Quality); + Assert.AreEqual(durability, state.Durability); + Assert.AreEqual(cp, state.CP); + return state; + } + + [TestMethod] + public void BasicActions() + { + AssertCraft( + Input1, + new[] { + ActionType.BasicTouch, + ActionType.BasicSynthesis, + ActionType.MastersMend + }, + 276, 262, 80, 469); + } + + [TestMethod] + public void BasicTouchCombo() + { + AssertCraft( + Input1, + new[] { + ActionType.Innovation, + ActionType.BasicTouch, + ActionType.StandardTouch, + ActionType.AdvancedTouch, + ActionType.StandardTouch, + ActionType.AdvancedTouch + }, + 0, 2828, 30, 425); + } + + [TestMethod] + public void WithBuffs1() + { + AssertCraft( + Input1, + new[] { + ActionType.Reflect, + ActionType.Manipulation, + ActionType.PreparatoryTouch, + ActionType.WasteNot2 + }, + 0, 890, 60, 335); + } + + [TestMethod] + public void WithBuffs2() + { + AssertCraft( + Input1, + new[] { + ActionType.MuscleMemory, + ActionType.GreatStrides, + ActionType.PrudentTouch, + ActionType.DelicateSynthesis + }, + 1150, 812, 55, 480); + } + + [TestMethod] + public void WithBuffs3() + { + AssertCraft( + Input1, + new[] { + ActionType.MuscleMemory, + ActionType.Manipulation, + ActionType.MastersMend, + ActionType.WasteNot2, + ActionType.Innovation, + ActionType.DelicateSynthesis, + ActionType.BasicTouch, + ActionType.GreatStrides, + ActionType.ByregotsBlessing + }, + 1150, 1925, 80, 163); + } + + [TestMethod] + public void TrainedFinesseProcs() + { + var state = AssertCraft( + Input1, + new[] { + ActionType.Reflect, + ActionType.WasteNot, + ActionType.PreparatoryTouch, + ActionType.PreparatoryTouch, + ActionType.BasicTouch, + ActionType.StandardTouch, + ActionType.PrudentTouch, + ActionType.PreparatoryTouch + }, + 0, 4064, 15, 332); + Assert.AreEqual(10, state.ActiveEffects.InnerQuiet); + Assert.IsTrue(ActionType.TrainedFinesse.Base().CanUse(new SimulatorNoRandom(state))); + } + + [TestMethod] + public void TestCompletedCraft1() + { + AssertCraft( + Input1, + new[] { + ActionType.Reflect, + ActionType.Manipulation, + ActionType.PreparatoryTouch, + ActionType.WasteNot2, + ActionType.PreparatoryTouch, + ActionType.Innovation, + ActionType.PreparatoryTouch, + ActionType.PreparatoryTouch, + ActionType.GreatStrides, + ActionType.ByregotsBlessing, + ActionType.Veneration, + ActionType.Groundwork, + ActionType.Groundwork, + ActionType.Groundwork, + }, + 3726, 8224, 5, 69); + } + + [TestMethod] + public void TestCompletedCraft2() + { + Console.WriteLine($"{Input2.BaseProgressGain} {Input2.BaseProgressGain * (3.6f * 2.5f)}"); + Console.WriteLine($"{(int)(Input2.BaseProgressGain * (3.6f * 2.5f))} {(int)MathF.Floor(Input2.BaseProgressGain * (3.6f * 2.5f))}"); + AssertCraft( + Input2, + new[] { + ActionType.MuscleMemory, + ActionType.Manipulation, + ActionType.Veneration, + ActionType.WasteNot2, + ActionType.Groundwork, + ActionType.Groundwork, + ActionType.StandardTouch, + ActionType.Innovation, + ActionType.PreparatoryTouch, + ActionType.PreparatoryTouch, + ActionType.PreparatoryTouch, + ActionType.PreparatoryTouch, + ActionType.GreatStrides, + ActionType.Innovation, + ActionType.PreparatoryTouch, + ActionType.TrainedFinesse, + ActionType.GreatStrides, + ActionType.ByregotsBlessing + }, + // Conflicting Info: + // TC https://ffxivteamcraft.com/simulator/35020/34800/4PTlwTV6w1aGCUdO2BRl + 3549, 10932, 5, 7); + // Craftingway https://craftingway.app/rotation/sandy-fafnir-doVCs + //3548, 10931, 5, 7); + } +} From d08fedb247607855460e08ed012795348d866cfe Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sat, 4 Nov 2023 11:43:34 -0700 Subject: [PATCH 48/58] Use fixed point arithmetic for progress/quality --- Simulator/Actions/AdvancedTouch.cs | 2 +- Simulator/Actions/BaseAction.cs | 2 +- Simulator/Actions/BasicSynthesis.cs | 2 +- Simulator/Actions/BasicTouch.cs | 2 +- Simulator/Actions/ByregotsBlessing.cs | 2 +- Simulator/Actions/CarefulSynthesis.cs | 2 +- Simulator/Actions/DelicateSynthesis.cs | 2 +- Simulator/Actions/FocusedSynthesis.cs | 2 +- Simulator/Actions/FocusedTouch.cs | 2 +- Simulator/Actions/Groundwork.cs | 4 +-- Simulator/Actions/HastyTouch.cs | 2 +- Simulator/Actions/IntensiveSynthesis.cs | 2 +- Simulator/Actions/MuscleMemory.cs | 2 +- Simulator/Actions/PreciseTouch.cs | 2 +- Simulator/Actions/PreparatoryTouch.cs | 2 +- Simulator/Actions/PrudentSynthesis.cs | 2 +- Simulator/Actions/PrudentTouch.cs | 2 +- Simulator/Actions/RapidSynthesis.cs | 2 +- Simulator/Actions/Reflect.cs | 2 +- Simulator/Actions/StandardTouch.cs | 2 +- Simulator/Actions/TrainedFinesse.cs | 2 +- Simulator/Simulator.cs | 38 ++++++++++++------------- Solver/ActionSet.cs | 4 ++- 23 files changed, 44 insertions(+), 42 deletions(-) diff --git a/Simulator/Actions/AdvancedTouch.cs b/Simulator/Actions/AdvancedTouch.cs index 0b5eb79..ec1f0af 100644 --- a/Simulator/Actions/AdvancedTouch.cs +++ b/Simulator/Actions/AdvancedTouch.cs @@ -9,5 +9,5 @@ internal sealed class AdvancedTouch : BaseAction public override bool IncreasesQuality => true; public override int CPCost(Simulator s) => s.ActionStates.TouchComboIdx == 2 ? 18 : 46; - public override float Efficiency(Simulator s) => 1.50f; + public override int Efficiency(Simulator s) => 150; } diff --git a/Simulator/Actions/BaseAction.cs b/Simulator/Actions/BaseAction.cs index 8b3edb1..8e006be 100644 --- a/Simulator/Actions/BaseAction.cs +++ b/Simulator/Actions/BaseAction.cs @@ -22,7 +22,7 @@ public abstract class BaseAction // Instanced properties public abstract int CPCost(Simulator s); - public virtual float Efficiency(Simulator s) => 0f; + public virtual int Efficiency(Simulator s) => 0; public virtual float SuccessRate(Simulator s) => 1f; public virtual bool CanUse(Simulator s) => diff --git a/Simulator/Actions/BasicSynthesis.cs b/Simulator/Actions/BasicSynthesis.cs index e3565c2..2ad8d82 100644 --- a/Simulator/Actions/BasicSynthesis.cs +++ b/Simulator/Actions/BasicSynthesis.cs @@ -10,5 +10,5 @@ internal sealed class BasicSynthesis : BaseAction public override int CPCost(Simulator s) => 0; // Basic Synthesis Mastery Trait - public override float Efficiency(Simulator s) => s.Input.Stats.Level >= 31 ? 1.20f : 1.00f; + public override int Efficiency(Simulator s) => s.Input.Stats.Level >= 31 ? 120 : 100; } diff --git a/Simulator/Actions/BasicTouch.cs b/Simulator/Actions/BasicTouch.cs index ae13294..e3a2434 100644 --- a/Simulator/Actions/BasicTouch.cs +++ b/Simulator/Actions/BasicTouch.cs @@ -9,5 +9,5 @@ internal sealed class BasicTouch : BaseAction public override bool IncreasesQuality => true; public override int CPCost(Simulator s) => 18; - public override float Efficiency(Simulator s) => 1.00f; + public override int Efficiency(Simulator s) => 100; } diff --git a/Simulator/Actions/ByregotsBlessing.cs b/Simulator/Actions/ByregotsBlessing.cs index da20ce8..257db8e 100644 --- a/Simulator/Actions/ByregotsBlessing.cs +++ b/Simulator/Actions/ByregotsBlessing.cs @@ -9,7 +9,7 @@ internal sealed class ByregotsBlessing : BaseAction public override bool IncreasesQuality => true; public override int CPCost(Simulator s) => 24; - public override float Efficiency(Simulator s) => 1.00f + (0.20f * s.GetEffectStrength(EffectType.InnerQuiet)); + public override int Efficiency(Simulator s) => 100 + (20 * s.GetEffectStrength(EffectType.InnerQuiet)); public override bool CanUse(Simulator s) => s.HasEffect(EffectType.InnerQuiet) && base.CanUse(s); diff --git a/Simulator/Actions/CarefulSynthesis.cs b/Simulator/Actions/CarefulSynthesis.cs index b72ea55..4449583 100644 --- a/Simulator/Actions/CarefulSynthesis.cs +++ b/Simulator/Actions/CarefulSynthesis.cs @@ -10,5 +10,5 @@ internal sealed class CarefulSynthesis : BaseAction public override int CPCost(Simulator s) => 7; // Careful Synthesis Mastery Trait - public override float Efficiency(Simulator s) => s.Input.Stats.Level >= 82 ? 1.80f : 1.50f; + public override int Efficiency(Simulator s) => s.Input.Stats.Level >= 82 ? 180 : 150; } diff --git a/Simulator/Actions/DelicateSynthesis.cs b/Simulator/Actions/DelicateSynthesis.cs index 8b88dcb..688352f 100644 --- a/Simulator/Actions/DelicateSynthesis.cs +++ b/Simulator/Actions/DelicateSynthesis.cs @@ -10,5 +10,5 @@ internal sealed class DelicateSynthesis : BaseAction public override bool IncreasesQuality => true; public override int CPCost(Simulator s) => 32; - public override float Efficiency(Simulator s) => 1.00f; + public override int Efficiency(Simulator s) => 100; } diff --git a/Simulator/Actions/FocusedSynthesis.cs b/Simulator/Actions/FocusedSynthesis.cs index df256cf..407077e 100644 --- a/Simulator/Actions/FocusedSynthesis.cs +++ b/Simulator/Actions/FocusedSynthesis.cs @@ -9,6 +9,6 @@ internal sealed class FocusedSynthesis : BaseAction public override bool IncreasesProgress => true; public override int CPCost(Simulator s) => 5; - public override float Efficiency(Simulator s) => 2.00f; + public override int Efficiency(Simulator s) => 200; public override float SuccessRate(Simulator s) => s.ActionStates.Observed ? 1.00f : 0.50f; } diff --git a/Simulator/Actions/FocusedTouch.cs b/Simulator/Actions/FocusedTouch.cs index 928ef2d..1afde89 100644 --- a/Simulator/Actions/FocusedTouch.cs +++ b/Simulator/Actions/FocusedTouch.cs @@ -9,6 +9,6 @@ internal sealed class FocusedTouch : BaseAction public override bool IncreasesQuality => true; public override int CPCost(Simulator s) => 18; - public override float Efficiency(Simulator s) => 1.50f; + public override int Efficiency(Simulator s) => 150; public override float SuccessRate(Simulator s) => s.ActionStates.Observed ? 1.00f : 0.50f; } diff --git a/Simulator/Actions/Groundwork.cs b/Simulator/Actions/Groundwork.cs index c953602..118b3ec 100644 --- a/Simulator/Actions/Groundwork.cs +++ b/Simulator/Actions/Groundwork.cs @@ -10,10 +10,10 @@ internal sealed class Groundwork : BaseAction public override int DurabilityCost => 20; public override int CPCost(Simulator s) => 18; - public override float Efficiency(Simulator s) + public override int Efficiency(Simulator s) { // Groundwork Mastery Trait - var ret = s.Input.Stats.Level >= 86 ? 3.60f : 3.00f; + var ret = s.Input.Stats.Level >= 86 ? 360 : 300; return s.Durability < s.CalculateDurabilityCost(DurabilityCost) ? ret / 2 : ret; } } diff --git a/Simulator/Actions/HastyTouch.cs b/Simulator/Actions/HastyTouch.cs index e0005b5..cfa179d 100644 --- a/Simulator/Actions/HastyTouch.cs +++ b/Simulator/Actions/HastyTouch.cs @@ -9,6 +9,6 @@ internal sealed class HastyTouch : BaseAction public override bool IncreasesQuality => true; public override int CPCost(Simulator s) => 0; - public override float Efficiency(Simulator s) => 1.00f; + public override int Efficiency(Simulator s) => 100; public override float SuccessRate(Simulator s) => 0.60f; } diff --git a/Simulator/Actions/IntensiveSynthesis.cs b/Simulator/Actions/IntensiveSynthesis.cs index 707bac0..54db7b0 100644 --- a/Simulator/Actions/IntensiveSynthesis.cs +++ b/Simulator/Actions/IntensiveSynthesis.cs @@ -9,7 +9,7 @@ internal sealed class IntensiveSynthesis : BaseAction public override bool IncreasesProgress => true; public override int CPCost(Simulator s) => 6; - public override float Efficiency(Simulator s) => 4.00f; + public override int Efficiency(Simulator s) => 400; public override bool CanUse(Simulator s) => (s.Condition == Condition.Good || s.Condition == Condition.Excellent || s.HasEffect(EffectType.HeartAndSoul)) diff --git a/Simulator/Actions/MuscleMemory.cs b/Simulator/Actions/MuscleMemory.cs index c5efc16..f6eb09d 100644 --- a/Simulator/Actions/MuscleMemory.cs +++ b/Simulator/Actions/MuscleMemory.cs @@ -9,7 +9,7 @@ internal sealed class MuscleMemory : BaseAction public override bool IncreasesProgress => true; public override int CPCost(Simulator s) => 6; - public override float Efficiency(Simulator s) => 3.00f; + public override int Efficiency(Simulator s) => 300; public override bool CanUse(Simulator s) => s.IsFirstStep && base.CanUse(s); diff --git a/Simulator/Actions/PreciseTouch.cs b/Simulator/Actions/PreciseTouch.cs index 97489f5..1be9c2d 100644 --- a/Simulator/Actions/PreciseTouch.cs +++ b/Simulator/Actions/PreciseTouch.cs @@ -9,7 +9,7 @@ internal sealed class PreciseTouch : BaseAction public override bool IncreasesQuality => true; public override int CPCost(Simulator s) => 18; - public override float Efficiency(Simulator s) => 1.50f; + public override int Efficiency(Simulator s) => 150; public override bool CanUse(Simulator s) => (s.Condition == Condition.Good || s.Condition == Condition.Excellent || s.HasEffect(EffectType.HeartAndSoul)) diff --git a/Simulator/Actions/PreparatoryTouch.cs b/Simulator/Actions/PreparatoryTouch.cs index 0f83cd7..f5f0950 100644 --- a/Simulator/Actions/PreparatoryTouch.cs +++ b/Simulator/Actions/PreparatoryTouch.cs @@ -10,7 +10,7 @@ internal sealed class PreparatoryTouch : BaseAction public override int DurabilityCost => 20; public override int CPCost(Simulator s) => 40; - public override float Efficiency(Simulator s) => 2.00f; + public override int Efficiency(Simulator s) => 200; public override void UseSuccess(Simulator s) { diff --git a/Simulator/Actions/PrudentSynthesis.cs b/Simulator/Actions/PrudentSynthesis.cs index 91d4a6b..1ac3a22 100644 --- a/Simulator/Actions/PrudentSynthesis.cs +++ b/Simulator/Actions/PrudentSynthesis.cs @@ -10,7 +10,7 @@ internal sealed class PrudentSynthesis : BaseAction public override int DurabilityCost => base.DurabilityCost / 2; public override int CPCost(Simulator s) => 18; - public override float Efficiency(Simulator s) => 1.80f; + public override int Efficiency(Simulator s) => 180; public override bool CanUse(Simulator s) => !(s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2)) diff --git a/Simulator/Actions/PrudentTouch.cs b/Simulator/Actions/PrudentTouch.cs index a082d9e..f01f814 100644 --- a/Simulator/Actions/PrudentTouch.cs +++ b/Simulator/Actions/PrudentTouch.cs @@ -10,7 +10,7 @@ internal sealed class PrudentTouch : BaseAction public override int DurabilityCost => base.DurabilityCost / 2; public override int CPCost(Simulator s) => 25; - public override float Efficiency(Simulator s) => 1.00f; + public override int Efficiency(Simulator s) => 100; public override bool CanUse(Simulator s) => !(s.HasEffect(EffectType.WasteNot) || s.HasEffect(EffectType.WasteNot2)) diff --git a/Simulator/Actions/RapidSynthesis.cs b/Simulator/Actions/RapidSynthesis.cs index 7bfc8fc..d24056e 100644 --- a/Simulator/Actions/RapidSynthesis.cs +++ b/Simulator/Actions/RapidSynthesis.cs @@ -10,6 +10,6 @@ internal sealed class RapidSynthesis : BaseAction public override int CPCost(Simulator s) => 0; // Rapid Synthesis Mastery Trait - public override float Efficiency(Simulator s) => s.Input.Stats.Level >= 63 ? 5.00f : 2.50f; + public override int Efficiency(Simulator s) => s.Input.Stats.Level >= 63 ? 500 : 250; public override float SuccessRate(Simulator s) => 0.50f; } diff --git a/Simulator/Actions/Reflect.cs b/Simulator/Actions/Reflect.cs index fe3afac..9a7dbdc 100644 --- a/Simulator/Actions/Reflect.cs +++ b/Simulator/Actions/Reflect.cs @@ -9,7 +9,7 @@ internal sealed class Reflect : BaseAction public override bool IncreasesQuality => true; public override int CPCost(Simulator s) => 6; - public override float Efficiency(Simulator s) => 1.00f; + public override int Efficiency(Simulator s) => 100; public override bool CanUse(Simulator s) => s.IsFirstStep && base.CanUse(s); diff --git a/Simulator/Actions/StandardTouch.cs b/Simulator/Actions/StandardTouch.cs index b3e0e58..59b0d1e 100644 --- a/Simulator/Actions/StandardTouch.cs +++ b/Simulator/Actions/StandardTouch.cs @@ -9,5 +9,5 @@ internal sealed class StandardTouch : BaseAction public override bool IncreasesQuality => true; public override int CPCost(Simulator s) => s.ActionStates.TouchComboIdx == 1 ? 18 : 32; - public override float Efficiency(Simulator s) => 1.25f; + public override int Efficiency(Simulator s) => 125; } diff --git a/Simulator/Actions/TrainedFinesse.cs b/Simulator/Actions/TrainedFinesse.cs index d884a8c..28026c2 100644 --- a/Simulator/Actions/TrainedFinesse.cs +++ b/Simulator/Actions/TrainedFinesse.cs @@ -10,7 +10,7 @@ internal sealed class TrainedFinesse : BaseAction public override int DurabilityCost => 0; public override int CPCost(Simulator s) => 32; - public override float Efficiency(Simulator s) => 1.00f; + public override int Efficiency(Simulator s) => 100; public override bool CanUse(Simulator s) => s.GetEffectStrength(EffectType.InnerQuiet) == 10 diff --git a/Simulator/Simulator.cs b/Simulator/Simulator.cs index d108e4c..492b4db 100644 --- a/Simulator/Simulator.cs +++ b/Simulator/Simulator.cs @@ -213,51 +213,51 @@ public int CalculateCPCost(int amount) return (int)Math.Ceiling(amt); } - public int CalculateProgressGain(float efficiency, bool dryRun = true) + public int CalculateProgressGain(int efficiency, bool dryRun = true) { - var buffModifier = 1.00f; + var buffModifier = 100; if (HasEffect(EffectType.MuscleMemory)) { - buffModifier += 1.00f; + buffModifier += 100; if (!dryRun) RemoveEffect(EffectType.MuscleMemory); } if (HasEffect(EffectType.Veneration)) - buffModifier += 0.50f; + buffModifier += 50; var conditionModifier = Condition switch { - Condition.Malleable => 1.50f, - _ => 1.00f + Condition.Malleable => 150, + _ => 100 }; - var progressGain = (int)(Input.BaseProgressGain * efficiency * conditionModifier * buffModifier); + var progressGain = (int)((long)Input.BaseProgressGain * efficiency * conditionModifier * buffModifier / 1e6); return progressGain; } - public int CalculateQualityGain(float efficiency, bool dryRun = true) + public int CalculateQualityGain(int efficiency, bool dryRun = true) { - var buffModifier = 1.00f; + var buffModifier = 100; if (HasEffect(EffectType.GreatStrides)) { - buffModifier += 1.00f; + buffModifier += 100; if (!dryRun) RemoveEffect(EffectType.GreatStrides); } if (HasEffect(EffectType.Innovation)) - buffModifier += 0.50f; + buffModifier += 50; - buffModifier *= 1 + (GetEffectStrength(EffectType.InnerQuiet) * 0.10f); + var iqModifier = 100 + (GetEffectStrength(EffectType.InnerQuiet) * 10); var conditionModifier = Condition switch { - Condition.Poor => 0.50f, - Condition.Good => Input.Stats.HasSplendorousBuff ? 1.75f : 1.50f, - Condition.Excellent => 4.00f, - _ => 1.00f, + Condition.Poor => 50, + Condition.Good => Input.Stats.HasSplendorousBuff ? 175 : 150, + Condition.Excellent => 400, + _ => 100, }; - var qualityGain = (int)(Input.BaseQualityGain * efficiency * conditionModifier * buffModifier); + var qualityGain = (int)((long)Input.BaseQualityGain * efficiency * conditionModifier * iqModifier * buffModifier / 1e8); return qualityGain; } @@ -297,10 +297,10 @@ public void ReduceCP(int amount) => ReduceCPRaw(CalculateCPCost(amount)); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void IncreaseProgress(float efficiency) => + public void IncreaseProgress(int efficiency) => IncreaseProgressRaw(CalculateProgressGain(efficiency, false)); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void IncreaseQuality(float efficiency) => + public void IncreaseQuality(int efficiency) => IncreaseQualityRaw(CalculateQualityGain(efficiency, false)); } diff --git a/Solver/ActionSet.cs b/Solver/ActionSet.cs index 43b5874..b4af25f 100644 --- a/Solver/ActionSet.cs +++ b/Solver/ActionSet.cs @@ -49,6 +49,8 @@ public struct ActionSet static ActionSet() { AcceptedActionsLUT = new int[Enum.GetValues().Length]; + for (var i = 0; i < AcceptedActionsLUT.Length; i++) + AcceptedActionsLUT[i] = -1; for (var i = 0; i < AcceptedActions.Length; i++) AcceptedActionsLUT[(byte)AcceptedActions[i]] = i; } @@ -57,7 +59,7 @@ static ActionSet() [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int FromAction(ActionType action) { - var ret = Simulator.AcceptedActionsLUT[(byte)action]; + var ret = AcceptedActionsLUT[(byte)action]; if (ret == -1) throw new ArgumentOutOfRangeException(nameof(action), action, $"Action {action} is unsupported in {nameof(ActionSet)}."); return ret; From 4f953f2fa3e690af1be7dcad895f79b6202a412f Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sat, 4 Nov 2023 13:49:47 -0700 Subject: [PATCH 49/58] Add benchmarking and trace build config --- .gitignore | 3 +- Benchmark/Bench.cs | 97 ++++++++++++++++++++++++ Benchmark/Craftimizer.Benchmark.csproj | 2 + Benchmark/Program.cs | 10 ++- Craftimizer.Test/Craftimizer.Test.csproj | 6 ++ Craftimizer.Test/Simulator/Simulator.cs | 2 - Craftimizer.Test/Solver/ActionSet.cs | 4 + Craftimizer.sln | 65 +++++++--------- Craftimizer/Craftimizer.csproj | 1 + Simulator/Craftimizer.Simulator.csproj | 7 ++ Solver/ActionSet.cs | 2 - Solver/Craftimizer.Solver.csproj | 7 ++ 12 files changed, 158 insertions(+), 48 deletions(-) create mode 100644 Benchmark/Bench.cs diff --git a/.gitignore b/.gitignore index 7990fe7..7defab2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vs/ obj/ bin/ -*.user \ No newline at end of file +*.user +BenchmarkDotNet.Artifacts/ \ No newline at end of file diff --git a/Benchmark/Bench.cs b/Benchmark/Bench.cs new file mode 100644 index 0000000..5e2e764 --- /dev/null +++ b/Benchmark/Bench.cs @@ -0,0 +1,97 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Engines; +using Craftimizer.Simulator; +using Craftimizer.Solver; + +namespace Craftimizer.Benchmark; + +[SimpleJob(RunStrategy.Monitoring)] +[MinColumn, Q1Column, Q3Column, MaxColumn] +public class Bench +{ + private static SimulationInput[] Inputs { get; } = new SimulationInput[] { + // https://craftingway.app/rotation/loud-namazu-jVe9Y + // Chondrite Saw + new(new() + { + Craftsmanship = 3304, + Control = 3374, + CP = 575, + Level = 90, + CanUseManipulation = true, + HasSplendorousBuff = false, + IsSpecialist = false, + CLvl = 560, + }, + new() + { + IsExpert = false, + ClassJobLevel = 90, + RLvl = 560, + ConditionsFlag = 0b1111, + MaxDurability = 80, + MaxQuality = 7200, + MaxProgress = 3500, + QualityModifier = 80, + QualityDivider = 115, + ProgressModifier = 90, + ProgressDivider = 130 + }), + + // https://craftingway.app/rotation/sandy-fafnir-doVCs + // Classical Longsword + new(new() + { + Craftsmanship = 3290, + Control = 3541, + CP = 649, + Level = 90, + CanUseManipulation = true, + HasSplendorousBuff = false, + IsSpecialist = false, + CLvl = 560, + }, + new() + { + IsExpert = false, + ClassJobLevel = 90, + RLvl = 580, + ConditionsFlag = 0b1111, + MaxDurability = 70, + MaxQuality = 10920, + MaxProgress = 3900, + QualityModifier = 70, + QualityDivider = 115, + ProgressModifier = 80, + ProgressDivider = 130 + }) + }; + + public static IEnumerable States => Inputs.Select(i => new SimulationState(i)); + + public static IEnumerable Configs => new SolverConfig[] + { + new() + { + Iterations = 100_000, + ForkCount = 32, + FurcatedActionCount = 16, + MaxStepCount = 30, + } + }; + + [ParamsSource(nameof(States))] + public SimulationState State { get; set; } + + [ParamsSource(nameof(Configs))] + public SolverConfig Config { get; set; } + + [Benchmark] + public async Task Solve() + { + var solver = new Solver.Solver(Config, State); + solver.Start(); + var (_, s) = await solver.GetTask().ConfigureAwait(false); + return (float)s.Quality / s.Input.Recipe.MaxQuality; + } +} diff --git a/Benchmark/Craftimizer.Benchmark.csproj b/Benchmark/Craftimizer.Benchmark.csproj index 9d68105..e255672 100644 --- a/Benchmark/Craftimizer.Benchmark.csproj +++ b/Benchmark/Craftimizer.Benchmark.csproj @@ -5,6 +5,8 @@ Exe enable enable + x64 + Debug;Release;Trace diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index d3717ca..397686c 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -1,3 +1,4 @@ +using BenchmarkDotNet.Running; using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; using Craftimizer.Solver; @@ -7,10 +8,11 @@ namespace Craftimizer.Benchmark; internal static class Program { - private static async Task Main() + private static async Task Main(string[] args) { - //var summary = BenchmarkRunner.Run(); - //return; + Environment.SetEnvironmentVariable("IS_BENCH", "1"); + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + return; //TypeLayout.PrintLayout>(true); //return; @@ -78,7 +80,7 @@ private static async Task Main() Console.WriteLine($"{state.Quality} {state.CP} {state.Progress} {state.Durability}"); //return; var solver = new Solver.Solver(config, state); - solver.OnLog += s => Console.WriteLine(s); + solver.OnLog += Console.WriteLine; solver.OnNewAction += s => Console.WriteLine(s); solver.Start(); var (_, s) = await solver.GetTask().ConfigureAwait(false); diff --git a/Craftimizer.Test/Craftimizer.Test.csproj b/Craftimizer.Test/Craftimizer.Test.csproj index 5344fb4..f2d7fde 100644 --- a/Craftimizer.Test/Craftimizer.Test.csproj +++ b/Craftimizer.Test/Craftimizer.Test.csproj @@ -7,6 +7,8 @@ false true + x64 + Debug;Release @@ -21,4 +23,8 @@ + + $(DefineConstants);IS_DETERMINISTIC + + diff --git a/Craftimizer.Test/Simulator/Simulator.cs b/Craftimizer.Test/Simulator/Simulator.cs index f628654..20756fc 100644 --- a/Craftimizer.Test/Simulator/Simulator.cs +++ b/Craftimizer.Test/Simulator/Simulator.cs @@ -1,6 +1,4 @@ using Craftimizer.Simulator.Actions; -using System; -using System.Reflection; namespace Craftimizer.Test.Simulator; diff --git a/Craftimizer.Test/Solver/ActionSet.cs b/Craftimizer.Test/Solver/ActionSet.cs index 30e4bef..b6fb34f 100644 --- a/Craftimizer.Test/Solver/ActionSet.cs +++ b/Craftimizer.Test/Solver/ActionSet.cs @@ -105,6 +105,10 @@ public void TestElementAt() [TestMethod] public void TestRandomIndex() { +#if IS_DETERMINISTIC + Assert.Inconclusive("Craftimizer is built for benchmarking; all random actions are deterministic and not actually random."); +#endif + var actions = new[] { ActionType.BasicTouch, diff --git a/Craftimizer.sln b/Craftimizer.sln index 9b6d4a8..b8a6abd 100644 --- a/Craftimizer.sln +++ b/Craftimizer.sln @@ -17,56 +17,43 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Solver", "Solve {172EE849-AC7E-4F2A-ACAB-EF9D065523B3} = {172EE849-AC7E-4F2A-ACAB-EF9D065523B3} EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Craftimizer.Test", "Craftimizer.Test\Craftimizer.Test.csproj", "{C3AEA981-9DA8-405C-995B-86528493891B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Test", "Craftimizer.Test\Craftimizer.Test.csproj", "{C3AEA981-9DA8-405C-995B-86528493891B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 - Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 + Trace|x64 = Trace|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.ActiveCfg = Debug|x64 - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|Any CPU.Build.0 = Debug|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.ActiveCfg = Release|x64 - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|Any CPU.Build.0 = Release|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 - {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.ActiveCfg = Debug|Any CPU - {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.Build.0 = Debug|Any CPU - {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|Any CPU.Build.0 = Release|Any CPU - {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.ActiveCfg = Release|Any CPU - {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.Build.0 = Release|Any CPU - {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.ActiveCfg = Debug|Any CPU - {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.Build.0 = Debug|Any CPU - {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|Any CPU.Build.0 = Release|Any CPU - {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.ActiveCfg = Release|Any CPU - {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.Build.0 = Release|Any CPU - {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.ActiveCfg = Debug|Any CPU - {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.Build.0 = Debug|Any CPU - {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|Any CPU.Build.0 = Release|Any CPU - {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.ActiveCfg = Release|Any CPU - {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.Build.0 = Release|Any CPU - {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.ActiveCfg = Debug|Any CPU - {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.Build.0 = Debug|Any CPU - {C3AEA981-9DA8-405C-995B-86528493891B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C3AEA981-9DA8-405C-995B-86528493891B}.Release|Any CPU.Build.0 = Release|Any CPU - {C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.ActiveCfg = Release|Any CPU - {C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.Build.0 = Release|Any CPU + {13C812E9-0D42-4B95-8646-40EEBF30636F}.Trace|x64.ActiveCfg = Release|x64 + {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.ActiveCfg = Debug|x64 + {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.Build.0 = Debug|x64 + {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.ActiveCfg = Release|x64 + {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.Build.0 = Release|x64 + {057C4B64-4D99-4847-9BCF-966571CAE57C}.Trace|x64.ActiveCfg = Trace|x64 + {057C4B64-4D99-4847-9BCF-966571CAE57C}.Trace|x64.Build.0 = Trace|x64 + {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.ActiveCfg = Debug|x64 + {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.Build.0 = Debug|x64 + {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.ActiveCfg = Release|x64 + {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.Build.0 = Release|x64 + {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Trace|x64.ActiveCfg = Trace|x64 + {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Trace|x64.Build.0 = Trace|x64 + {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.ActiveCfg = Debug|x64 + {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.Build.0 = Debug|x64 + {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.ActiveCfg = Release|x64 + {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.Build.0 = Release|x64 + {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Trace|x64.ActiveCfg = Trace|x64 + {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Trace|x64.Build.0 = Trace|x64 + {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.ActiveCfg = Debug|x64 + {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.Build.0 = Debug|x64 + {C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.ActiveCfg = Release|x64 + {C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.Build.0 = Release|x64 + {C3AEA981-9DA8-405C-995B-86528493891B}.Trace|x64.ActiveCfg = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Craftimizer/Craftimizer.csproj b/Craftimizer/Craftimizer.csproj index 9fd555d..3368159 100644 --- a/Craftimizer/Craftimizer.csproj +++ b/Craftimizer/Craftimizer.csproj @@ -4,6 +4,7 @@ Asriel Camora 1.9.1.0 https://github.com/WorkingRobot/craftimizer.git + Debug;Release diff --git a/Simulator/Craftimizer.Simulator.csproj b/Simulator/Craftimizer.Simulator.csproj index ec99977..da4e1c5 100644 --- a/Simulator/Craftimizer.Simulator.csproj +++ b/Simulator/Craftimizer.Simulator.csproj @@ -4,6 +4,8 @@ net7.0 enable enable + x64 + Debug;Release;Trace @@ -13,4 +15,9 @@ + + $(DefineConstants);IS_DETERMINISTIC + $(DefineConstants);IS_DETERMINISTIC;IS_TRACE + + diff --git a/Solver/ActionSet.cs b/Solver/ActionSet.cs index b4af25f..98c0b9f 100644 --- a/Solver/ActionSet.cs +++ b/Solver/ActionSet.cs @@ -5,8 +5,6 @@ namespace Craftimizer.Solver; -// #define IS_DETERMINISTIC - public struct ActionSet { private uint bits; diff --git a/Solver/Craftimizer.Solver.csproj b/Solver/Craftimizer.Solver.csproj index 8ff7314..0779688 100644 --- a/Solver/Craftimizer.Solver.csproj +++ b/Solver/Craftimizer.Solver.csproj @@ -5,6 +5,8 @@ enable enable True + x64 + Debug;Release;Trace @@ -18,4 +20,9 @@ + + $(DefineConstants);IS_DETERMINISTIC + $(DefineConstants);IS_DETERMINISTIC;IS_TRACE + + From 269a7f9870377087ccdd3a8190b8ee9c4be3a93f Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sat, 4 Nov 2023 18:44:30 -0700 Subject: [PATCH 50/58] Optimize trace build, add trace sim code --- Benchmark/Craftimizer.Benchmark.csproj | 16 +++ Benchmark/Program.cs | 131 +++++++++++++++-------- Craftimizer.Test/Craftimizer.Test.csproj | 6 +- Simulator/Craftimizer.Simulator.csproj | 10 +- Solver/Craftimizer.Solver.csproj | 10 +- 5 files changed, 118 insertions(+), 55 deletions(-) diff --git a/Benchmark/Craftimizer.Benchmark.csproj b/Benchmark/Craftimizer.Benchmark.csproj index e255672..dde07aa 100644 --- a/Benchmark/Craftimizer.Benchmark.csproj +++ b/Benchmark/Craftimizer.Benchmark.csproj @@ -9,6 +9,12 @@ Debug;Release;Trace + + + + + + @@ -22,4 +28,14 @@ + + + $(DefineConstants);IS_DETERMINISTIC + + + + True + $(DefineConstants);IS_DETERMINISTIC;IS_TRACE + + diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index 397686c..280e367 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -1,4 +1,3 @@ -using BenchmarkDotNet.Running; using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; using Craftimizer.Solver; @@ -10,19 +9,16 @@ internal static class Program { private static async Task Main(string[] args) { +#if !IS_TRACE Environment.SetEnvironmentVariable("IS_BENCH", "1"); - BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); - return; - - //TypeLayout.PrintLayout>(true); - //return; - + BenchmarkDotNet.Running.BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); +#else var input = new SimulationInput( - new CharacterStats + new() { - Craftsmanship = 4078, - Control = 3897, - CP = 704, + Craftsmanship = 4041, + Control = 3905, + CP = 609, Level = 90, CanUseManipulation = true, HasSplendorousBuff = false, @@ -44,47 +40,90 @@ private static async Task Main(string[] args) ProgressDivider = 130, } ); - var config = new SolverConfig() { - Iterations = 100_000, - ForkCount = 32, - FurcatedActionCount = 16, - MaxStepCount = 30, + Algorithm = SolverAlgorithm.Stepwise, + Iterations = 30000 }; - - var sim = new SimulatorNoRandom(new(input)); - (_, var state) = sim.Execute(new(input), ActionType.MuscleMemory); - (_, state) = sim.Execute(state, ActionType.PrudentTouch); - //(_, state) = sim.Execute(state, ActionType.Manipulation); - //(_, state) = sim.Execute(state, ActionType.Veneration); - //(_, state) = sim.Execute(state, ActionType.WasteNot); - //(_, state) = sim.Execute(state, ActionType.Groundwork); - //(_, state) = sim.Execute(state, ActionType.Groundwork); - //(_, state) = sim.Execute(state, ActionType.Groundwork); - //(_, state) = sim.Execute(state, ActionType.Innovation); - //(_, state) = sim.Execute(state, ActionType.PrudentTouch); - //(_, state) = sim.Execute(state, ActionType.AdvancedTouchCombo); - //(_, state) = sim.Execute(state, ActionType.Manipulation); - //(_, state) = sim.Execute(state, ActionType.Innovation); - //(_, state) = sim.Execute(state, ActionType.PrudentTouch); - //(_, state) = sim.Execute(state, ActionType.AdvancedTouchCombo); - //(_, state) = sim.Execute(state, ActionType.GreatStrides); - //(_, state) = sim.Execute(state, ActionType.Innovation); - //(_, state) = sim.Execute(state, ActionType.FocusedTouchCombo); - //(_, state) = sim.Execute(state, ActionType.GreatStrides); - //(_, state) = sim.Execute(state, ActionType.ByregotsBlessing); - //(_, state) = sim.Execute(state, ActionType.CarefulSynthesis); - //(_, state) = sim.Execute(state, ActionType.CarefulSynthesis); - - Console.WriteLine($"{state.Quality} {state.CP} {state.Progress} {state.Durability}"); - //return; - var solver = new Solver.Solver(config, state); - solver.OnLog += Console.WriteLine; - solver.OnNewAction += s => Console.WriteLine(s); + var solver = new Solver.Solver(config, new(input)); + solver.OnNewAction += s => Console.WriteLine($">{s}"); solver.Start(); var (_, s) = await solver.GetTask().ConfigureAwait(false); Console.WriteLine($"Qual: {s.Quality}/{s.Input.Recipe.MaxQuality}"); +#endif + return; + + ////TypeLayout.PrintLayout>(true); + ////return; + + //var input = new SimulationInput( + // new CharacterStats + // { + // Craftsmanship = 4078, + // Control = 3897, + // CP = 704, + // Level = 90, + // CanUseManipulation = true, + // HasSplendorousBuff = false, + // IsSpecialist = false, + // CLvl = 560, + // }, + // new RecipeInfo() + // { + // IsExpert = false, + // ClassJobLevel = 90, + // RLvl = 640, + // ConditionsFlag = 15, + // MaxDurability = 70, + // MaxQuality = 14040, + // MaxProgress = 6600, + // QualityModifier = 70, + // QualityDivider = 115, + // ProgressModifier = 80, + // ProgressDivider = 130, + // } + //); + + //var config = new SolverConfig() + //{ + // Iterations = 100_000, + // ForkCount = 32, + // FurcatedActionCount = 16, + // MaxStepCount = 30, + //}; + + //var sim = new SimulatorNoRandom(new(input)); + //(_, var state) = sim.Execute(new(input), ActionType.MuscleMemory); + //(_, state) = sim.Execute(state, ActionType.PrudentTouch); + ////(_, state) = sim.Execute(state, ActionType.Manipulation); + ////(_, state) = sim.Execute(state, ActionType.Veneration); + ////(_, state) = sim.Execute(state, ActionType.WasteNot); + ////(_, state) = sim.Execute(state, ActionType.Groundwork); + ////(_, state) = sim.Execute(state, ActionType.Groundwork); + ////(_, state) = sim.Execute(state, ActionType.Groundwork); + ////(_, state) = sim.Execute(state, ActionType.Innovation); + ////(_, state) = sim.Execute(state, ActionType.PrudentTouch); + ////(_, state) = sim.Execute(state, ActionType.AdvancedTouchCombo); + ////(_, state) = sim.Execute(state, ActionType.Manipulation); + ////(_, state) = sim.Execute(state, ActionType.Innovation); + ////(_, state) = sim.Execute(state, ActionType.PrudentTouch); + ////(_, state) = sim.Execute(state, ActionType.AdvancedTouchCombo); + ////(_, state) = sim.Execute(state, ActionType.GreatStrides); + ////(_, state) = sim.Execute(state, ActionType.Innovation); + ////(_, state) = sim.Execute(state, ActionType.FocusedTouchCombo); + ////(_, state) = sim.Execute(state, ActionType.GreatStrides); + ////(_, state) = sim.Execute(state, ActionType.ByregotsBlessing); + ////(_, state) = sim.Execute(state, ActionType.CarefulSynthesis); + ////(_, state) = sim.Execute(state, ActionType.CarefulSynthesis); + + //Console.WriteLine($"{state.Quality} {state.CP} {state.Progress} {state.Durability}"); + ////return; + //var solver = new Solver.Solver(config, state); + //solver.OnLog += Console.WriteLine; + //solver.OnNewAction += s => Console.WriteLine(s); + //solver.Start(); + //var (_, s) = await solver.GetTask().ConfigureAwait(false); + //Console.WriteLine($"Qual: {s.Quality}/{s.Input.Recipe.MaxQuality}"); } private static void Benchmark(Func search) diff --git a/Craftimizer.Test/Craftimizer.Test.csproj b/Craftimizer.Test/Craftimizer.Test.csproj index f2d7fde..520abba 100644 --- a/Craftimizer.Test/Craftimizer.Test.csproj +++ b/Craftimizer.Test/Craftimizer.Test.csproj @@ -22,9 +22,9 @@ - - - $(DefineConstants);IS_DETERMINISTIC + + + $(DefineConstants);IS_DETERMINISTIC diff --git a/Simulator/Craftimizer.Simulator.csproj b/Simulator/Craftimizer.Simulator.csproj index da4e1c5..908f912 100644 --- a/Simulator/Craftimizer.Simulator.csproj +++ b/Simulator/Craftimizer.Simulator.csproj @@ -15,9 +15,13 @@ - - $(DefineConstants);IS_DETERMINISTIC - $(DefineConstants);IS_DETERMINISTIC;IS_TRACE + + $(DefineConstants);IS_DETERMINISTIC + + + + True + $(DefineConstants);IS_DETERMINISTIC;IS_TRACE diff --git a/Solver/Craftimizer.Solver.csproj b/Solver/Craftimizer.Solver.csproj index 0779688..9be317b 100644 --- a/Solver/Craftimizer.Solver.csproj +++ b/Solver/Craftimizer.Solver.csproj @@ -20,9 +20,13 @@ - - $(DefineConstants);IS_DETERMINISTIC - $(DefineConstants);IS_DETERMINISTIC;IS_TRACE + + $(DefineConstants);IS_DETERMINISTIC + + + + True + $(DefineConstants);IS_DETERMINISTIC;IS_TRACE From 5bee39678c052583ac766f3dc4f44b9c6b2b9276 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sat, 4 Nov 2023 18:44:54 -0700 Subject: [PATCH 51/58] Fixed stepwise solver never finishing --- Solver/Solver.cs | 2 +- Solver/Trace.cs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 Solver/Trace.cs diff --git a/Solver/Solver.cs b/Solver/Solver.cs index 0cb8cec..ca6343e 100644 --- a/Solver/Solver.cs +++ b/Solver/Solver.cs @@ -311,7 +311,7 @@ private Task SearchStepwise() if (sim.IsComplete) break; - var solver = new MCTS(MCTSConfig, State); + var solver = new MCTS(MCTSConfig, state); var s = Stopwatch.StartNew(); solver.Search(Config.Iterations, Token); diff --git a/Solver/Trace.cs b/Solver/Trace.cs new file mode 100644 index 0000000..36cdc02 --- /dev/null +++ b/Solver/Trace.cs @@ -0,0 +1,12 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Craftimizer.Solver; + +internal static class Trace +{ + [Conditional("IS_TRACE")] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + public static void Log(string msg) => + Console.WriteLine(msg); +} From bfc8708fbd5cab4597d08b30e63c270ab7118e10 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sat, 4 Nov 2023 19:40:31 -0700 Subject: [PATCH 52/58] Update solver scoring; remove byregot adjustment --- Benchmark/Program.cs | 209 +++++++++-------------- Craftimizer.Test/Craftimizer.Test.csproj | 9 +- Craftimizer.Test/Solver/ActionSet.cs | 28 ++- Craftimizer.sln | 3 +- Solver/ActionSet.cs | 38 ++++- Solver/SimulationNode.cs | 7 +- 6 files changed, 155 insertions(+), 139 deletions(-) diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index 280e367..1fea691 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -1,18 +1,30 @@ using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; using Craftimizer.Solver; -using System.Diagnostics; namespace Craftimizer.Benchmark; internal static class Program { - private static async Task Main(string[] args) + private static Task Main(string[] args) { #if !IS_TRACE + RunBench(args); + return Task.CompletedTask; +#else + return RunTrace(); +#endif + // return RunOther(); + } + + private static void RunBench(string[] args) + { Environment.SetEnvironmentVariable("IS_BENCH", "1"); BenchmarkDotNet.Running.BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); -#else + } + + private static async Task RunTrace() + { var input = new SimulationInput( new() { @@ -43,143 +55,88 @@ private static async Task Main(string[] args) var config = new SolverConfig() { Algorithm = SolverAlgorithm.Stepwise, - Iterations = 30000 + Iterations = 30000, + MaxStepCount = 25 }; var solver = new Solver.Solver(config, new(input)); solver.OnNewAction += s => Console.WriteLine($">{s}"); solver.Start(); var (_, s) = await solver.GetTask().ConfigureAwait(false); Console.WriteLine($"Qual: {s.Quality}/{s.Input.Recipe.MaxQuality}"); -#endif - return; - - ////TypeLayout.PrintLayout>(true); - ////return; - - //var input = new SimulationInput( - // new CharacterStats - // { - // Craftsmanship = 4078, - // Control = 3897, - // CP = 704, - // Level = 90, - // CanUseManipulation = true, - // HasSplendorousBuff = false, - // IsSpecialist = false, - // CLvl = 560, - // }, - // new RecipeInfo() - // { - // IsExpert = false, - // ClassJobLevel = 90, - // RLvl = 640, - // ConditionsFlag = 15, - // MaxDurability = 70, - // MaxQuality = 14040, - // MaxProgress = 6600, - // QualityModifier = 70, - // QualityDivider = 115, - // ProgressModifier = 80, - // ProgressDivider = 130, - // } - //); - - //var config = new SolverConfig() - //{ - // Iterations = 100_000, - // ForkCount = 32, - // FurcatedActionCount = 16, - // MaxStepCount = 30, - //}; - - //var sim = new SimulatorNoRandom(new(input)); - //(_, var state) = sim.Execute(new(input), ActionType.MuscleMemory); - //(_, state) = sim.Execute(state, ActionType.PrudentTouch); - ////(_, state) = sim.Execute(state, ActionType.Manipulation); - ////(_, state) = sim.Execute(state, ActionType.Veneration); - ////(_, state) = sim.Execute(state, ActionType.WasteNot); - ////(_, state) = sim.Execute(state, ActionType.Groundwork); - ////(_, state) = sim.Execute(state, ActionType.Groundwork); - ////(_, state) = sim.Execute(state, ActionType.Groundwork); - ////(_, state) = sim.Execute(state, ActionType.Innovation); - ////(_, state) = sim.Execute(state, ActionType.PrudentTouch); - ////(_, state) = sim.Execute(state, ActionType.AdvancedTouchCombo); - ////(_, state) = sim.Execute(state, ActionType.Manipulation); - ////(_, state) = sim.Execute(state, ActionType.Innovation); - ////(_, state) = sim.Execute(state, ActionType.PrudentTouch); - ////(_, state) = sim.Execute(state, ActionType.AdvancedTouchCombo); - ////(_, state) = sim.Execute(state, ActionType.GreatStrides); - ////(_, state) = sim.Execute(state, ActionType.Innovation); - ////(_, state) = sim.Execute(state, ActionType.FocusedTouchCombo); - ////(_, state) = sim.Execute(state, ActionType.GreatStrides); - ////(_, state) = sim.Execute(state, ActionType.ByregotsBlessing); - ////(_, state) = sim.Execute(state, ActionType.CarefulSynthesis); - ////(_, state) = sim.Execute(state, ActionType.CarefulSynthesis); - - //Console.WriteLine($"{state.Quality} {state.CP} {state.Progress} {state.Durability}"); - ////return; - //var solver = new Solver.Solver(config, state); - //solver.OnLog += Console.WriteLine; - //solver.OnNewAction += s => Console.WriteLine(s); - //solver.Start(); - //var (_, s) = await solver.GetTask().ConfigureAwait(false); - //Console.WriteLine($"Qual: {s.Quality}/{s.Input.Recipe.MaxQuality}"); } - private static void Benchmark(Func search) + private static async Task RunOther() { - var s = Stopwatch.StartNew(); - List q = new(); - for (var i = 0; i < 15; ++i) - { - var state = search().State; - //Console.WriteLine($"Qual: {state.Quality}/{state.Input.Recipe.MaxQuality}"); - - q.Add(state.Quality); - } + //TypeLayout.PrintLayout>(true); + //return; - s.Stop(); - Console.WriteLine($"{s.Elapsed.TotalMilliseconds / 60:0.00}ms/cycle"); - Console.WriteLine(string.Join(',', q)); - q.Sort(); - Console.WriteLine($"Min: {Quartile(q, 0)}, Max: {Quartile(q, 4)}, Avg: {Quartile(q, 2)}, Q1: {Quartile(q, 1)}, Q3: {Quartile(q, 3)}"); - } + var input = new SimulationInput( + new CharacterStats + { + Craftsmanship = 4078, + Control = 3897, + CP = 704, + Level = 90, + CanUseManipulation = true, + HasSplendorousBuff = false, + IsSpecialist = false, + CLvl = 560, + }, + new RecipeInfo() + { + IsExpert = false, + ClassJobLevel = 90, + RLvl = 640, + ConditionsFlag = 15, + MaxDurability = 70, + MaxQuality = 14040, + MaxProgress = 6600, + QualityModifier = 70, + QualityDivider = 115, + ProgressModifier = 80, + ProgressDivider = 130, + } + ); - // https://stackoverflow.com/a/31536435 - private static float Quartile(List input, int quartile) - { - float dblPercentage = quartile switch + var config = new SolverConfig() { - 0 => 0, // Smallest value in the data set - 1 => 25, // First quartile (25th percentile) - 2 => 50, // Second quartile (50th percentile) - 3 => 75, // Third quartile (75th percentile) - 4 => 100, // Largest value in the data set - _ => 0, + Iterations = 100_000, + ForkCount = 32, + FurcatedActionCount = 16, + MaxStepCount = 30, }; - if (dblPercentage >= 100) return input[^1]; - var position = (input.Count + 1) * dblPercentage / 100f; - var n = (dblPercentage / 100f * (input.Count - 1)) + 1; - - float leftNumber, rightNumber; - if (position >= 1) - { - leftNumber = input[(int)MathF.Floor(n) - 1]; - rightNumber = input[(int)MathF.Floor(n)]; - } - else - { - leftNumber = input[0]; // first data - rightNumber = input[1]; // first data - } + var sim = new SimulatorNoRandom(new(input)); + (_, var state) = sim.Execute(new(input), ActionType.MuscleMemory); + (_, state) = sim.Execute(state, ActionType.PrudentTouch); + //(_, state) = sim.Execute(state, ActionType.Manipulation); + //(_, state) = sim.Execute(state, ActionType.Veneration); + //(_, state) = sim.Execute(state, ActionType.WasteNot); + //(_, state) = sim.Execute(state, ActionType.Groundwork); + //(_, state) = sim.Execute(state, ActionType.Groundwork); + //(_, state) = sim.Execute(state, ActionType.Groundwork); + //(_, state) = sim.Execute(state, ActionType.Innovation); + //(_, state) = sim.Execute(state, ActionType.PrudentTouch); + //(_, state) = sim.Execute(state, ActionType.AdvancedTouchCombo); + //(_, state) = sim.Execute(state, ActionType.Manipulation); + //(_, state) = sim.Execute(state, ActionType.Innovation); + //(_, state) = sim.Execute(state, ActionType.PrudentTouch); + //(_, state) = sim.Execute(state, ActionType.AdvancedTouchCombo); + //(_, state) = sim.Execute(state, ActionType.GreatStrides); + //(_, state) = sim.Execute(state, ActionType.Innovation); + //(_, state) = sim.Execute(state, ActionType.FocusedTouchCombo); + //(_, state) = sim.Execute(state, ActionType.GreatStrides); + //(_, state) = sim.Execute(state, ActionType.ByregotsBlessing); + //(_, state) = sim.Execute(state, ActionType.CarefulSynthesis); + //(_, state) = sim.Execute(state, ActionType.CarefulSynthesis); - if (leftNumber == rightNumber) - return leftNumber; - else - { - var part = n - MathF.Floor(n); - return leftNumber + (part * (rightNumber - leftNumber)); - } + Console.WriteLine($"{state.Quality} {state.CP} {state.Progress} {state.Durability}"); + //return; + var solver = new Solver.Solver(config, state); + solver.OnLog += Console.WriteLine; + solver.OnNewAction += s => Console.WriteLine(s); + solver.Start(); + var (_, s) = await solver.GetTask().ConfigureAwait(false); + Console.WriteLine($"Qual: {s.Quality}/{s.Input.Recipe.MaxQuality}"); } } diff --git a/Craftimizer.Test/Craftimizer.Test.csproj b/Craftimizer.Test/Craftimizer.Test.csproj index 520abba..5c59fdb 100644 --- a/Craftimizer.Test/Craftimizer.Test.csproj +++ b/Craftimizer.Test/Craftimizer.Test.csproj @@ -8,7 +8,7 @@ false true x64 - Debug;Release + Debug;Release;Trace @@ -22,9 +22,14 @@ - + $(DefineConstants);IS_DETERMINISTIC + + True + $(DefineConstants);IS_DETERMINISTIC;IS_TRACE + + diff --git a/Craftimizer.Test/Solver/ActionSet.cs b/Craftimizer.Test/Solver/ActionSet.cs index b6fb34f..d34e387 100644 --- a/Craftimizer.Test/Solver/ActionSet.cs +++ b/Craftimizer.Test/Solver/ActionSet.cs @@ -89,33 +89,47 @@ public void TestElementAt() Assert.AreEqual(4, set.Count); +#if !IS_TRACE Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(0)); Assert.AreEqual(ActionType.FocusedTouch, set.ElementAt(1)); Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(2)); Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(3)); +#else + Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(0)); + Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(1)); + Assert.AreEqual(ActionType.FocusedTouch, set.ElementAt(2)); + Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(3)); +#endif set.RemoveAction(ActionType.FocusedTouch); Assert.AreEqual(3, set.Count); + +#if !IS_TRACE Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(0)); Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(1)); Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(2)); +#else + Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(0)); + Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(1)); + Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(2)); +#endif } [TestMethod] public void TestRandomIndex() { #if IS_DETERMINISTIC - Assert.Inconclusive("Craftimizer is built for benchmarking; all random actions are deterministic and not actually random."); + Assert.Inconclusive("Craftimizer is currently built for determinism; all random actions are not actually random."); #endif var actions = new[] - { - ActionType.BasicTouch, - ActionType.BasicSynthesis, - ActionType.GreatStrides, - ActionType.TrainedFinesse, - }; + { + ActionType.BasicTouch, + ActionType.BasicSynthesis, + ActionType.GreatStrides, + ActionType.TrainedFinesse, + }; var set = new ActionSet(); foreach(var action in actions) diff --git a/Craftimizer.sln b/Craftimizer.sln index b8a6abd..3f01805 100644 --- a/Craftimizer.sln +++ b/Craftimizer.sln @@ -53,7 +53,8 @@ Global {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.Build.0 = Debug|x64 {C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.ActiveCfg = Release|x64 {C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.Build.0 = Release|x64 - {C3AEA981-9DA8-405C-995B-86528493891B}.Trace|x64.ActiveCfg = Release|x64 + {C3AEA981-9DA8-405C-995B-86528493891B}.Trace|x64.ActiveCfg = Trace|x64 + {C3AEA981-9DA8-405C-995B-86528493891B}.Trace|x64.Build.0 = Trace|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Solver/ActionSet.cs b/Solver/ActionSet.cs index 98c0b9f..9d73151 100644 --- a/Solver/ActionSet.cs +++ b/Solver/ActionSet.cs @@ -11,6 +11,7 @@ public struct ActionSet public static readonly ActionType[] AcceptedActions = new[] { +#if !IS_TRACE ActionType.StandardTouchCombo, ActionType.AdvancedTouchCombo, ActionType.FocusedTouchCombo, @@ -40,6 +41,36 @@ public struct ActionSet ActionType.Observe, ActionType.MastersMend, ActionType.BasicTouch, +#else + //ActionType.BasicSynthesis, + ActionType.BasicTouch, + ActionType.MastersMend, + ActionType.Observe, + ActionType.WasteNot, + ActionType.Veneration, + ActionType.StandardTouch, + ActionType.GreatStrides, + ActionType.Innovation, + ActionType.BasicSynthesis, + ActionType.WasteNot2, + ActionType.ByregotsBlessing, + ActionType.MuscleMemory, + //ActionType.CarefulSynthesis, + ActionType.Manipulation, + ActionType.PrudentTouch, + ActionType.FocusedSynthesis, + ActionType.FocusedTouch, + ActionType.Reflect, + ActionType.PreparatoryTouch, + //ActionType.Groundwork, + ActionType.DelicateSynthesis, + ActionType.TrainedEye, + ActionType.CarefulSynthesis, + ActionType.AdvancedTouch, + ActionType.Groundwork, + ActionType.PrudentSynthesis, + ActionType.TrainedFinesse, +#endif }; public static readonly int[] AcceptedActionsLUT; @@ -64,7 +95,12 @@ private static int FromAction(ActionType action) } [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static ActionType ToAction(int index) => AcceptedActions[index]; + private static ActionType ToAction(int index) + { + if (index < 0 || index >= AcceptedActions.Length) + throw new ArgumentOutOfRangeException(nameof(index), index, $"Index {index} is out of range for {nameof(ActionSet)}."); + return AcceptedActions[index]; + } [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint ToMask(ActionType action) => 1u << (FromAction(action) + 1); diff --git a/Solver/SimulationNode.cs b/Solver/SimulationNode.cs index 3f8639e..7b77582 100644 --- a/Solver/SimulationNode.cs +++ b/Solver/SimulationNode.cs @@ -46,6 +46,9 @@ private static bool CanByregot(SimulationState state) if (completionState != CompletionState.ProgressComplete) return null; + if (state.Input.Recipe.MaxQuality == 0) + return 1f - ((float)(state.ActionCount + 1) / config.MaxStepCount); + static float Apply(float bonus, float value, float target) => bonus * (target > 0 ? Math.Min(1f, value / target) : 1); @@ -55,7 +58,7 @@ static float Apply(float bonus, float value, float target) => state.Input.Recipe.MaxProgress ); - var byregotBonus = CanByregot(state) ? (state.ActiveEffects.InnerQuiet * .2f + 1) * state.Input.BaseQualityGain : 0; + var byregotBonus = 0;// CanByregot(state) ? (state.ActiveEffects.InnerQuiet * .2f + 1) * state.Input.BaseQualityGain : 0; var qualityScore = Apply( config.ScoreQuality, state.Quality + byregotBonus, @@ -75,7 +78,7 @@ static float Apply(float bonus, float value, float target) => ); var fewerStepsScore = - config.ScoreSteps * (1f - (float)(state.ActionCount + 1) / config.MaxStepCount); + config.ScoreSteps * (1f - ((float)(state.ActionCount + 1) / config.MaxStepCount)); return progressScore + qualityScore + durabilityScore + cpScore + fewerStepsScore; } From 3167b0db44800a1b837a162ba337cd269e1e266f Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sat, 4 Nov 2023 19:53:13 -0700 Subject: [PATCH 53/58] No more byregot for scoring >:c --- Solver/SimulationNode.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/Solver/SimulationNode.cs b/Solver/SimulationNode.cs index 7b77582..0b67f9f 100644 --- a/Solver/SimulationNode.cs +++ b/Solver/SimulationNode.cs @@ -33,14 +33,6 @@ public static CompletionState GetCompletionState(CompletionState simCompletionSt public readonly float? CalculateScore(MCTSConfig config) => CalculateScoreForState(State, SimulationCompletionState, config); - private static bool CanByregot(SimulationState state) - { - if (state.ActiveEffects.InnerQuiet == 0) - return false; - - return BaseComboAction.VerifyDurability2(state, 10); - } - public static float? CalculateScoreForState(SimulationState state, CompletionState completionState, MCTSConfig config) { if (completionState != CompletionState.ProgressComplete) @@ -48,7 +40,7 @@ private static bool CanByregot(SimulationState state) if (state.Input.Recipe.MaxQuality == 0) return 1f - ((float)(state.ActionCount + 1) / config.MaxStepCount); - + static float Apply(float bonus, float value, float target) => bonus * (target > 0 ? Math.Min(1f, value / target) : 1); @@ -58,10 +50,9 @@ static float Apply(float bonus, float value, float target) => state.Input.Recipe.MaxProgress ); - var byregotBonus = 0;// CanByregot(state) ? (state.ActiveEffects.InnerQuiet * .2f + 1) * state.Input.BaseQualityGain : 0; var qualityScore = Apply( config.ScoreQuality, - state.Quality + byregotBonus, + state.Quality, state.Input.Recipe.MaxQuality ); From e07617f1c882cacafdf86a9067a7f6c3781afc38 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sat, 4 Nov 2023 23:15:20 -0700 Subject: [PATCH 54/58] Remove tracing --- Benchmark/Craftimizer.Benchmark.csproj | 7 +----- Benchmark/Program.cs | 4 --- Craftimizer.Test/Craftimizer.Test.csproj | 8 +----- Craftimizer.Test/Solver/ActionSet.cs | 13 ---------- Craftimizer.sln | 10 -------- Simulator/Craftimizer.Simulator.csproj | 7 +----- Solver/ActionSet.cs | 31 ------------------------ Solver/Craftimizer.Solver.csproj | 7 +----- Solver/SimulationNode.cs | 8 ++++-- Solver/Trace.cs | 12 --------- 10 files changed, 10 insertions(+), 97 deletions(-) delete mode 100644 Solver/Trace.cs diff --git a/Benchmark/Craftimizer.Benchmark.csproj b/Benchmark/Craftimizer.Benchmark.csproj index dde07aa..3012dd1 100644 --- a/Benchmark/Craftimizer.Benchmark.csproj +++ b/Benchmark/Craftimizer.Benchmark.csproj @@ -6,7 +6,7 @@ enable enable x64 - Debug;Release;Trace + Debug;Release @@ -32,10 +32,5 @@ $(DefineConstants);IS_DETERMINISTIC - - - True - $(DefineConstants);IS_DETERMINISTIC;IS_TRACE - diff --git a/Benchmark/Program.cs b/Benchmark/Program.cs index 1fea691..70c6299 100644 --- a/Benchmark/Program.cs +++ b/Benchmark/Program.cs @@ -8,12 +8,8 @@ internal static class Program { private static Task Main(string[] args) { -#if !IS_TRACE RunBench(args); return Task.CompletedTask; -#else - return RunTrace(); -#endif // return RunOther(); } diff --git a/Craftimizer.Test/Craftimizer.Test.csproj b/Craftimizer.Test/Craftimizer.Test.csproj index 5c59fdb..f206b95 100644 --- a/Craftimizer.Test/Craftimizer.Test.csproj +++ b/Craftimizer.Test/Craftimizer.Test.csproj @@ -4,11 +4,10 @@ net7.0 enable enable - false true x64 - Debug;Release;Trace + Debug;Release @@ -27,9 +26,4 @@ $(DefineConstants);IS_DETERMINISTIC - - True - $(DefineConstants);IS_DETERMINISTIC;IS_TRACE - - diff --git a/Craftimizer.Test/Solver/ActionSet.cs b/Craftimizer.Test/Solver/ActionSet.cs index d34e387..0f4c9df 100644 --- a/Craftimizer.Test/Solver/ActionSet.cs +++ b/Craftimizer.Test/Solver/ActionSet.cs @@ -89,31 +89,18 @@ public void TestElementAt() Assert.AreEqual(4, set.Count); -#if !IS_TRACE Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(0)); Assert.AreEqual(ActionType.FocusedTouch, set.ElementAt(1)); Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(2)); Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(3)); -#else - Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(0)); - Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(1)); - Assert.AreEqual(ActionType.FocusedTouch, set.ElementAt(2)); - Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(3)); -#endif set.RemoveAction(ActionType.FocusedTouch); Assert.AreEqual(3, set.Count); -#if !IS_TRACE Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(0)); Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(1)); Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(2)); -#else - Assert.AreEqual(ActionType.BasicSynthesis, set.ElementAt(0)); - Assert.AreEqual(ActionType.ByregotsBlessing, set.ElementAt(1)); - Assert.AreEqual(ActionType.DelicateSynthesis, set.ElementAt(2)); -#endif } [TestMethod] diff --git a/Craftimizer.sln b/Craftimizer.sln index 3f01805..1aefbd7 100644 --- a/Craftimizer.sln +++ b/Craftimizer.sln @@ -23,38 +23,28 @@ Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 Release|x64 = Release|x64 - Trace|x64 = Trace|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.ActiveCfg = Debug|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Debug|x64.Build.0 = Debug|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.ActiveCfg = Release|x64 {13C812E9-0D42-4B95-8646-40EEBF30636F}.Release|x64.Build.0 = Release|x64 - {13C812E9-0D42-4B95-8646-40EEBF30636F}.Trace|x64.ActiveCfg = Release|x64 {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.ActiveCfg = Debug|x64 {057C4B64-4D99-4847-9BCF-966571CAE57C}.Debug|x64.Build.0 = Debug|x64 {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.ActiveCfg = Release|x64 {057C4B64-4D99-4847-9BCF-966571CAE57C}.Release|x64.Build.0 = Release|x64 - {057C4B64-4D99-4847-9BCF-966571CAE57C}.Trace|x64.ActiveCfg = Trace|x64 - {057C4B64-4D99-4847-9BCF-966571CAE57C}.Trace|x64.Build.0 = Trace|x64 {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.ActiveCfg = Debug|x64 {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Debug|x64.Build.0 = Debug|x64 {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.ActiveCfg = Release|x64 {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Release|x64.Build.0 = Release|x64 - {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Trace|x64.ActiveCfg = Trace|x64 - {172EE849-AC7E-4F2A-ACAB-EF9D065523B3}.Trace|x64.Build.0 = Trace|x64 {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.ActiveCfg = Debug|x64 {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Debug|x64.Build.0 = Debug|x64 {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.ActiveCfg = Release|x64 {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Release|x64.Build.0 = Release|x64 - {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Trace|x64.ActiveCfg = Trace|x64 - {2B0EA452-6DFC-48DB-9049-EA782E600C21}.Trace|x64.Build.0 = Trace|x64 {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.ActiveCfg = Debug|x64 {C3AEA981-9DA8-405C-995B-86528493891B}.Debug|x64.Build.0 = Debug|x64 {C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.ActiveCfg = Release|x64 {C3AEA981-9DA8-405C-995B-86528493891B}.Release|x64.Build.0 = Release|x64 - {C3AEA981-9DA8-405C-995B-86528493891B}.Trace|x64.ActiveCfg = Trace|x64 - {C3AEA981-9DA8-405C-995B-86528493891B}.Trace|x64.Build.0 = Trace|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Simulator/Craftimizer.Simulator.csproj b/Simulator/Craftimizer.Simulator.csproj index 908f912..7ade191 100644 --- a/Simulator/Craftimizer.Simulator.csproj +++ b/Simulator/Craftimizer.Simulator.csproj @@ -5,7 +5,7 @@ enable enable x64 - Debug;Release;Trace + Debug;Release @@ -19,9 +19,4 @@ $(DefineConstants);IS_DETERMINISTIC - - True - $(DefineConstants);IS_DETERMINISTIC;IS_TRACE - - diff --git a/Solver/ActionSet.cs b/Solver/ActionSet.cs index 9d73151..24b135d 100644 --- a/Solver/ActionSet.cs +++ b/Solver/ActionSet.cs @@ -11,7 +11,6 @@ public struct ActionSet public static readonly ActionType[] AcceptedActions = new[] { -#if !IS_TRACE ActionType.StandardTouchCombo, ActionType.AdvancedTouchCombo, ActionType.FocusedTouchCombo, @@ -41,36 +40,6 @@ public struct ActionSet ActionType.Observe, ActionType.MastersMend, ActionType.BasicTouch, -#else - //ActionType.BasicSynthesis, - ActionType.BasicTouch, - ActionType.MastersMend, - ActionType.Observe, - ActionType.WasteNot, - ActionType.Veneration, - ActionType.StandardTouch, - ActionType.GreatStrides, - ActionType.Innovation, - ActionType.BasicSynthesis, - ActionType.WasteNot2, - ActionType.ByregotsBlessing, - ActionType.MuscleMemory, - //ActionType.CarefulSynthesis, - ActionType.Manipulation, - ActionType.PrudentTouch, - ActionType.FocusedSynthesis, - ActionType.FocusedTouch, - ActionType.Reflect, - ActionType.PreparatoryTouch, - //ActionType.Groundwork, - ActionType.DelicateSynthesis, - ActionType.TrainedEye, - ActionType.CarefulSynthesis, - ActionType.AdvancedTouch, - ActionType.Groundwork, - ActionType.PrudentSynthesis, - ActionType.TrainedFinesse, -#endif }; public static readonly int[] AcceptedActionsLUT; diff --git a/Solver/Craftimizer.Solver.csproj b/Solver/Craftimizer.Solver.csproj index 9be317b..35e7190 100644 --- a/Solver/Craftimizer.Solver.csproj +++ b/Solver/Craftimizer.Solver.csproj @@ -6,7 +6,7 @@ enable True x64 - Debug;Release;Trace + Debug;Release @@ -24,9 +24,4 @@ $(DefineConstants);IS_DETERMINISTIC - - True - $(DefineConstants);IS_DETERMINISTIC;IS_TRACE - - diff --git a/Solver/SimulationNode.cs b/Solver/SimulationNode.cs index 0b67f9f..a0c722b 100644 --- a/Solver/SimulationNode.cs +++ b/Solver/SimulationNode.cs @@ -1,5 +1,7 @@ using Craftimizer.Simulator; using Craftimizer.Simulator.Actions; +using System.Diagnostics.Contracts; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Craftimizer.Solver; @@ -40,9 +42,11 @@ public static CompletionState GetCompletionState(CompletionState simCompletionSt if (state.Input.Recipe.MaxQuality == 0) return 1f - ((float)(state.ActionCount + 1) / config.MaxStepCount); - + + [Pure] + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] static float Apply(float bonus, float value, float target) => - bonus * (target > 0 ? Math.Min(1f, value / target) : 1); + bonus * (target > 0 ? Math.Clamp(value / target, 0, 1) : 1); var progressScore = Apply( config.ScoreProgress, diff --git a/Solver/Trace.cs b/Solver/Trace.cs deleted file mode 100644 index 36cdc02..0000000 --- a/Solver/Trace.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace Craftimizer.Solver; - -internal static class Trace -{ - [Conditional("IS_TRACE")] - [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - public static void Log(string msg) => - Console.WriteLine(msg); -} From a44b15a9e5d93ada4fc1690fb690209df41ee909 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sun, 5 Nov 2023 01:19:05 -0800 Subject: [PATCH 55/58] RecipeNote layout changes --- Craftimizer/Windows/RecipeNote.cs | 84 +++++++++++++------------------ 1 file changed, 34 insertions(+), 50 deletions(-) diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index 2562e9c..21a3b2a 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -197,8 +197,6 @@ public override void Draw() { if (table) { - ImGui.TableSetupColumn("col1", ImGuiTableColumnFlags.WidthFixed, 0); - ImGui.TableSetupColumn("col2", ImGuiTableColumnFlags.WidthFixed, 0); ImGui.TableNextColumn(); DrawCharacterStats(); ImGui.TableNextColumn(); @@ -216,46 +214,35 @@ public override void Draw() ImGui.Separator(); - using (var table = ImRaii.Table("macros", 1, ImGuiTableFlags.SizingStretchSame)) + var panelWidth = availWidth - ImGui.GetStyle().ItemSpacing.X * 2; + using (var panel = ImGuiUtils.GroupPanel("Best Saved Macro", panelWidth, out _)) { - if (table) + var stepsPanelWidthOffset = ImGui.GetContentRegionAvail().X - panelWidth; + if (BestSavedMacro is { } savedMacro) { - ImGui.TableNextColumn(); - - availWidth -= ImGui.GetStyle().ItemSpacing.X * 2; - using (var panel = ImGuiUtils.GroupPanel("Best Saved Macro", availWidth, out _)) - { - var stepsAvailWidthOffset = ImGui.GetContentRegionAvail().X - availWidth; - if (BestSavedMacro is { } savedMacro) - { - ImGuiUtils.TextCentered(savedMacro.Item1.Name, availWidth); - DrawMacro((savedMacro.Item1.Actions, savedMacro.Item2), a => { savedMacro.Item1.ActionEnumerable = a; Service.Configuration.Save(); }, stepsAvailWidthOffset, true); - } - else - { - ImGui.Text(""); - DrawMacro(null, null, stepsAvailWidthOffset, true); - } - } + ImGuiUtils.TextCentered(savedMacro.Item1.Name, panelWidth); + DrawMacro((savedMacro.Item1.Actions, savedMacro.Item2), a => { savedMacro.Item1.ActionEnumerable = a; Service.Configuration.Save(); }, stepsPanelWidthOffset, true); + } + else + DrawMacro(null, null, stepsPanelWidthOffset, true); + } - using (var panel = ImGuiUtils.GroupPanel("Suggested Macro", availWidth, out _)) - { - var stepsAvailWidthOffset = ImGui.GetContentRegionAvail().X - availWidth; - if (BestSuggestedMacro is { } suggestedMacro) - DrawMacro((suggestedMacro.Actions, suggestedMacro.State), null, stepsAvailWidthOffset, false); - else - DrawMacro(null, null, stepsAvailWidthOffset, false); - } + using (var panel = ImGuiUtils.GroupPanel("Suggested Macro", panelWidth, out _)) + { + var stepsPanelWidthOffset = ImGui.GetContentRegionAvail().X - panelWidth; + if (BestSuggestedMacro is { } suggestedMacro) + DrawMacro((suggestedMacro.Actions, suggestedMacro.State), null, stepsPanelWidthOffset, false); + else + DrawMacro(null, null, stepsPanelWidthOffset, false); + } - ImGuiHelpers.ScaledDummy(5); + ImGuiHelpers.ScaledDummy(5); - if (ImGui.Button("View Saved Macros", new(-1, 0))) - Service.Plugin.OpenMacroListWindow(); + if (ImGui.Button("View Saved Macros", new(availWidth, 0))) + Service.Plugin.OpenMacroListWindow(); - if (ImGui.Button("Open in Simulator", new(-1, 0))) - Service.Plugin.OpenMacroEditor(CharacterStats!, RecipeData!, new(Service.ClientState.LocalPlayer!.StatusList), Enumerable.Empty(), null); - } - } + if (ImGui.Button("Open in Simulator", new(availWidth, 0))) + Service.Plugin.OpenMacroEditor(CharacterStats!, RecipeData!, new(Service.ClientState.LocalPlayer!.StatusList), Enumerable.Empty(), null); } private void DrawCharacterStats() @@ -440,26 +427,25 @@ private void DrawCharacterStats() break; case CraftableStatus.OK: { - using var table = ImRaii.Table("characterStats", 2, ImGuiTableFlags.NoHostExtendX); + using var table = ImRaii.Table("characterStats", 2); if (table) { - ImGui.TableSetupColumn("ccol1", ImGuiTableColumnFlags.WidthFixed, 100); - ImGui.TableSetupColumn("ccol2", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 100); ImGui.TableNextColumn(); ImGui.Text("Craftsmanship"); ImGui.TableNextColumn(); - ImGui.Text($"{CharacterStats!.Craftsmanship}"); + ImGuiUtils.TextRight($"{CharacterStats!.Craftsmanship}"); ImGui.TableNextColumn(); ImGui.Text("Control"); ImGui.TableNextColumn(); - ImGui.Text($"{CharacterStats.Control}"); + ImGuiUtils.TextRight($"{CharacterStats.Control}"); ImGui.TableNextColumn(); ImGui.Text("CP"); ImGui.TableNextColumn(); - ImGui.Text($"{CharacterStats.CP}"); + ImGuiUtils.TextRight($"{CharacterStats.CP}"); } } break; @@ -532,23 +518,22 @@ private void DrawRecipeStats() using var table = ImRaii.Table("recipeStats", 2); if (table) { - ImGui.TableSetupColumn("rcol1", ImGuiTableColumnFlags.WidthFixed, 100); - ImGui.TableSetupColumn("rcol2", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 100); ImGui.TableNextColumn(); ImGui.Text("Progress"); ImGui.TableNextColumn(); - ImGui.Text($"{RecipeData.RecipeInfo.MaxProgress}"); + ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxProgress}"); ImGui.TableNextColumn(); ImGui.Text("Quality"); ImGui.TableNextColumn(); - ImGui.Text($"{RecipeData.RecipeInfo.MaxQuality}"); + ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxQuality}"); ImGui.TableNextColumn(); ImGui.Text("Durability"); ImGui.TableNextColumn(); - ImGui.Text($"{RecipeData.RecipeInfo.MaxDurability}"); + ImGuiUtils.TextRight($"{RecipeData.RecipeInfo.MaxDurability}"); } } @@ -709,11 +694,10 @@ private static void DrawRequiredStatsTable(int current, int required) if (current >= required) throw new ArgumentOutOfRangeException(nameof(current)); - using var table = ImRaii.Table("requiredStats", 2, ImGuiTableFlags.NoHostExtendX); + using var table = ImRaii.Table("requiredStats", 2); if (table) { - ImGui.TableSetupColumn("ccol1", ImGuiTableColumnFlags.WidthFixed, 100); - ImGui.TableSetupColumn("ccol2", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 100); ImGui.TableNextColumn(); ImGui.Text("Current"); From 80a758e24bc11a1b429dcbc9219963a6d04b41ae Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Sun, 5 Nov 2023 01:25:11 -0800 Subject: [PATCH 56/58] Release 1.9.2.0 --- Craftimizer/Craftimizer.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Craftimizer/Craftimizer.csproj b/Craftimizer/Craftimizer.csproj index 3368159..e42b3ec 100644 --- a/Craftimizer/Craftimizer.csproj +++ b/Craftimizer/Craftimizer.csproj @@ -2,7 +2,7 @@ Asriel Camora - 1.9.1.0 + 1.9.2.0 https://github.com/WorkingRobot/craftimizer.git Debug;Release From 9a490c1653263e5ed39d3a6ccf5877844bcf1219 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Mon, 6 Nov 2023 16:31:51 -0800 Subject: [PATCH 57/58] Rename test project folder --- Craftimizer.sln | 2 +- {Craftimizer.Test => Test}/Craftimizer.Test.csproj | 0 {Craftimizer.Test => Test}/Simulator/Simulator.cs | 0 {Craftimizer.Test => Test}/Solver/ActionSet.cs | 0 {Craftimizer.Test => Test}/Usings.cs | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename {Craftimizer.Test => Test}/Craftimizer.Test.csproj (100%) rename {Craftimizer.Test => Test}/Simulator/Simulator.cs (100%) rename {Craftimizer.Test => Test}/Solver/ActionSet.cs (100%) rename {Craftimizer.Test => Test}/Usings.cs (100%) diff --git a/Craftimizer.sln b/Craftimizer.sln index 1aefbd7..3c77500 100644 --- a/Craftimizer.sln +++ b/Craftimizer.sln @@ -17,7 +17,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Solver", "Solve {172EE849-AC7E-4F2A-ACAB-EF9D065523B3} = {172EE849-AC7E-4F2A-ACAB-EF9D065523B3} EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Test", "Craftimizer.Test\Craftimizer.Test.csproj", "{C3AEA981-9DA8-405C-995B-86528493891B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Craftimizer.Test", "Test\Craftimizer.Test.csproj", "{C3AEA981-9DA8-405C-995B-86528493891B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/Craftimizer.Test/Craftimizer.Test.csproj b/Test/Craftimizer.Test.csproj similarity index 100% rename from Craftimizer.Test/Craftimizer.Test.csproj rename to Test/Craftimizer.Test.csproj diff --git a/Craftimizer.Test/Simulator/Simulator.cs b/Test/Simulator/Simulator.cs similarity index 100% rename from Craftimizer.Test/Simulator/Simulator.cs rename to Test/Simulator/Simulator.cs diff --git a/Craftimizer.Test/Solver/ActionSet.cs b/Test/Solver/ActionSet.cs similarity index 100% rename from Craftimizer.Test/Solver/ActionSet.cs rename to Test/Solver/ActionSet.cs diff --git a/Craftimizer.Test/Usings.cs b/Test/Usings.cs similarity index 100% rename from Craftimizer.Test/Usings.cs rename to Test/Usings.cs From f8c5f9c9db5cc3030b7c8144f69d400a6455e833 Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Wed, 8 Nov 2023 04:24:10 -0800 Subject: [PATCH 58/58] Fix viewport offset on RecipeNote --- Craftimizer/Windows/RecipeNote.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Craftimizer/Windows/RecipeNote.cs b/Craftimizer/Windows/RecipeNote.cs index 21a3b2a..5abd350 100644 --- a/Craftimizer/Windows/RecipeNote.cs +++ b/Craftimizer/Windows/RecipeNote.cs @@ -41,7 +41,7 @@ public sealed unsafe class RecipeNote : Window, IDisposable | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus; - public enum CraftableStatus + public enum CraftableStatus { OK, LockedClassJob, @@ -71,7 +71,7 @@ public enum CraftableStatus private IDalamudTextureWrap NoManipulationBadge { get; } private GameFontHandle AxisFont { get; } - public RecipeNote() : base("Craftimizer RecipeNote", WindowFlags, false) + public RecipeNote() : base("Craftimizer RecipeNote", WindowFlags) { ExpertBadge = Service.IconManager.GetAssemblyTexture("Graphics.expert_badge.png"); CollectibleBadge = Service.IconManager.GetAssemblyTexture("Graphics.collectible_badge.png"); @@ -85,6 +85,12 @@ public RecipeNote() : base("Craftimizer RecipeNote", WindowFlags, false) ShowCloseButton = false; IsOpen = true; + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new(-1), + MaximumSize = new(10000, 10000) + }; + Service.WindowSystem.AddWindow(this); } @@ -182,12 +188,7 @@ public override void PreDraw() var node = (AtkResNode*)Addon->Unk458; // unit.GetNodeById(59); var nodeParent = Addon->Unk258; // unit.GetNodeById(57); - Position = pos + new Vector2(size.X, (nodeParent->Y + node->Y) * scale); - SizeConstraints = new WindowSizeConstraints - { - MinimumSize = new(-1), - MaximumSize = new(10000, 10000) - }; + Position = ImGuiHelpers.MainViewport.Pos + pos + new Vector2(size.X, (nodeParent->Y + node->Y) * scale); } public override void Draw()