From f5991618783aeb2a5fbafbfda45bae96b0b3ea57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?= <93977077+mukjepscarlet@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:52:59 +0800 Subject: [PATCH 1/3] squashed commit --- src-theme/package-lock.json | 14 - src-theme/public/img/clickgui/icon-any.svg | 5 + src-theme/public/img/clickgui/icon-arrow.png | Bin 0 -> 1054 bytes src-theme/public/img/clickgui/icon-axe.png | Bin 0 -> 1108 bytes src-theme/public/img/clickgui/icon-blocks.png | Bin 0 -> 8465 bytes src-theme/public/img/clickgui/icon-egg.png | Bin 0 -> 1017 bytes src-theme/public/img/clickgui/icon-food.png | Bin 0 -> 1208 bytes src-theme/public/img/clickgui/icon-hoe.png | Bin 0 -> 1074 bytes src-theme/public/img/clickgui/icon-ignore.svg | 7 + .../public/img/clickgui/icon-pickaxe.png | Bin 0 -> 1099 bytes src-theme/public/img/clickgui/icon-potion.png | Bin 0 -> 1170 bytes .../img/clickgui/icon-question-mark.svg | 12 + src-theme/public/img/clickgui/icon-shovel.png | Bin 0 -> 2734 bytes src-theme/public/img/clickgui/icon-sword.png | Bin 0 -> 1193 bytes .../public/img/clickgui/icon-value-none.svg | 11 + .../public/img/menu/icon-exit-danger.svg | 14 + src-theme/public/img/menu/icon-plus.svg | 7 + src-theme/src/integration/types.ts | 42 +++ .../setting/common/GenericSetting.svelte | 3 + .../clickgui/setting/common/ValueInput.svelte | 2 +- .../InventoryPresetValue.svelte | 79 +++++ .../setting/inventoryPreset/ItemImage.svelte | 57 ++++ .../inventoryPreset/ItemPreview.svelte | 42 +++ .../config/ItemGroupSelector.svelte | 186 +++++++++++ .../inventoryPreset/config/PresetModal.svelte | 172 ++++++++++ .../config/PresetTooltip.svelte | 137 ++++++++ .../setting/inventoryPreset/config/item.scss | 11 + .../maxStacks/MaxStacksContainer.svelte | 57 ++++ .../config/maxStacks/StackContainer.svelte | 158 +++++++++ .../config/presetItem/GroupComponent.svelte | 126 ++++++++ .../presetItem/GroupPreferenceSelector.svelte | 141 ++++++++ .../config/presetItem/GroupPreview.svelte | 48 +++ .../config/presetItem/Items.svelte | 92 ++++++ .../presetItem/PresetItemSelector.svelte | 305 ++++++++++++++++++ .../inventoryPreset/config/select.scss | 121 +++++++ src-theme/src/util/utils.ts | 32 ++ .../liquidbounce/config/gson/GsonInstance.kt | 4 + .../adapter/FrontendSlotPreferenceAdapter.kt | 46 +++ .../gson/adapter/InventoryPresetAdapter.kt | 35 ++ .../liquidbounce/config/types/Value.kt | 8 + .../config/types/nesting/Configurable.kt | 16 + .../inventoryPreset/FrontendItemLimitRules.kt | 15 + .../inventoryPreset/FrontendSlotPreference.kt | 144 +++++++++ .../inventoryPreset/InventoryPreset.kt | 71 ++++ .../module/modules/misc/ModuleElytraSwap.kt | 2 +- .../player/cheststealer/ModuleChestStealer.kt | 4 +- .../player/invcleaner/CleanupPlanGenerator.kt | 207 ++++++------ .../player/invcleaner/CleanupPlanTemplate.kt | 85 +++++ .../ItemAmountConstraintProvider.kt | 32 ++ .../player/invcleaner/ItemCategorization.kt | 132 +++----- .../player/invcleaner/ItemDispenserRack.kt | 45 +++ .../modules/player/invcleaner/ItemMerge.kt | 2 +- .../ItemNumberConstraintEnforcer.kt | 64 ++++ .../invcleaner/ItemNumberConstraints.kt | 50 ++- .../modules/player/invcleaner/ItemPacker.kt | 164 ---------- .../invcleaner/ModuleInventoryCleaner.kt | 214 +++++++----- .../player/invcleaner/WishOrganizer.kt | 69 ++++ .../player/invcleaner/items/ArmorItemFacet.kt | 4 +- .../player/invcleaner/items/ArrowItemFacet.kt | 41 --- .../player/invcleaner/items/BlockItemFacet.kt | 2 +- .../player/invcleaner/items/BowItemFacet.kt | 10 +- .../invcleaner/items/CrossbowItemFacet.kt | 14 +- .../player/invcleaner/items/FoodItemFacet.kt | 21 +- .../player/invcleaner/items/ItemFacet.kt | 22 +- .../invcleaner/items/MiningToolItemFacet.kt | 27 +- .../invcleaner/items/PotionItemFacet.kt | 43 +-- .../invcleaner/items/PrimitiveItemFacet.kt | 19 +- .../player/invcleaner/items/RodItemFacet.kt | 47 --- .../invcleaner/items/ShieldItemFacet.kt | 47 --- .../player/invcleaner/items/SwordItemFacet.kt | 4 +- .../invcleaner/items/ThrowableItemFacet.kt | 2 +- .../invcleaner/items/WeaponItemFacet.kt | 52 ++- .../module/modules/render/ModuleZoom.kt | 2 + .../scaffold/ScaffoldBlockItemSelection.kt | 2 +- .../liquidbounce/utils/inventory/ItemSlot.kt | 17 +- .../liquidbounce/utils/item/ArmorPiece.kt | 2 +- .../utils/sorting/ComparatorChain.kt | 10 +- 77 files changed, 2978 insertions(+), 700 deletions(-) create mode 100644 src-theme/public/img/clickgui/icon-any.svg create mode 100644 src-theme/public/img/clickgui/icon-arrow.png create mode 100644 src-theme/public/img/clickgui/icon-axe.png create mode 100644 src-theme/public/img/clickgui/icon-blocks.png create mode 100644 src-theme/public/img/clickgui/icon-egg.png create mode 100644 src-theme/public/img/clickgui/icon-food.png create mode 100644 src-theme/public/img/clickgui/icon-hoe.png create mode 100644 src-theme/public/img/clickgui/icon-ignore.svg create mode 100644 src-theme/public/img/clickgui/icon-pickaxe.png create mode 100644 src-theme/public/img/clickgui/icon-potion.png create mode 100644 src-theme/public/img/clickgui/icon-question-mark.svg create mode 100644 src-theme/public/img/clickgui/icon-shovel.png create mode 100644 src-theme/public/img/clickgui/icon-sword.png create mode 100644 src-theme/public/img/clickgui/icon-value-none.svg create mode 100644 src-theme/public/img/menu/icon-exit-danger.svg create mode 100644 src-theme/public/img/menu/icon-plus.svg create mode 100644 src-theme/src/routes/clickgui/setting/inventoryPreset/InventoryPresetValue.svelte create mode 100644 src-theme/src/routes/clickgui/setting/inventoryPreset/ItemImage.svelte create mode 100644 src-theme/src/routes/clickgui/setting/inventoryPreset/ItemPreview.svelte create mode 100644 src-theme/src/routes/clickgui/setting/inventoryPreset/config/ItemGroupSelector.svelte create mode 100644 src-theme/src/routes/clickgui/setting/inventoryPreset/config/PresetModal.svelte create mode 100644 src-theme/src/routes/clickgui/setting/inventoryPreset/config/PresetTooltip.svelte create mode 100644 src-theme/src/routes/clickgui/setting/inventoryPreset/config/item.scss create mode 100644 src-theme/src/routes/clickgui/setting/inventoryPreset/config/maxStacks/MaxStacksContainer.svelte create mode 100644 src-theme/src/routes/clickgui/setting/inventoryPreset/config/maxStacks/StackContainer.svelte create mode 100644 src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupComponent.svelte create mode 100644 src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupPreferenceSelector.svelte create mode 100644 src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupPreview.svelte create mode 100644 src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/Items.svelte create mode 100644 src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/PresetItemSelector.svelte create mode 100644 src-theme/src/routes/clickgui/setting/inventoryPreset/config/select.scss create mode 100644 src-theme/src/util/utils.ts create mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/config/gson/adapter/FrontendSlotPreferenceAdapter.kt create mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/config/gson/adapter/InventoryPresetAdapter.kt create mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/FrontendItemLimitRules.kt create mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/FrontendSlotPreference.kt create mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/InventoryPreset.kt create mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/CleanupPlanTemplate.kt create mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemAmountConstraintProvider.kt create mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemDispenserRack.kt create mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemNumberConstraintEnforcer.kt delete mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemPacker.kt create mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/WishOrganizer.kt delete mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ArrowItemFacet.kt delete mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/RodItemFacet.kt delete mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ShieldItemFacet.kt diff --git a/src-theme/package-lock.json b/src-theme/package-lock.json index 238703bf75e..5f1e5d6fbd8 100644 --- a/src-theme/package-lock.json +++ b/src-theme/package-lock.json @@ -2031,20 +2031,6 @@ } } }, - "node_modules/svelte-check/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/svelte-spa-router": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-4.0.1.tgz", diff --git a/src-theme/public/img/clickgui/icon-any.svg b/src-theme/public/img/clickgui/icon-any.svg new file mode 100644 index 00000000000..3fb958988ad --- /dev/null +++ b/src-theme/public/img/clickgui/icon-any.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src-theme/public/img/clickgui/icon-arrow.png b/src-theme/public/img/clickgui/icon-arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..10da0be3549bbc0c8891f050a440b6f1c23c8894 GIT binary patch literal 1054 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I4M=vMPuFE&U~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}*5j~)%+dJEU9PM7!UPIW1k8Q2NU3itm+K0Ftky7DAHBa#YP_CZ-HRVY6?{nE z`0BxnXrLz9G){;$+D$>K>;yEQ}9x3^-J#iA^$r`(eL zq35FKY%Q8<`u=3R#Hp9sr(>2|cCmyLc(eiC#d zlu7wGlhy@=-E|-Af81R=b^fd`lbi|{ygpiJcdX%E?1I4JIq#E?XU<{xm;S&j|EG3o z!nes^qP)9h7@2P$I9lo*{H@`%Y{HL|6MSz@{bSNvBUfR+IR6w=^LB^xv#v)M%P;45 zn6=C69s_&(fr`dW*RA(h-?(>sfBLMye;EwzihB5(Z?6SL3tN)6y9IL4HOno>p@g?p-=6>E7E;Br=6aEvg!X< zV^=B+@(VP~fYgM&9{=iGBBw3%J@K}TJNA9i&X#k&m!E)q1M@S)>vd5%)dF{C+(0-G e8e|M$aG*QCW>Nyz!OAN?KzvVEKbLh*2~7YL52INC literal 0 HcmV?d00001 diff --git a/src-theme/public/img/clickgui/icon-axe.png b/src-theme/public/img/clickgui/icon-axe.png new file mode 100644 index 0000000000000000000000000000000000000000..749add688619978d9d41a7ba31337a96c83e7872 GIT binary patch literal 1108 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 literal 0 HcmV?d00001 diff --git a/src-theme/public/img/clickgui/icon-egg.png b/src-theme/public/img/clickgui/icon-egg.png new file mode 100644 index 0000000000000000000000000000000000000000..c254f4b725fc9a0d06cbec1386dbf7a72e617fae GIT binary patch literal 1017 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I4M=vMPuFE&U~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}*5j~)%+dJEU9PM7!UPIW1k8Q2NU3itm+K0Ftky7DAHBa#YP_CZ-HRVY6?{nE z`0BxnXrLz9G){;$+D$>K>;yEQ}9x3^-J#iA^$r`(eL zq35FKY%Q8<`u=3R#Hp9sr(>2|cCmyLc(eiC#d zlu7wGlhy@=-E|-Af81R=b^fd`lbi|{ygpiJcdX%E?1I4JIq#E?XU<{xm;S&j|EG3o z!nes^qP)9h7@2P$I9lo*{H@`%Y{HL|6MSz@{bSNvBUfR+IR6w=^LB^xv#v)M%P;45 zn6=C69s_&(fr`dW*RA(h-?(>sfBLMye;EwzihB5(Z?6SL3tN)6y9tdTT=yP)M@GHKN2hKQ}iuuY|$5 zC^fMpHASI3vm`^o-P1Q9ypc~F7~K~=T^vIyZoR#`F}K-4q#;qbQdp}>%StUl@K~at z%CQB@7qx575&V z^>6&;_45k8)XGPd%eYVa9Xm~R%R8I%7k1rI$5b48##-?rr~A8CyZF{dU%$%7!Q|4Q z;J^Z--mA=RiTS(EJ!Dn6?Ye8*a!WE_Sf2kp=gD0`YlVGHC{A3vsa{>BD$ZR~xNk+! zJ=u-SN(f_64TbXLE2~)UZ_J(Dp1=;b6YPtJw>QmiaZ%^E_C>gTe~DWM4fH7|~# literal 0 HcmV?d00001 diff --git a/src-theme/public/img/clickgui/icon-food.png b/src-theme/public/img/clickgui/icon-food.png new file mode 100644 index 0000000000000000000000000000000000000000..a62f6529a18d3e7c4b2031bdeb693581cca56b7f GIT binary patch literal 1208 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I4M=vMPuFE&U~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}*5j~)%+dJEU9PM7!UPIW1k8Q2NU3itm+K0Ftky7DAHBa#YP_CZ-HRVY6?{nE z`0BxnXrLz9G){;$+D$>K>;yEQ}9x3^-J#iA^$r`(eL zq35FKY%Q8<`u=3R#Hp9sr(>2|cCmyLc(eiC#d zlu7wGlhy@=-E|-Af81R=b^fd`lbi|{ygpiJcdX%E?1I4JIq#E?XU<{xm;S&j|EG3o z!nes^qP)9h7@2P$I9lo*{H@`%Y{HL|6MSz@{bSNvBUfR+IR6w=^LB^xv#v)M%P;45 zn6=C69s_&(fr`dW*RA(h-?(>sfBLMye;EwzihB5(Z?6SL3tN)6y9qlk;aKP)M@GHKN2hKQ}iuuY|$5 zC^fMpHASI3vm`^o-P1Q9ypd0wfq`j)r;B4q#jUq@4ZA)$NHjeB`aIt?KtN=ppv(G+ zacgWBZvVixQhAyfS5s5R*LR|JH#&_|<}a69bZk23kHk-3S%KCtFkA?=UH-s$)v8~8 zYpiAX>R(y^ROe;sU~p{U5P(v*zTDURG52hm@T`IuJ#qEZd~jJo1r7nmL(6Bj{JHe| zFW=hLuW$X&e}mA-*u)^HP!pA2H}`(t_WPRs->ddIy`FbIJ>zbIfyb}P-9_TM)(T86 z4GIn{98hZQsc+d;B}EtCe|J89`)%36(%*mWwyevXbz#@d9I>eF5jsw<^R`E4#J<{{ z`*hZt8;pgVZYbKl{=F`L#d$nyE7w}-Ubls%pMTn%?zEeKeqrhN-?m%cRoJv$n`&pc zk@=BO53(1duFibk;-b!Rv2>@6T>ph#FIqITt_$CN+pvR0*+D>oiG@)KM74y8+H-dL zv9%peym_=23kVCA*|C6W37thBRcPG2fpFJ_^%e&yfi=8fCKD()# zmLZu2GW6e;b9ZF5uOCtQ+Ok;t`BAyvI5f+;2{SLzzeG${TB}Ub$8pM$=xen{O+D7ZVfR8YMP)zebo9d z<~!onJO7uB)SJHWv8%e;N_F0q@!w0VTCY9*wZ0u>B*MQej6p4N;dUM+Nk%h$HtpH} q`s=JO6-)gskm4BZaRxBBuuA^g%L&b*+umFO@jYGrT-G@yGywo@5BS&s literal 0 HcmV?d00001 diff --git a/src-theme/public/img/clickgui/icon-hoe.png b/src-theme/public/img/clickgui/icon-hoe.png new file mode 100644 index 0000000000000000000000000000000000000000..7acfad74a335462d07d82d9f112062151d09ccbe GIT binary patch literal 1074 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I4M=vMPuFE&U~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}*5j~)%+dJEU9PM7!UPIW1k8Q2NU3itm+K0Ftky7DAHBa#YP_CZ-HRVY6?{nE z`0BxnXrLz9G){;$+D$>K>;yEQ}9x3^-J#iA^$r`(eL zq35FKY%Q8<`u=3R#Hp9sr(>2|cCmyLc(eiC#d zlu7wGlhy@=-E|-Af81R=b^fd`lbi|{ygpiJcdX%E?1I4JIq#E?XU<{xm;S&j|EG3o z!nes^qP)9h7@2P$I9lo*{H@`%Y{HL|6MSz@{bSNvBUfR+IR6w=^LB^xv#v)M%P;45 zn6=C69s_&(fr`dW*RA(h-?(>sfBLMye;EwzihB5(Z?6SL3tN)6y9EaktaqI1!jk(PZA`OY^Jq}O%WHly4^qgzu zZu`I&;`5McBNMklvvEW6OHE6jw)d<3Z+O++^|YDmuMV_?fuUjVuTSbfVnajwP3^wd z?U#R5|4G-BrGvq-fkOaFO)J^q=}~g1aQ9uO^Rt&}zWknj+iXjozOb;h0+UOFf&&W& zl0f6}^0W70j&oiTa~ODpHKUvpJgi=5VaZSMK!7j~_CJ;^MY{fJ->vf*N>LZ8$w zp7bh75;E;e&WOGLePQWklbJ0KIM^7K90U}YSisc$!zwAsuXgeI`pjC!X`4R}Xx!_s zQc>wbP0A9;uB{Q7Ji)xv&tlR3AX6UaOYf&w9ZCdR$%E`rvx6#6<}VWN+oa=sKKga| zsy7W5O-SBlIiWYRWzU9dvVD7fEZ!i30c=gqp@(XU?+R`(LNy2~a0oCqF<4HxyJv2B z`(Zv + + + + + + diff --git a/src-theme/public/img/clickgui/icon-pickaxe.png b/src-theme/public/img/clickgui/icon-pickaxe.png new file mode 100644 index 0000000000000000000000000000000000000000..d669fb13e9ca43987491429ff82d583401816ee0 GIT binary patch literal 1099 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I4M=vMPuFE&U~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}*5j~)%+dJEU9PM7!UPIW1k8Q2NU3itm+K0Ftky7DAHBa#YP_CZ-HRVY6?{nE z`0BxnXrLz9G){;$+D$>K>;yEQ}9x3^-J#iA^$r`(eL zq35FKY%Q8<`u=3R#Hp9sr(>2|cCmyLc(eiC#d zlu7wGlhy@=-E|-Af81R=b^fd`lbi|{ygpiJcdX%E?1I4JIq#E?XU<{xm;S&j|EG3o z!nes^qP)9h7@2P$I9lo*{H@`%Y{HL|6MSz@{bSNvBUfR+IR6w=^LB^xv#v)M%P;45 zn6=C69s_&(fr`dW*RA(h-?(>sfBLMye;EwzihB5(Z?6SL3tN)6y9s&1kU!C?r|p8d2h$pPQSSSHj?2 zl$uzQnxasiS(2gP?&%v4-pD7;z`&&J>EaktaqI2fjk$*$B-#==ZZ>qCvkp+$D55Ic z%H5X0uC;8zhKPf@ti}@goM!^OcjavRM7;b<53?Kx^ROewy7@{yjHy<|Y4- zx&FFT#D2H@{$>sV#wG?q1t?XLX)<|2$)Q54xr?r6-!_Z-USZQ!*Y~tYa;>#o|HWNL zlLXgBuMNAn>+{c;Et1v>eF-caOfC%y4q)nnX1P<`j{EQT?%99%VZfKKdE2A6yer!2 z^7_^`Q_h2eJP4Hv4*QPZ2{+KW{nl*Ol{epizxaB|TDvsX{k4x%0ZTK&IUEz#CkcI0 zeA%L0>lJ$0&0S}MDD|0VyYo0uPQurVq*2q-YIfT`vLp-)aPtz~Ne%JW&n{0Auf-c!ut7u5C+g$M25XrcjPS?+TP)f0=yLNHO>a$M43{#1!H4tm XizXB>yqR)pE{N~x>gTe~DWM4f_uICI literal 0 HcmV?d00001 diff --git a/src-theme/public/img/clickgui/icon-potion.png b/src-theme/public/img/clickgui/icon-potion.png new file mode 100644 index 0000000000000000000000000000000000000000..63e33c88ab8a19c45942aceb04483ec9d1a93aa8 GIT binary patch literal 1170 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I4M=vMPuFE&U~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}*5j~)%+dJEU9PM7!UPIW1k8Q2NU3itm+K0Ftky7DAHBa#YP_CZ-HRVY6?{nE z`0BxnXrLz9G){;$+D$>K>;yEQ}9x3^-J#iA^$r`(eL zq35FKY%Q8<`u=3R#Hp9sr(>2|cCmyLc(eiC#d zlu7wGlhy@=-E|-Af81R=b^fd`lbi|{ygpiJcdX%E?1I4JIq#E?XU<{xm;S&j|EG3o z!nes^qP)9h7@2P$I9lo*{H@`%Y{HL|6MSz@{bSNvBUfR+IR6w=^LB^xv#v)M%P;45 zn6=C69s_&(fr`dW*RA(h-?(>sfBLMye;EwzihB5(Z?6SL3tN)6y9&pCB4=&!7mj~Jh2YY7u z+aJu9>OUT@`CXc?-9PhT|NcqoLJCYS4GIn{9AN6y`}Ok`^jE6Ov>!gm2@!(Iu$=g9 z#QD>|YY~U9McMAyjQv?#U$L+-Dme%!FhQx7`7>JnTq@hm>uYiAZP}&e{=96>>y7@@ z`8lw3FgP}F2tX;hQ`JuUKCa<(|5CjCa%RT~ zehx8BNLI6)@IF~5Bs906WcS?*br(4grETV1Yc1FBx&QssBF?qlem=kc)@9tyFsbt5 z25A*k;1FPJVgOTzoTol}8F4o4v*!Hj-FKZ{XKjtjh}|1k&IFZ*QchYo{hiib&%X5c z+2_hjf8Twt&WJ4$5~|g@UbVLjP3<9nuVR0XFPVXX7psl6rg~+>epwkF`1`NjmUUON zEE$m<-oa2AzqB&BDo|wM_fDPZr!Um4oWJer>#t6))h2sp#J+p~zkunH5D#)>Zu;;u sFW$n+@MP>eVLo+uG(lpE0Ss)e*vE=wr|0(0t_SfwUHx3vIVCg!00C{&t^fc4 literal 0 HcmV?d00001 diff --git a/src-theme/public/img/clickgui/icon-question-mark.svg b/src-theme/public/img/clickgui/icon-question-mark.svg new file mode 100644 index 00000000000..5cfc8566246 --- /dev/null +++ b/src-theme/public/img/clickgui/icon-question-mark.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/src-theme/public/img/clickgui/icon-shovel.png b/src-theme/public/img/clickgui/icon-shovel.png new file mode 100644 index 0000000000000000000000000000000000000000..b77d5bd843ed92c65b42b1e0d388c140a50432c7 GIT binary patch literal 2734 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 DB4rQN literal 0 HcmV?d00001 diff --git a/src-theme/public/img/clickgui/icon-sword.png b/src-theme/public/img/clickgui/icon-sword.png new file mode 100644 index 0000000000000000000000000000000000000000..5f6f29bd2aeabacbda0f4e0cabe5e8cd3689b79e GIT binary patch literal 1193 zcmeAS@N?(olHy`uVBq!ia0vp^3qY8I4M=vMPuFE&U~I{Bb`J1#c2+1T%1_J8No8Qr zm{>c}*5j~)%+dJEU9PM7!UPIW1k8Q2NU3itm+K0Ftky7DAHBa#YP_CZ-HRVY6?{nE z`0BxnXrLz9G){;$+D$>K>;yEQ}9x3^-J#iA^$r`(eL zq35FKY%Q8<`u=3R#Hp9sr(>2|cCmyLc(eiC#d zlu7wGlhy@=-E|-Af81R=b^fd`lbi|{ygpiJcdX%E?1I4JIq#E?XU<{xm;S&j|EG3o z!nes^qP)9h7@2P$I9lo*{H@`%Y{HL|6MSz@{bSNvBUfR+IR6w=^LB^xv#v)M%P;45 zn6=C69s_&(fr`dW*RA(h-?(>sfBLMye;EwzihB5(Z?6SL3tN)6y9EaktaqI2fgPBZ@5^WDnZ!}k2mJ(sHjcRbu z-Y9DQL1&u6i-Te%>!VvW73Zf+nmgmUCBL?|{^kw#i<{YIEsN9?W%iMW4E38ksuF~OM`*~3kR6`W&TCW*2u|66Uj3kUI#s9_WfCTu1NMEuY+Y(@zL`H zce=%*&LdTO`}D4r#$Mz+{;;6qTJHAS8F%Yv1C2z456BBEo$~YVUfwMEQbnb7|NX_K zyYK2nr5{RUM+zj6E0eD7pX;x#_Huo$tz3Ub?Df)%Rv^ovwn8b<*Kc+I747U%lX?8n zV%NR5W!=|$-4-wUo|u$`VcyaI>MC;!&ZKRAQFZ*WVbu3SOCCmks*9ho#)kWTyQ}~( z9vvGvVAMV%pZZ#@sq-HGXykO)zn;zIANB?jiQu>l2&hq0`77Ud`l-;`>gk*L>%N}i qLotKJ<7nH<|1nEmA3zs3wPO_dH#5v@Nkl&b5O})!xvX + + + + + + + + + + diff --git a/src-theme/public/img/menu/icon-exit-danger.svg b/src-theme/public/img/menu/icon-exit-danger.svg new file mode 100644 index 00000000000..916d07ee3ef --- /dev/null +++ b/src-theme/public/img/menu/icon-exit-danger.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/src-theme/public/img/menu/icon-plus.svg b/src-theme/public/img/menu/icon-plus.svg new file mode 100644 index 00000000000..7f06b3149d9 --- /dev/null +++ b/src-theme/public/img/menu/icon-plus.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src-theme/src/integration/types.ts b/src-theme/src/integration/types.ts index 4bbe1384896..3777a475c21 100644 --- a/src-theme/src/integration/types.ts +++ b/src-theme/src/integration/types.ts @@ -15,6 +15,7 @@ export interface GroupedModules { export type ModuleSetting = BlocksSetting + | InventoryPresetValue | BooleanSetting | FloatSetting | FloatRangeSetting @@ -34,6 +35,47 @@ export type ModuleSetting = | VectorSetting | KeySetting; +export interface SingleItemPreference { + type: "SINGLE"; + item: string; +} +export interface GroupItemPreference { + type: "GROUP"; + group: "ARROWS" | "SWORD" | "WEAPON" | "AXE" | "HOE" | "SHOVEL" | "PICKAXE" | "FOOD" | "POTION" | "BLOCK" | "THROWABLE"; +} + +export interface IgnoreItemPreference { + type: "IGNORE"; +} + +export interface AnyPresetItem { + type: "ANY"; +} + +export type PresetItem = + SingleItemPreference + | GroupItemPreference + | IgnoreItemPreference + | AnyPresetItem; + +export interface MaxStacksGroup { + itemCount: number; + items: PresetItem[]; +} + +export type PresetItemGroup = PresetItem[]; + +export interface InventoryPreset { + items: PresetItemGroup[]; + maxStacks: MaxStacksGroup[]; +} + +export interface InventoryPresetValue { + name: string; + valueType: string; + value: InventoryPreset; +} + export interface BlocksSetting { valueType: string; name: string; diff --git a/src-theme/src/routes/clickgui/setting/common/GenericSetting.svelte b/src-theme/src/routes/clickgui/setting/common/GenericSetting.svelte index 95148485b46..d9f5623987d 100644 --- a/src-theme/src/routes/clickgui/setting/common/GenericSetting.svelte +++ b/src-theme/src/routes/clickgui/setting/common/GenericSetting.svelte @@ -19,6 +19,7 @@ import MutableListSetting from "../list/MutableListSetting.svelte"; import ItemListSetting from "../list/ItemListSetting.svelte"; import RegistryListSetting from "../list/RegistryListSetting.svelte"; + import InventoryPresetValue from "../inventoryPreset/InventoryPresetValue.svelte"; export let setting: ModuleSetting; export let path: string; @@ -28,6 +29,8 @@
{#if setting.valueType === "BOOLEAN"} + {:else if setting.valueType === "INVENTORY_PRESET"} + {:else if setting.valueType === "CHOICE"} {:else if setting.valueType === "CHOOSE"} diff --git a/src-theme/src/routes/clickgui/setting/common/ValueInput.svelte b/src-theme/src/routes/clickgui/setting/common/ValueInput.svelte index 75f757de2a8..22d3bdf4fbd 100644 --- a/src-theme/src/routes/clickgui/setting/common/ValueInput.svelte +++ b/src-theme/src/routes/clickgui/setting/common/ValueInput.svelte @@ -53,4 +53,4 @@ min-width: 5px; display: inline-block; } - \ No newline at end of file + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/InventoryPresetValue.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/InventoryPresetValue.svelte new file mode 100644 index 00000000000..f3735381657 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/InventoryPresetValue.svelte @@ -0,0 +1,79 @@ + + + + +
+
+ {$spaceSeperatedNames ? "Inventory Preset" : "InventoryPreset"} +
+ +
+
configuring = true}> + {#each preset.items as group, idx (idx)} + + {/each} +
+
+
+ +{#if configuring} + configuring = false} + on:change={handleChange} + /> +{/if} + + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/ItemImage.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/ItemImage.svelte new file mode 100644 index 00000000000..7c8eaea84e6 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/ItemImage.svelte @@ -0,0 +1,57 @@ + + +{#if item.type === "SINGLE"} + {item.item}/ +{:else if item.type === "GROUP"} + {item.group} +{:else if item.type === "IGNORE"} + Tools +{:else if item.type === "ANY"} + Any +{:else} + Any +{/if} + + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/ItemPreview.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/ItemPreview.svelte new file mode 100644 index 00000000000..1495e1ccc92 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/ItemPreview.svelte @@ -0,0 +1,42 @@ + + +
+ {#if rendered.length > 0} +
+ +
+ {/if} +
+ + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/ItemGroupSelector.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/ItemGroupSelector.svelte new file mode 100644 index 00000000000..ed7e245a903 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/ItemGroupSelector.svelte @@ -0,0 +1,186 @@ + + + + + +
+
+
+
expanded = !expanded}> + Add +
+ + {#if expanded} +
expanded = false} + use:portal + > + !choiceItems.includes(it)} + /> +
+ {/if} +
+ + {#each items as item, idx} +
removeItem(idx)}> +
+ +
+
+ {/each} +
+
+ + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/PresetModal.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/PresetModal.svelte new file mode 100644 index 00000000000..4b58aa36890 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/PresetModal.svelte @@ -0,0 +1,172 @@ + + + + + + + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/PresetTooltip.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/PresetTooltip.svelte new file mode 100644 index 00000000000..dc6523c3992 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/PresetTooltip.svelte @@ -0,0 +1,137 @@ + + + hovered = true} + onmouseleave={() => hovered = false} +> + + {#if hovered} + + {text} + + {/if} + + + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/item.scss b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/item.scss new file mode 100644 index 00000000000..36e2d41181b --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/item.scss @@ -0,0 +1,11 @@ +@use "sass:color"; +@use "../../../../../colors.scss" as *; + +.item-background { + background-color: rgba($clickgui-base-color, 0.85); + outline: 1px solid color.adjust($clickgui-text-color, $lightness: -85%); + + &:hover { + outline: 1px solid color.adjust($clickgui-text-color, $lightness: -70%); + } +} diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/maxStacks/MaxStacksContainer.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/maxStacks/MaxStacksContainer.svelte new file mode 100644 index 00000000000..1eea9a0a824 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/maxStacks/MaxStacksContainer.svelte @@ -0,0 +1,57 @@ + + + + +{#if groups.length > 0} +
+
+ {#each groups as group, idx} + handleDelete(idx)} + /> + {/each} +
+
+{/if} + + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/maxStacks/StackContainer.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/maxStacks/StackContainer.svelte new file mode 100644 index 00000000000..60f46cdc8d2 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/maxStacks/StackContainer.svelte @@ -0,0 +1,158 @@ + + + + + +
+
+ + +
+
+ Limit +
+ {#if group.itemCount < 9 * 4 * 64} + 64 x + updateItemCount(e.detail.value, nItems)}/> + + + updateItemCount(nStacks, e.detail.value)}/> + {:else} + + {/if} +
+
+
+
+
+
+ exit +
+
+ + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupComponent.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupComponent.svelte new file mode 100644 index 00000000000..c09e096545b --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupComponent.svelte @@ -0,0 +1,126 @@ + + + + +
+
expanded = !expanded} + > +
+ +
+
+ + {#if expanded} +
+ + +
+ {idx === 0 ? "Offhand" : idx} +
+
+ {/if} +
+ + + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupPreferenceSelector.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupPreferenceSelector.svelte new file mode 100644 index 00000000000..200a43e6c70 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupPreferenceSelector.svelte @@ -0,0 +1,141 @@ + + +
+
+ Preference +
+ +
+ + + + + +
+ + {#if showingItems} + + + {/if} +
+ + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupPreview.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupPreview.svelte new file mode 100644 index 00000000000..f2fe177d62f --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupPreview.svelte @@ -0,0 +1,48 @@ + + +
+ {#if group.length === 0} +
+ +
+ {:else} + {#each group.slice(0, 3) as item} +
+ +
+ {/each} + + {#if group.length > 3} +
+{Math.min(9, group.length - 3)}
+ {/if} + {/if} +
+ + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/Items.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/Items.svelte new file mode 100644 index 00000000000..769847ecd7b --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/Items.svelte @@ -0,0 +1,92 @@ + + + + +
+ {#each items as group, idx (idx)} +
handleDragStart(e, idx)} + on:dragover={(e) => handleDragOver(e, idx)} + on:dragend={handleDragEnd} + > + +
+ + {#if idx === 0} +
+ {/if} + {/each} +
+ + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/PresetItemSelector.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/PresetItemSelector.svelte new file mode 100644 index 00000000000..390ebcfe4cd --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/PresetItemSelector.svelte @@ -0,0 +1,305 @@ + + + + +
+
+ {searchQuery === "" ? "Select Items" : "Search"} +
+ + {#if searchQuery === ""} +
+ Quick Select +
+ {#each commonItems as commonItem} +
setItemProxy(commonItem)}> +
+ +
+
+ {/each} +
+
+ {/if} +
+ Specific Items + + +
+ + {#if searchQuery === ""} +
+ Item Groups +
+ +
setItemProxy(item.item)}> +
+
+ +
+
+ {$spaceSeperatedNames ? convertToSpacedString(item.name) : item.name} + {#if item.tooltip != null} + + {/if} +
+
+
+
+ {:else} + {#if renderedItems.length > 0} +
+ +
setItemProxy({type: "SINGLE", item: item.identifier})}> +
+ {item.identifier}/ +
+ + + {item.name} + +
+
+
+ {:else} + No Results + {/if} + {/if} +
+ + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/select.scss b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/select.scss new file mode 100644 index 00000000000..0dfefc64f97 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/select.scss @@ -0,0 +1,121 @@ +@use "sass:color"; +@use "../../../../../colors.scss" as *; + +.selector-container-wrapper { + z-index: 9999; + position: absolute; + width: 250px; + max-height: 450px; + min-height: 0; + padding: 20px; + background-color: rgba($clickgui-base-color, 0.95); + outline: 1px solid color.adjust($clickgui-text-color, $lightness: -85%); + border-radius: 3px; +} + + +.select-title { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 500; + font-size: 16px; + color: $clickgui-text-color; +} + +.select-selector { + display: flex; + flex-direction: column; + gap: 20px; + height: 100%; +} + +.icon-wrapper { + transition: background-color 0.3s ease; + background-color: rgba($clickgui-base-color, 0.85); + border: 1px solid color.adjust($clickgui-text-color, $lightness: -85%); + border-radius: 3px; + min-width: 30px; + min-height: 30px; + display: flex; + align-items: center; + justify-content: center; +} + +.result-item { + display: flex; + height: 40px; + align-items: center; + gap: 10px; + position: relative; + cursor: pointer; + + &:hover { + .name { + color: $clickgui-text-color; + } + + .icon-wrapper { + background-color: color.adjust($clickgui-text-color, $lightness: -80%); + } + } +} + +.icon { + width: 20px; + height: 20px; +} + +.name { + transition: color 0.3s ease; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: $clickgui-text-dimmed-color; +} + +.search { + position: relative; +} + +.search-input { + width: 100%; + height: 35px; + border-radius: 3px; + border: none; + background: transparent; + padding-left: 35px; + outline: solid 1px color.adjust($clickgui-text-color, $lightness: -90%); + color: white; +} + +.search-icon { + position: absolute; + left: 0; + top: 0; + width: 35px; + height: 35px; + display: flex; + align-items: center; + justify-content: center; + + & > img { + width: 20px; + height: 20px; + } +} + +.results { + height: 100%; + overflow-y: auto; + overflow-x: hidden; +} + +.items-group-title { + font-size: 12px; + color: rgba($clickgui-text-dimmed-color, 0.6); + font-weight: 600; + margin-left: 5px; + text-transform: uppercase; +} diff --git a/src-theme/src/util/utils.ts b/src-theme/src/util/utils.ts new file mode 100644 index 00000000000..526271114d8 --- /dev/null +++ b/src-theme/src/util/utils.ts @@ -0,0 +1,32 @@ +export function portal(node: HTMLElement, target: HTMLElement = document.body) { + target.appendChild(node); + return { + destroy() { + if (node.parentNode) node.parentNode.removeChild(node); + } + }; +} + +export function clickOutside(node: HTMLElement, callback: (event: MouseEvent) => void) { + const handleClick = (event: MouseEvent) => { + if (!node.contains(event.target as Node)) { + callback(event); + } + }; + + const handleDrag = (event: DragEvent) => { + if (!node.contains(event.target as Node)) { + callback(event); + } + }; + + document.addEventListener('click', handleClick, true); + document.addEventListener('dragstart', handleDrag, true); + + return { + destroy() { + document.removeEventListener('click', handleClick, true); + document.removeEventListener('dragstart', handleDrag, true); + } + }; +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/GsonInstance.kt b/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/GsonInstance.kt index 92be4fba61a..dc3071cd48a 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/GsonInstance.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/GsonInstance.kt @@ -30,6 +30,8 @@ import net.ccbluex.liquidbounce.config.gson.serializer.minecraft.* import net.ccbluex.liquidbounce.config.gson.stategies.ExcludeStrategy import net.ccbluex.liquidbounce.config.gson.stategies.ProtocolExcludeStrategy import net.ccbluex.liquidbounce.config.types.NamedChoice +import net.ccbluex.liquidbounce.features.inventoryPreset.InventoryPreset +import net.ccbluex.liquidbounce.features.inventoryPreset.FrontendSlotPreference import net.ccbluex.liquidbounce.config.types.nesting.ChoiceConfigurable import net.ccbluex.liquidbounce.config.types.nesting.Configurable import net.ccbluex.liquidbounce.integration.theme.component.Component @@ -147,6 +149,8 @@ internal fun GsonBuilder.registerCommonTypeAdapters() = .registerTypeHierarchyAdapter(ClosedRange::class.javaObjectType, RangeAdapter) .registerTypeHierarchyAdapter(IntRange::class.javaObjectType, IntRangeAdapter) .registerTypeHierarchyAdapter(Item::class.javaObjectType, ItemAdapter) + .registerTypeHierarchyAdapter(InventoryPreset::class.java, InventoryPresetAdapter) + .registerTypeHierarchyAdapter(FrontendSlotPreference::class.java, FrontendSlotPreferenceAdapter) .registerTypeHierarchyAdapter(SoundEvent::class.javaObjectType, SoundEventAdapter) .registerTypeHierarchyAdapter(StatusEffect::class.javaObjectType, StatusEffectAdapter) .registerTypeHierarchyAdapter(Color4b::class.javaObjectType, ColorAdapter) diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/adapter/FrontendSlotPreferenceAdapter.kt b/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/adapter/FrontendSlotPreferenceAdapter.kt new file mode 100644 index 00000000000..2dd047eec94 --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/adapter/FrontendSlotPreferenceAdapter.kt @@ -0,0 +1,46 @@ +@file:Suppress("WildcardImport") + +package net.ccbluex.liquidbounce.config.gson.adapter + +import com.google.gson.* +import net.ccbluex.liquidbounce.features.inventoryPreset.FrontendSlotPreference +import net.ccbluex.liquidbounce.features.inventoryPreset.FrontendSlotPreference.GroupSlotPreference.ItemGroupType +import net.minecraft.item.Item +import java.lang.reflect.Type + +object FrontendSlotPreferenceAdapter : + JsonSerializer, JsonDeserializer { + override fun serialize( + src: FrontendSlotPreference, + typeOfSrc: Type?, + context: JsonSerializationContext + ): JsonObject { + return src.serialize(context) + } + + override fun deserialize( + json: JsonElement, + typeOfT: Type?, + context: JsonDeserializationContext + ): FrontendSlotPreference { + val obj = json.asJsonObject + + return when (obj["type"].asString) { + "SINGLE" -> FrontendSlotPreference.SingleSlotPreference( + context.deserialize( + obj["item"], + Item::class.java + ) + ) + "GROUP" -> FrontendSlotPreference.GroupSlotPreference( + context.deserialize( + obj["group"], + ItemGroupType::class.java + ) + ) + "IGNORE" -> FrontendSlotPreference.IgnoreSlotPreference + "ANY" -> FrontendSlotPreference.AnySlotPreference + else -> error("Unknown slot preference ${obj["type"]}") + } + } +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/adapter/InventoryPresetAdapter.kt b/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/adapter/InventoryPresetAdapter.kt new file mode 100644 index 00000000000..cfea2304665 --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/adapter/InventoryPresetAdapter.kt @@ -0,0 +1,35 @@ +package net.ccbluex.liquidbounce.config.gson.adapter + +import com.google.gson.* +import com.google.gson.reflect.TypeToken +import net.ccbluex.liquidbounce.features.inventoryPreset.InventoryPreset +import net.ccbluex.liquidbounce.features.inventoryPreset.FrontendSlotPreference +import net.ccbluex.liquidbounce.features.inventoryPreset.FrontendItemLimitRules +import net.ccbluex.liquidbounce.utils.kotlin.mapArray +import java.lang.reflect.Type + +object InventoryPresetAdapter : JsonSerializer, JsonDeserializer { + override fun serialize( + src: InventoryPreset, + typeOfSrc: Type, + context: JsonSerializationContext + ): JsonElement = JsonObject().apply { + add("items", context.serialize(src.itemRulesToArray())) + add("maxStacks", context.serialize(src.itemLimitRules)) + } + + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): InventoryPreset = with (json.asJsonObject) { + val items = getAsJsonArray("items").map { context.decode>(it) } + val throws = getAsJsonArray("maxStacks").map { context.decode(it) } + + return InventoryPreset(items.toTypedArray(), throws) + } + + private inline fun JsonDeserializationContext.decode(element: JsonElement): T { + return deserialize(element, object: TypeToken() {}.type) + } +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/config/types/Value.kt b/src/main/kotlin/net/ccbluex/liquidbounce/config/types/Value.kt index f42309ea9c2..63f29687b62 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/config/types/Value.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/config/types/Value.kt @@ -29,6 +29,8 @@ import net.ccbluex.liquidbounce.config.types.nesting.ChoiceConfigurable import net.ccbluex.liquidbounce.config.util.AutoCompletionProvider import net.ccbluex.liquidbounce.event.EventManager import net.ccbluex.liquidbounce.event.events.ValueChangedEvent +import net.ccbluex.liquidbounce.features.inventoryPreset.InventoryPreset +import net.ccbluex.liquidbounce.features.misc.FriendManager import net.ccbluex.liquidbounce.lang.translation import net.ccbluex.liquidbounce.script.ScriptApiRequired import net.ccbluex.liquidbounce.script.asArray @@ -312,6 +314,11 @@ open class Value( } +class InventoryPresetValue : Value("InventoryPreset", + defaultValue = InventoryPreset(), + valueType = ValueType.INVENTORY_PRESET, +) + /** * Order by name of [Value] (ignoreCase) */ @@ -428,6 +435,7 @@ enum class ValueType( SERVER_PACKET, KEY(HumanInputDeserializer.keyDeserializer), BIND, + INVENTORY_PRESET, VECTOR_I, VECTOR_D, diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/config/types/nesting/Configurable.kt b/src/main/kotlin/net/ccbluex/liquidbounce/config/types/nesting/Configurable.kt index 8e2c15a2369..b2fad645ef7 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/config/types/nesting/Configurable.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/config/types/nesting/Configurable.kt @@ -20,6 +20,7 @@ package net.ccbluex.liquidbounce.config.types.nesting import net.ccbluex.liquidbounce.config.types.* import net.ccbluex.liquidbounce.event.EventListener +import net.ccbluex.liquidbounce.features.module.ClientModule import net.ccbluex.liquidbounce.render.engine.type.Color4b import net.ccbluex.liquidbounce.utils.client.toLowerCamelCase import net.ccbluex.liquidbounce.utils.input.InputBind @@ -305,6 +306,21 @@ open class Configurable( fun > serverPackets(name: String, default: C) = registryList(name, default, ValueType.SERVER_PACKET) + fun inventoryPreset() = InventoryPresetValue().apply { + require(this@Configurable is ClientModule) { + "Requires that it only be in a module, " + + "it can't be a child of anything else because the design might go " + + "wrong (maybe this will be resolved in a future implementation, " + + "but for now it is like this)" + } + + require(this@Configurable.inner.find { it is InventoryPresetValue } == null) { + "It can only be one for, it is not possible to specify it twice yet." + } + + this@Configurable.inner.add(this) + } + inline fun multiEnumChoice( name: String, vararg default: T, diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/FrontendItemLimitRules.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/FrontendItemLimitRules.kt new file mode 100644 index 00000000000..cfc0415195f --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/FrontendItemLimitRules.kt @@ -0,0 +1,15 @@ +package net.ccbluex.liquidbounce.features.inventoryPreset + +/** + * Represents a group restriction limiting the maximum total stacks for specific items. + */ +class FrontendItemLimitRules( + val itemCount: Int, + val items: Set = emptySet() +) { + init { + require(items.find { it == FrontendSlotPreference.IgnoreSlotPreference } == null) { + "An item in limits cannot be ignored." + } + } +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/FrontendSlotPreference.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/FrontendSlotPreference.kt new file mode 100644 index 00000000000..0f5b02521e8 --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/FrontendSlotPreference.kt @@ -0,0 +1,144 @@ +package net.ccbluex.liquidbounce.features.inventoryPreset + +import com.google.gson.JsonObject +import com.google.gson.JsonSerializationContext +import com.google.gson.annotations.SerializedName +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.CleanupPlanTemplate +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.CleanupPlanTemplate.CleanupPlanRestrictions.RestrictionType +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.GenericItemType +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items.MiningToolItemFacet +import net.minecraft.item.Item +import net.minecraft.item.Items + +/** + * Contains the frontend representation of the user defined preference of what should a slot contain. + */ +sealed class FrontendSlotPreference { + /** + * Converts the frontend representation of the user + * configured preset into a version + * which the [net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.CleanupPlanGenerator] understands. + */ + abstract fun toBackendRepresentation(): ConvertedSlotPreference + abstract fun serialize(context: JsonSerializationContext): JsonObject + + class SingleSlotPreference(private val item: Item) : FrontendSlotPreference() { + companion object { + /** + * Some items like bow or crossbow represent an item type with additional sorting logic. + * Those items must be remapped. + */ + val itemSpecialTypeMap = mapOf( + Items.BOW to CleanupPlanTemplate.SlotContentPreference(GenericItemType.BOW), + Items.CROSSBOW to CleanupPlanTemplate.SlotContentPreference(GenericItemType.CROSSBOW), + ) + } + + override fun toBackendRepresentation(): ConvertedSlotPreference { + val specialType = itemSpecialTypeMap[item] + + if (specialType != null) { + return ConvertedSlotPreference(specialType) + } + + val contentPreference = CleanupPlanTemplate.SlotContentPreference( + itemType = GenericItemType.ANY_ITEM, + subtypes = setOf(item) + ) + + return ConvertedSlotPreference(contentPreference) + } + + override fun serialize(context: JsonSerializationContext) = JsonObject().apply { + addProperty("type", "SINGLE") + + add("item", context.serialize(item)) + } + } + + class GroupSlotPreference(private val itemGroupType: ItemGroupType) : FrontendSlotPreference() { + override fun toBackendRepresentation(): ConvertedSlotPreference { + return ConvertedSlotPreference(itemGroupType.preference) + } + + /** + * Enum representing item categories used for preset item classification. + */ + @Suppress("UNUSED") + enum class ItemGroupType(val preference: CleanupPlanTemplate.SlotContentPreference) { + @SerializedName("ARROWS") + ARROWS(CleanupPlanTemplate.SlotContentPreference(GenericItemType.ARROW)), + @SerializedName("SWORD") + SWORD(CleanupPlanTemplate.SlotContentPreference(GenericItemType.SWORD)), + @SerializedName("WEAPON") + WEAPON(CleanupPlanTemplate.SlotContentPreference(GenericItemType.WEAPON)), + @SerializedName("AXE") + AXE_TOOL( + CleanupPlanTemplate.SlotContentPreference( + GenericItemType.TOOL, + setOf(MiningToolItemFacet.ItemToolType.AXE) + ) + ), + @SerializedName("HOE") + HOE_TOOL( + CleanupPlanTemplate.SlotContentPreference( + GenericItemType.TOOL, + setOf(MiningToolItemFacet.ItemToolType.HOE) + ) + ), + @SerializedName("SHOVEL") + SHOVEL_TOOL( + CleanupPlanTemplate.SlotContentPreference( + GenericItemType.TOOL, + setOf(MiningToolItemFacet.ItemToolType.SHOVEL) + ) + ), + @SerializedName("PICKAXE") + PICKAXE_TOOL( + CleanupPlanTemplate.SlotContentPreference( + GenericItemType.TOOL, + setOf(MiningToolItemFacet.ItemToolType.PICKAXE) + ) + ), + @SerializedName("FOOD") + FOOD(CleanupPlanTemplate.SlotContentPreference(GenericItemType.FOOD)), + @SerializedName("POTION") + POTION(CleanupPlanTemplate.SlotContentPreference(GenericItemType.POTION)), + @SerializedName("BLOCK") + BLOCK(CleanupPlanTemplate.SlotContentPreference(GenericItemType.BLOCK)), + @SerializedName("THROWABLE") + THROWABLE(CleanupPlanTemplate.SlotContentPreference(GenericItemType.THROWABLE)) + } + + override fun serialize(context: JsonSerializationContext) = JsonObject().apply { + addProperty("type", "GROUP") + + add("group", context.serialize(itemGroupType)) + } + } + + data object IgnoreSlotPreference : FrontendSlotPreference() { + override fun toBackendRepresentation(): ConvertedSlotPreference { + return ConvertedSlotPreference(null, RestrictionType.FORBID_TAMPERING) + } + + override fun serialize(context: JsonSerializationContext) = JsonObject().apply { + addProperty("type", "IGNORE") + } + } + + data object AnySlotPreference : FrontendSlotPreference() { + override fun toBackendRepresentation(): ConvertedSlotPreference { + return ConvertedSlotPreference(null, RestrictionType.NONE) + } + + override fun serialize(context: JsonSerializationContext) = JsonObject().apply { + addProperty("type", "ANY") + } + } + + data class ConvertedSlotPreference( + val contentPreference: CleanupPlanTemplate.SlotContentPreference?, + val slotRestriction: RestrictionType = RestrictionType.NONE + ) +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/InventoryPreset.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/InventoryPreset.kt new file mode 100644 index 00000000000..87f3f03cde2 --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/InventoryPreset.kt @@ -0,0 +1,71 @@ +@file:Suppress("WildcardImport") + +package net.ccbluex.liquidbounce.features.inventoryPreset + +import net.ccbluex.liquidbounce.utils.inventory.HotbarItemSlot +import net.ccbluex.liquidbounce.utils.inventory.OffHandSlot + +/** + * Represents an inventory preset configuration defining item groups for specific slots and stack limitations. + * + * This preset maintains a strict relationship between array indices and inventory slots: + * - The [items] array is guaranteed to contain exactly 10 elements. + * - Index 0 always represents the [OffHandSlot] + * - Indices 1-9 correspond to hotbar slots 0-8 respectively (index -1 adjustment) + * + * @property itemLimitRules Array of stack limitation groups applying to the entire inventory + * @param items Initial item group configuration (must contain exactly 10 elements). + * Each array position maps to: + * - [OffHandSlot] for index 0 + * - [HotbarItemSlot] (0-8) for indices 1-9 + * + * @throws IllegalArgumentException if item array size isn't exactly 10 during initialization + */ +@Suppress("MagicNumber") +class InventoryPreset( + items: Array> = Array(10) { listOf() }, + val itemLimitRules: List = emptyList() +) { + val items: Map> + + init { + // Required because the frontend would break if there weren't exactly 10 entries... + require(items.size == 10) + + require(items.flatMap { it }.find { it == FrontendSlotPreference.AnySlotPreference } == null) { + "For an item to be Any, the list must be empty." + } + + items.forEach { preferences -> + val ignoreCount = preferences.count { it == FrontendSlotPreference.IgnoreSlotPreference } + require(ignoreCount == 0 || (ignoreCount == 1 && preferences.size == 1)) { + "If you use IgnoreSlotPreference, it must be the ONLY element in the list" + } + } + + val itemMap = items + .mapIndexed { index, item -> getSlotForIndex(index) to item } + .associate { it } + + this.items = itemMap + } + + private fun getSlotForIndex(idx: Int): HotbarItemSlot { + return when (idx) { + 0 -> OffHandSlot + else -> HotbarItemSlot(idx - 1) + } + } + + fun itemRulesToArray(): Array> { + return Array(10) { + val preferences = items[getSlotForIndex(it)] + + if (preferences.isNullOrEmpty()) { + return@Array listOf() + } + + preferences + } + } +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/misc/ModuleElytraSwap.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/misc/ModuleElytraSwap.kt index b8a9d7d944e..627664e990f 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/misc/ModuleElytraSwap.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/misc/ModuleElytraSwap.kt @@ -88,7 +88,7 @@ object ModuleElytraSwap : ClientModule( schedule(constraints, actions) } - private fun Item.isChestplate() = this is ArmorItem && type() == EquipmentType.CHESTPLATE + private fun Item.isChestplate() = this is ArmorItem && this.type() == EquipmentType.CHESTPLATE private fun ItemStack.isElytra() = this.item == Items.ELYTRA diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/cheststealer/ModuleChestStealer.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/cheststealer/ModuleChestStealer.kt index acf1813b473..aac1a4db018 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/cheststealer/ModuleChestStealer.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/cheststealer/ModuleChestStealer.kt @@ -28,6 +28,8 @@ import net.ccbluex.liquidbounce.features.module.ClientModule import net.ccbluex.liquidbounce.features.module.modules.player.cheststealer.features.FeatureChestAura import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.* import net.ccbluex.liquidbounce.utils.inventory.* +import net.ccbluex.liquidbounce.utils.inventory.ItemSlot.ItemSlotType +import net.ccbluex.liquidbounce.utils.item.* import net.minecraft.client.gui.screen.ingame.GenericContainerScreen import net.minecraft.text.Text import kotlin.math.ceil @@ -212,7 +214,7 @@ object ModuleChestStealer : ClientModule("ChestStealer", Category.PLAYER) { } else { val availableItems = findNonEmptySlotsInInventory() + findItemsInContainer(screen) - CleanupPlanGenerator(ModuleInventoryCleaner.cleanupTemplateFromSettings, availableItems).generatePlan() + CleanupPlanGenerator(ModuleInventoryCleaner.cleanupTemplateFromSettings, availableItems).plan } return cleanupPlan diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/CleanupPlanGenerator.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/CleanupPlanGenerator.kt index 25a85ab2c26..69c8d476085 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/CleanupPlanGenerator.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/CleanupPlanGenerator.kt @@ -18,81 +18,120 @@ */ package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.CleanupPlanTemplate.CleanupPlanRestrictions.RestrictionType +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemNumberConstraintEnforcer.SatisfactionStatus import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items.ItemFacet import net.ccbluex.liquidbounce.utils.inventory.ItemSlot import net.ccbluex.liquidbounce.utils.item.isNothing -class CleanupPlanGenerator( - private val template: CleanupPlanPlacementTemplate, - private val availableItems: List, -) : ItemPacker.ItemAmountContraintProvider { - private val hotbarSwaps: ArrayList = ArrayList() +class CleanupPlanGenerator(private val template: CleanupPlanTemplate, private val availableItems: List) { + private val wishOrganizer = WishOrganizer(this.template) + private val constraintEnforcer = ItemNumberConstraintEnforcer(template) - private val packer = ItemPacker() + val plan: InventoryCleanupPlan - private val currentLimit = HashMap() + init { + val allItemFacets = discoverItemFacets() + // All slots the cleaner may swap into other slots + val availableItemFacets = allItemFacets.filter { + this.template.restrictions.getRestrictionFor(it.itemSlot) < RestrictionType.FORBID_REPLACING + } - // TODO Implement greedy check - /** - * Keeps track of where a specific type of item should be placed. e.g. BLOCK -> [Hotbar 7, Hotbar 8] - */ - private val categoryToSlotsMap: Map> = - template.slotContentMap.entries - .filter { (_, itemType) -> itemType.category != null } - .groupBy { (_, itemType) -> itemType.category!! } - .mapValues { (_, entries) -> entries.map { (slot, _) -> slot } } + val itemDispenserRack = ItemDispenserRack(this.wishOrganizer, availableItemFacets) - fun generatePlan(): InventoryCleanupPlan { - val categorizer = ItemCategorization(availableItems) + val usefulItems = HashSet() - // Contains all facets that the available items represent. i.e. if we have an axe in slot 5, this would be - // (Axe(Slot 5), Weapon(Slot 5)) since the axe can also function as a weapon. - val itemFacets = availableItems.flatMap { categorizer.getItemFacets(it).asIterable() } + // Consider all slots that may not be touched at all as useful. + usefulItems.addAll(template.restrictions.getSlotsWithAtLeast(RestrictionType.FORBID_TAMPERING)) - // i.e. BLOCK -> [Block(Slot 5), Block(Slot 6)] - // Keep priority in mind (Tool slots are processed before weapon slots) - val facetsGroupedByType = - itemFacets - .groupBy { it.category } - .entries - .sortedByDescending { it.key.type.allocationPriority } + val swaps = generateSwaps(itemDispenserRack, usefulItems) - for ((category, availableItems) in facetsGroupedByType) { - processItemCategory(category, availableItems) - } - - // We aren't allowed to touch those, so we just consider them as useful. - packer.usefulItems.addAll(this.template.forbiddenSlots) + findOtherUsefulItems(usefulItems, allItemFacets) - return InventoryCleanupPlan( - usefulItems = packer.usefulItems, - swaps = hotbarSwaps, + this.plan = InventoryCleanupPlan( + usefulItems = usefulItems, + swaps = swaps, mergeableItems = groupItemsByType(), ) } - private fun processItemCategory( - category: ItemCategory, - availableItems: List, - ) { - val hotbarSlotsToFill = this.categoryToSlotsMap[category] + /** + * This function marks all useful items that aren't filled into hotbar slots (i.e., arrows) as useful. + */ + private fun findOtherUsefulItems(usefulItems: HashSet, allItemFacets: List) { + val facetsGroupedByCategory = allItemFacets + .groupBy { it.category } + .entries + .sortedBy { this.template.itemAmountConstraintProvider.getAllocationPriority(it.key) } + + for ((_, facetsInCategory) in facetsGroupedByCategory) { + for (facet in facetsInCategory.sortedDescending()) { + val satisfactionStatus = this.constraintEnforcer.getSatisfactionStatus(facet) + + when (satisfactionStatus) { + SatisfactionStatus.NOT_SATISFIED -> { + this.constraintEnforcer.addItem(facet) + + usefulItems.add(facet.itemSlot) + } + SatisfactionStatus.SATISFIED -> {} + SatisfactionStatus.OVERSATURATED -> { + throw IllegalArgumentException("Oversaturated behavior is currently not implemented.") + } + } + } + } + } + + private fun generateSwaps( + itemDispenserRack: ItemDispenserRack, + usefulItems: HashSet + ): ArrayList { + val finishedSlots = HashSet() + + // Consider all slots that we aren't allowed to change as done. + finishedSlots.addAll(template.restrictions.getSlotsWithAtLeast(RestrictionType.FORBID_REPLACING)) - // We need to fill all hotbar slots with this item type. + val swaps: ArrayList = ArrayList() - // Use a descending sort order so that we can fill the slots with the best items first. - val prioritizedItemList = availableItems.sortedDescending() + for (wish in this.wishOrganizer.organizedWishes) { + // If a better wish was already fulfilled, skip this second wish. + if (wish.targetSlot in finishedSlots) { + continue + } + + val availableItem = itemDispenserRack.nextItemForGroup(wish.id) + + if (availableItem == null) { + continue + } + + finishedSlots.add(wish.targetSlot) + usefulItems.add(availableItem.itemSlot) + + // Move the item to the target slot if necessary. + if (availableItem.itemSlot != wish.targetSlot) { + swaps.add( + InventorySwap( + from = availableItem.itemSlot, + to = wish.targetSlot, + priority = availableItem.category.type.allocationPriority + ) + ) + } + } + return swaps + } + + /** + * Discovers all facets from [availableItems]. Filters out any slot that has been restricted + */ + private fun discoverItemFacets(): List { + val categorizer = ItemCategorization(availableItems) - // Decide where the items should go. - val requiredMoves = - this.packer.packItems( - itemsToFillIn = prioritizedItemList, - hotbarSlotsToFill = hotbarSlotsToFill, - contraintProvider = this, - forbiddenSlots = this.template.forbiddenSlots, - forbiddenSlotsToFill = this.template.forbiddenSlotsToFill - ) + val availableItemFacets = availableItems.flatMap { categorizer.getItemFacets(it).asIterable() } - this.hotbarSwaps.addAll(requiredMoves) + return availableItemFacets } private fun groupItemsByType(): HashMap> { @@ -116,62 +155,4 @@ class CleanupPlanGenerator( return itemsByType } - - override fun getSatisfactionStatus(item: ItemFacet): ItemPacker.ItemAmountContraintProvider.SatisfactionStatus { - val constraints = this.template.itemAmountConstraintProvider(item) - - constraints.sortBy { it.group.priority } - - for (constraintInfo in constraints) { - val currentCount = this.currentLimit[constraintInfo.group] ?: 0 - - if (currentCount > constraintInfo.group.acceptableRange.last) { - return ItemPacker.ItemAmountContraintProvider.SatisfactionStatus.OVERSATURATED - } else if (currentCount < constraintInfo.group.acceptableRange.first) { - return ItemPacker.ItemAmountContraintProvider.SatisfactionStatus.NOT_SATISFIED - } - } - - return ItemPacker.ItemAmountContraintProvider.SatisfactionStatus.SATISFIED - } - - override fun addItem(item: ItemFacet) { - val constraints = this.template.itemAmountConstraintProvider(item) - - for (constraintInfo in constraints) { - val current = this.currentLimit.getOrDefault(constraintInfo.group, 0) - - this.currentLimit[constraintInfo.group] = current + constraintInfo.amountAddedByItem - } - } -} - -class CleanupPlanPlacementTemplate( - /** - * Contains requests for each slot (e.g. Slot 1 -> SWORD, Slot 8 -> BLOCK, etc.) - */ - val slotContentMap: Map, - /** - * A function which provides constraint groups for each item category and the number which the item counts against - * the given constraint. More info on how constraints work at [ItemNumberContraintGroup]. - */ - val itemAmountConstraintProvider: (ItemFacet) -> ArrayList, - /** - * If false, slots which also contains items of that category, those items are not replaced with other items. - */ - val isGreedy: Boolean, - val forbiddenSlots: Set, - val forbiddenSlotsToFill: Set -) - -enum class ItemSlotType { - HOTBAR, - OFFHAND, - ARMOR, - INVENTORY, - - /** - * e.g. chests - */ - CONTAINER, } diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/CleanupPlanTemplate.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/CleanupPlanTemplate.kt new file mode 100644 index 00000000000..308f3795cf3 --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/CleanupPlanTemplate.kt @@ -0,0 +1,85 @@ +package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner + +import net.ccbluex.liquidbounce.utils.inventory.ItemSlot + +class CleanupPlanTemplate( + /** + * Contains requests for each slot (e.g. Slot 1 -> SWORD, Slot 8 -> BLOCK, etc.) + */ + val slotContentMap: Map, + /** + * A function which provides constraint groups for each item category and the number which the item counts against + * the given constraint. More info on how constraints work at [ItemNumberContraintGroup]. + */ + val itemAmountConstraintProvider: ItemAmountConstraintProvider, + /** + * See [CleanupPlanRestrictions] + */ + val restrictions: CleanupPlanRestrictions, +) { + + class CleanupPlanSlotContent( + /** + * Content wishes for the target slot. + * + * ## Example: + * - Configuration for the slot: `[(sword), (snowball, egg), (apple)]` + * - Available items: `[1x sword, 3x snowball, 16x egg, 64x apple]` + * + * Behaviour: + * 1. The slot would be filled in + * 2. If the sword wasn't available, the next wish is considered. + * So it searches for the best snowball or egg in the list. + * Since 16 eggs are better than 3 snowballs, it will prefer those. + * 3. If the eggs weren't available or the snowballs were more, it would fill the slot with the snowball stack. + * 4. If no eggs and snowballs are available either, the apples would be filled in. + */ + val slotContentPreferences: List, + val priority: Int, + ) + + data class SlotContentPreference( + val itemType: GenericItemType, + val subtypes: Set = setOf(Unit), + ) + + /** + * Contains all information about what the inv cleaner is *not allowed* to do. + */ + class CleanupPlanRestrictions( + private val slotRestrictionMap: Map, + ) { + + fun getRestrictionFor(slot: ItemSlot): RestrictionType { + return this.slotRestrictionMap.getOrDefault(slot, RestrictionType.NONE) + } + + fun getSlotsWithAtLeast(type: RestrictionType): List { + return this.slotRestrictionMap.entries + .filter { it.value >= type } + .map { it.key } + } + + enum class RestrictionType { + NONE, + + /** + * Forbids the inventory cleaner from replacing the item in that slot with another item according to + * the current template. + * The inventory cleaner may still decide that the current content of the slot is useless and throw it out. + * + * Used for preventing the replacement of items that + * [net.ccbluex.liquidbounce.features.module.modules.player.offhand.ModuleOffhand] placed in the offhand. + */ + FORBID_REPLACING, + + /** + * Prevents the invcleaner from touching those slots at all. + * + * This used to be user-configurable for specific hotbar slots. + * Currently, this is used to prevent inv cleaner from tampering with the armor slots. + */ + FORBID_TAMPERING + } + } +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemAmountConstraintProvider.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemAmountConstraintProvider.kt new file mode 100644 index 00000000000..41f170ea1ca --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemAmountConstraintProvider.kt @@ -0,0 +1,32 @@ +package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner + +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items.ItemFacet + +interface ItemAmountConstraintProvider { + fun getConstraints(item: ItemFacet): ArrayList + + /** + * Returns the priority of the given item category. + * Categories with values are processed first. + * + * This is useful when it comes to finding the minimal number of items required to fulfill the constraints. + * For example, if the constraints were `egg -> 64, egg, snowball -> 32`, it would be important to process the eggs + * first so that no snowballs are kept when having > 32 eggs. + */ + fun getAllocationPriority(itemGroup: ItemCategory): Int + + /** + * Filters out not applying default configurations. + * + * See [ItemConstraintInfo.default] for further information on that. + */ + fun getApplyingConstraints(item: ItemFacet): ArrayList { + val constraints = getConstraints(item) + + if (constraints.any { !it.default }) { + constraints.removeIf { it.default } + } + + return constraints + } +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemCategorization.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemCategorization.kt index 030529dd496..605003b15ab 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemCategorization.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemCategorization.kt @@ -18,32 +18,35 @@ */ package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner -import net.ccbluex.liquidbounce.config.types.NamedChoice import net.ccbluex.liquidbounce.features.module.modules.combat.autoarmor.ArmorEvaluation import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items.* import net.ccbluex.liquidbounce.features.module.modules.world.scaffold.ScaffoldBlockItemSelection import net.ccbluex.liquidbounce.utils.inventory.ItemSlot +import net.ccbluex.liquidbounce.utils.inventory.ItemSlot.ItemSlotType import net.ccbluex.liquidbounce.utils.inventory.VirtualItemSlot import net.ccbluex.liquidbounce.utils.item.* import net.ccbluex.liquidbounce.utils.kotlin.Priority import net.ccbluex.liquidbounce.utils.sorting.compareByCondition import net.minecraft.entity.EquipmentSlot -import net.minecraft.fluid.LavaFluid -import net.minecraft.fluid.WaterFluid import net.minecraft.item.* -import java.util.function.Predicate val PREFER_ITEMS_IN_HOTBAR: Comparator = compareByCondition(ItemFacet::isInHotbar) val STABILIZE_COMPARISON: Comparator = Comparator.comparingInt { it.itemStack.hashCode() } + val PREFER_BETTER_DURABILITY: Comparator = Comparator.comparingInt { it.itemStack.maxDamage - it.itemStack.damage } -data class ItemCategory(val type: ItemType, val subtype: Int) +val DEFAULT_TIE_BREAK: Array> = arrayOf( + PREFER_ITEMS_IN_HOTBAR, + STABILIZE_COMPARISON, +) + +data class ItemCategory(val type: GenericItemType, val subtype: Any = Unit) -enum class ItemType( +enum class GenericItemType( val oneIsSufficient: Boolean, /** * Higher priority means the item category is filled in first. @@ -55,76 +58,35 @@ enum class ItemType( * ## Used values * - Specialization (see above): 10 per level */ - val allocationPriority: Priority = Priority.NORMAL, - /** - * The user maybe wants to filter the items by a specific type. But the we don't need all versions of the item. - * To stop the invcleaner from keeping items of every type, we can specify what function a specific item serves. - * If that function is already served, we can just ignore it. - */ - val providedFunction: ItemFunction? = null + val allocationPriority: Priority = Priority.NORMAL ) { ARMOR(true, allocationPriority = Priority.IMPORTANT_FOR_PLAYER_LIFE), - SWORD(true, allocationPriority = Priority.IMPORTANT_FOR_USAGE_3, providedFunction = ItemFunction.WEAPON_LIKE), - WEAPON(true, allocationPriority = Priority.IMPORTANT_FOR_USAGE_2, providedFunction = ItemFunction.WEAPON_LIKE), + SWORD(true, allocationPriority = Priority.IMPORTANT_FOR_USAGE_3), + WEAPON(true, allocationPriority = Priority.IMPORTANT_FOR_USAGE_2), BOW(true), CROSSBOW(true), ARROW(true), TOOL(true, allocationPriority = Priority.IMPORTANT_FOR_USAGE_1), - ROD(true), THROWABLE(false), - SHIELD(true), FOOD(false), - BUCKET(false), - PEARL(false, allocationPriority = Priority.IMPORTANT_FOR_USAGE_1), - GAPPLE(false, allocationPriority = Priority.IMPORTANT_FOR_USAGE_1), POTION(false), BLOCK(false), - NONE(false), + /** + * Represents any item. Every item in the inventory has this type. + */ + ANY_ITEM(true), } enum class ItemFunction { WEAPON_LIKE, - FOOD, -} -enum class ItemSortChoice( - override val choiceName: String, - val category: ItemCategory?, /** - * This is the function that is used for the greedy check. - * - * IF IT WAS IMPLEMENTED + * Crossbows and bows. */ - val satisfactionCheck: Predicate? = null, -) : NamedChoice { - SWORD("Sword", ItemCategory(ItemType.SWORD, 0)), - WEAPON("Weapon", ItemCategory(ItemType.WEAPON, 0)), - BOW("Bow", ItemCategory(ItemType.BOW, 0)), - CROSSBOW("Crossbow", ItemCategory(ItemType.CROSSBOW, 0)), - AXE("Axe", ItemCategory(ItemType.TOOL, 0)), - PICKAXE("Pickaxe", ItemCategory(ItemType.TOOL, 1)), - ROD("Rod", ItemCategory(ItemType.ROD, 0)), - SHIELD("Shield", ItemCategory(ItemType.SHIELD, 0)), - WATER("Water", ItemCategory(ItemType.BUCKET, 0)), - LAVA("Lava", ItemCategory(ItemType.BUCKET, 1)), - MILK("Milk", ItemCategory(ItemType.BUCKET, 2)), - PEARL("Pearl", ItemCategory(ItemType.PEARL, 0), { it.item == Items.ENDER_PEARL }), - GAPPLE( - "Gapple", - ItemCategory(ItemType.GAPPLE, 0), - { it.item == Items.GOLDEN_APPLE || it.item == Items.ENCHANTED_GOLDEN_APPLE }, - ), - FOOD("Food", ItemCategory(ItemType.FOOD, 0), { it.foodComponent != null }), - POTION("Potion", ItemCategory(ItemType.POTION, 0)), - BLOCK("Block", ItemCategory(ItemType.BLOCK, 0), { it.item is BlockItem }), - THROWABLES("Throwables", ItemCategory(ItemType.THROWABLE, 0)), - IGNORE("Ignore", null), - NONE("None", null), + BOW_LIKE, + FOOD, } -/** - * @param expectedFullArmor what is the expected armor material when we have full armor (full iron, full dia, etc.) - */ class ItemCategorization( availableItems: List, ) { @@ -147,9 +109,9 @@ class ItemCategorization( } /** - * Sometimes there are situations where armor pieces are not the best ones with the current armor, but become + * Sometimes there are situations where armor pieces aren’t the best ones with the current armor, but become * the best ones as soon as we upgrade one of the other armor pieces. - * In those cases we don't want to miss out on this armor piece in the future thus we keep it. + * In those cases, we don't want to miss out on this armor piece in the future, thus we keep it. */ private val futureArmorToKeep: List private val armorComparator: ArmorComparator @@ -180,32 +142,24 @@ class ItemCategorization( return emptyArray() } - val specificItemFacets: Array = when (val item = slot.itemStack.item) { + val item = slot.itemStack.item + + val specificItemFacets: Array = when (item) { // Treat animal armor as a normal item - is AnimalArmorItem -> arrayOf(ItemFacet(slot)) is ArmorItem -> arrayOf(ArmorItemFacet(slot, this.futureArmorToKeep, this.armorComparator)) is SwordItem -> arrayOf(SwordItemFacet(slot)) is BowItem -> arrayOf(BowItemFacet(slot)) is CrossbowItem -> arrayOf(CrossbowItemFacet(slot)) - is ArrowItem -> arrayOf(ArrowItemFacet(slot)) + is ArrowItem -> arrayOf(PrimitiveItemFacet(slot, ItemCategory(GenericItemType.ARROW))) is MiningToolItem -> arrayOf(MiningToolItemFacet(slot)) - is FishingRodItem -> arrayOf(RodItemFacet(slot)) - is ShieldItem -> arrayOf(ShieldItemFacet(slot)) is BlockItem -> { - if (ScaffoldBlockItemSelection.isValidBlock(slot.itemStack) - && !ScaffoldBlockItemSelection.isBlockUnfavourable(slot.itemStack) - ) { + val isUsableBlock = (ScaffoldBlockItemSelection.isValidBlock(slot.itemStack) + && !ScaffoldBlockItemSelection.isBlockUnfavourable(slot.itemStack)) + + if (isUsableBlock) { arrayOf(BlockItemFacet(slot)) } else { - arrayOf(ItemFacet(slot)) - } - } - Items.MILK_BUCKET -> arrayOf(PrimitiveItemFacet(slot, ItemCategory(ItemType.BUCKET, 2))) - is BucketItem -> { - when (item.fluid) { - is WaterFluid -> arrayOf(PrimitiveItemFacet(slot, ItemCategory(ItemType.BUCKET, 0))) - is LavaFluid -> arrayOf(PrimitiveItemFacet(slot, ItemCategory(ItemType.BUCKET, 1))) - else -> arrayOf(PrimitiveItemFacet(slot, ItemCategory(ItemType.BUCKET, 3))) + emptyArray() } } is PotionItem -> { @@ -216,33 +170,25 @@ class ItemCategorization( if (areAllEffectsGood) { arrayOf(PotionItemFacet(slot)) } else { - arrayOf(ItemFacet(slot)) + emptyArray() } } - is EnderPearlItem -> arrayOf(PrimitiveItemFacet(slot, ItemCategory(ItemType.PEARL, 0))) - Items.GOLDEN_APPLE -> { - arrayOf( - FoodItemFacet(slot), - PrimitiveItemFacet(slot, ItemCategory(ItemType.GAPPLE, 0)), - ) - } - Items.ENCHANTED_GOLDEN_APPLE -> { - arrayOf( - FoodItemFacet(slot), - PrimitiveItemFacet(slot, ItemCategory(ItemType.GAPPLE, 0), 1), - ) - } Items.SNOWBALL, Items.EGG, Items.WIND_CHARGE -> arrayOf(ThrowableItemFacet(slot)) else -> { if (slot.itemStack.isFood) { arrayOf(FoodItemFacet(slot)) } else { - arrayOf(ItemFacet(slot)) + emptyArray() } } } - // Everything could be a weapon (i.e. a stick with Knochback II should be considered a weapon) - return specificItemFacets + WeaponItemFacet(slot) + val commonFacets = listOfNotNull( + PrimitiveItemFacet(slot, ItemCategory(GenericItemType.ANY_ITEM, item)), + // Everything could be a weapon (i.e. a stick with Knockback II should be preferred over a stick) + WeaponItemFacet.createIfUsefulAsWeapon(slot) + ) + + return specificItemFacets + commonFacets } } diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemDispenserRack.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemDispenserRack.kt new file mode 100644 index 00000000000..5fb80aabac9 --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemDispenserRack.kt @@ -0,0 +1,45 @@ +package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner + +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items.ItemFacet +import net.ccbluex.liquidbounce.utils.inventory.ItemSlot + +class ItemDispenserRack(wishOrganizer: WishOrganizer, itemFacets: List) { + private val dispensersForType: Map + private val alreadyDispensedItemSlots = HashSet() + + init { + val wishGroupAvailableFacetMap = HashMap>() + + for (facet in itemFacets) { + val wishGroupsForFacet = wishOrganizer.itemCategoryWishGroupMap[facet.category] ?: continue + + for (id in wishGroupsForFacet) { + wishGroupAvailableFacetMap.computeIfAbsent(id) { ArrayList() }.add(facet) + } + } + + wishGroupAvailableFacetMap.values.forEach { facetList -> facetList.sortDescending() } + + this.dispensersForType = wishGroupAvailableFacetMap.mapValues { ItemDispenser(it.value) } + } + + fun nextItemForGroup(id: WishOrganizer.WishItemGroupId) = this.dispensersForType[id]?.nextItem() + + private inner class ItemDispenser(itemList: List) { + private val itemListIterable: Iterator = itemList.iterator() + + fun nextItem(): ItemFacet? { + while (this.itemListIterable.hasNext()) { + val currentItem = this.itemListIterable.next() + + // Check if this item slot has already been dispensed. + // This is possible as an item might appear in multiple dispensers. + if (alreadyDispensedItemSlots.add(currentItem.itemSlot)) { + return currentItem + } + } + + return null + } + } +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemMerge.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemMerge.kt index d1d34a91bcb..55509ace086 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemMerge.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemMerge.kt @@ -23,7 +23,7 @@ import kotlin.math.ceil object ItemMerge { /** - * Find all item stack ids which should be double-clicked in order to merge them + * Find all item stack ids that should be double-clicked to merge them */ internal fun findStacksToMerge(cleanupPlan: InventoryCleanupPlan): List { val itemsToMerge = mutableListOf() diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemNumberConstraintEnforcer.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemNumberConstraintEnforcer.kt new file mode 100644 index 00000000000..9b424d723ba --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemNumberConstraintEnforcer.kt @@ -0,0 +1,64 @@ +package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner + +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items.ItemFacet + +/** + * This class serves two functions: + * - Keeps track of the current state of the fulfilment of the item number limits. + * - Decides whether an item is useful or not. + */ +class ItemNumberConstraintEnforcer(private val template: CleanupPlanTemplate) { + private val currentLimit = HashMap() + + /** + * Decides whether the given item facet is useful. + * The decision is made based on the items that have been added via [addItem] + */ + fun getSatisfactionStatus(item: ItemFacet): SatisfactionStatus { + val constraints = this.template.itemAmountConstraintProvider.getApplyingConstraints(item) + + constraints.sortBy { it.group.priority } + + for (constraintInfo in constraints) { + val currentCount = this.currentLimit[constraintInfo.group] ?: 0 + + if (currentCount > constraintInfo.group.acceptableRange.last) { + return SatisfactionStatus.OVERSATURATED + } else if (currentCount < constraintInfo.group.acceptableRange.first) { + return SatisfactionStatus.NOT_SATISFIED + } + } + + return SatisfactionStatus.SATISFIED + } + + /** + * Called when an item is kept in the inventory. + */ + fun addItem(item: ItemFacet) { + val constraints = this.template.itemAmountConstraintProvider.getApplyingConstraints(item) + + for (constraintInfo in constraints) { + val current = this.currentLimit.getOrDefault(constraintInfo.group, 0) + + this.currentLimit[constraintInfo.group] = current + constraintInfo.amountAddedByItem + } + } + + enum class SatisfactionStatus { + /** + * Keep the item + */ + NOT_SATISFIED, + + /** + * The item is not needed - except for filling slots. + */ + SATISFIED, + + /** + * The item shouldn't be kept - even if there are still slots to fill. + */ + OVERSATURATED, + } +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemNumberConstraints.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemNumberConstraints.kt index 3b6ea210796..7191f93595f 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemNumberConstraints.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemNumberConstraints.kt @@ -1,5 +1,7 @@ package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner +import java.util.Objects + /** * Defines an item constraint group. * @@ -21,7 +23,7 @@ abstract class ItemNumberContraintGroup( val acceptableRange: IntRange, /** * The priority of this constraint group. Lower values are processed first. - * It Affects the order in which items are processed. + * It affects the order in which items are processed. */ val priority: Int, ) { @@ -44,7 +46,35 @@ class ItemCategoryConstraintGroup( } override fun hashCode(): Int { - return category.hashCode() + return Objects.hash(this.javaClass, this.category) + } +} + +/** + * Used for implementing number constraints for a group of multiple specific items. + * For example: `[snowball, egg] -> >=32 (group id: 0) or [apple, steak, egg] >= 64 (group id: 1)`. + * + * Each of those categories will get a [groupId] which identifies the group. + * This allows a fast lookup of constraints for a specific item. + * In this example, + * the egg would be tagged with group numbers `0` and `1` while the steak would only be in group number `1`. + */ +class SpecificItemGroupConstraintGroup( + acceptableRange: IntRange, + priority: Int, + val groupId: Int +): ItemNumberContraintGroup(acceptableRange, priority) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SpecificItemGroupConstraintGroup + + return groupId == other.groupId + } + + override fun hashCode(): Int { + return Objects.hash(this.javaClass, this.groupId) } } @@ -63,11 +93,23 @@ class ItemFunctionCategoryConstraintGroup( } override fun hashCode(): Int { - return function.hashCode() + return Objects.hash(this.javaClass, this.function) } } class ItemConstraintInfo( val group: ItemNumberContraintGroup, - val amountAddedByItem: Int + val amountAddedByItem: Int, + /** + * Specifies whether this constraint is a default option. + * Constraints with this option can be considered fallback constraints which are only used in absence of any other + * configuration. + * + * For example, if the user did not configure anything, there might be a configuration like: + * `eggs -> 32 (default)`. + * This would make the inventory cleaner keep two stacks of eggs by default. + * As soon as the user adds their own configuration like `eggs -> 0 (non-default), eggs -> 32 (default)`, + * the default values are discarded. + */ + val default: Boolean, ) diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemPacker.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemPacker.kt deleted file mode 100644 index 109d208d175..00000000000 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemPacker.kt +++ /dev/null @@ -1,164 +0,0 @@ -package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner - -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemPacker.ItemAmountContraintProvider.SatisfactionStatus.OVERSATURATED -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemPacker.ItemAmountContraintProvider.SatisfactionStatus.SATISFIED -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items.ItemFacet -import net.ccbluex.liquidbounce.utils.inventory.ItemSlot -import net.minecraft.item.ItemStack - -/** - * After discovery phase (find all items, group them by their type, sort them by usefulness), this class tries to fit - * the given requirements (max blocks, required stack cound, etc.) and packs the given items in their target slots. - * - * Items that were deemed useful can be found in [usefulItems]. - */ -class ItemPacker { - /** - * Items that have already been used. For example if already we used Inventory slot 12 as a sword, we cannot reuse - * it as an axe in slot 2. - */ - private val alreadyAllocatedItems: HashSet = HashSet() - - /** - * If an item is used by a move, it will be in this list. - */ - val usefulItems = HashSet() - - /** - * Takes items from the [itemsToFillIn] list until it collected [maxItemCount] items is and [requiredStackCount] - * stacks. The items are marked as useful and fills in hotbar slots if there are still slots to fill. - * - * @return returns the item moves ("swaps") that should to be executed. - */ - fun packItems( - itemsToFillIn: List, - hotbarSlotsToFill: List?, - forbiddenSlots: Set, - forbiddenSlotsToFill: Set, - contraintProvider: ItemAmountContraintProvider - ): List { - val moves = ArrayList() - - val requriedStackCount = hotbarSlotsToFill?.size ?: 0 - - var currentStackCount = 0 - var currentItemCount = 0 - - // The iterator of hotbar slots that still need filling. - val leftHotbarSlotIterator = hotbarSlotsToFill?.iterator() - - for (filledInItem in itemsToFillIn) { - val constraintsSatisfied = contraintProvider.getSatisfactionStatus(filledInItem) - val allStacksFilled = currentStackCount >= requriedStackCount - - if (allStacksFilled && constraintsSatisfied == SATISFIED || constraintsSatisfied == OVERSATURATED) { - continue - } - - val filledInItemSlot = filledInItem.itemSlot - - // The item is already allocated and marked as useful, so we cannot use it again. - if (filledInItemSlot in alreadyAllocatedItems) { - continue - } - - usefulItems.add(filledInItemSlot) - - contraintProvider.addItem(filledInItem) - - currentItemCount += filledInItem.itemStack.count - currentStackCount++ - - // Don't fill in the item if (a) there is no place for it to go or (b) we aren't allowed to touch it. - if (leftHotbarSlotIterator == null || filledInItemSlot in forbiddenSlots) { - continue - } - - // Now find a fitting slot for the item. - val targetSlot = fillItemIntoSlot(filledInItemSlot, leftHotbarSlotIterator) - - if (targetSlot != null && targetSlot !in forbiddenSlotsToFill) { - moves.add(InventorySwap(filledInItemSlot, targetSlot, filledInItem.category.type.allocationPriority)) - } - } - - // Keep items that should be kept - itemsToFillIn.filter(ItemFacet::shouldKeep).forEach { this.usefulItems.add(it.itemSlot) } - - return moves - } - - /** - * Packs the given item into a good slot in the given target slots. - * - * @return the target slot that this item should be moved to, if a move should occur. - */ - private fun fillItemIntoSlot( - filledInItemSlot: ItemSlot, - leftTargetSlotsToFill: Iterator, - ): ItemSlot? { - while (leftTargetSlotsToFill.hasNext()) { - // Get the slots that still need to be filled if there are any (left/at all). - - val hotbarSlotToFill = leftTargetSlotsToFill.next() - - // We don't need to move around equivalent items - val areStacksSame = - ItemStack.areEqual( - filledInItemSlot.itemStack, - hotbarSlotToFill.itemStack, - ) - - when { - // The item is already in the potential target slot, don't change anything about it. - filledInItemSlot == hotbarSlotToFill -> { - // We mark the slot as used to prevent it being used for another slot. - alreadyAllocatedItems.add(hotbarSlotToFill) - - return null - } - - areStacksSame -> { - // We mark the slot as used to prevent it being used for another slot. - alreadyAllocatedItems.add(hotbarSlotToFill) - - // Find a new slot for the item - continue - } - // A move should occur - else -> { - // We will a swap. Both items have changed and should not be touched. - alreadyAllocatedItems.add(filledInItemSlot) - alreadyAllocatedItems.add(hotbarSlotToFill) - - return hotbarSlotToFill - } - } - } - - // We found no target slot - return null - } - - interface ItemAmountContraintProvider { - fun getSatisfactionStatus(item: ItemFacet): SatisfactionStatus - fun addItem(item: ItemFacet) - - enum class SatisfactionStatus { - /** - * Keep the item - */ - NOT_SATISFIED, - - /** - * The item is not needed - except for filling slots. - */ - SATISFIED, - - /** - * The item shouldn't be kept - even if there are still slots to fill. - */ - OVERSATURATED, - } - } -} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ModuleInventoryCleaner.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ModuleInventoryCleaner.kt index ff1ac82dd90..f98cb9cbc85 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ModuleInventoryCleaner.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ModuleInventoryCleaner.kt @@ -22,100 +22,92 @@ import net.ccbluex.liquidbounce.event.events.ScheduleInventoryActionEvent import net.ccbluex.liquidbounce.event.handler import net.ccbluex.liquidbounce.features.module.Category import net.ccbluex.liquidbounce.features.module.ClientModule +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.CleanupPlanTemplate.CleanupPlanRestrictions +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.CleanupPlanTemplate.CleanupPlanRestrictions.RestrictionType +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.CleanupPlanTemplate.CleanupPlanSlotContent import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items.ItemFacet import net.ccbluex.liquidbounce.features.module.modules.player.offhand.ModuleOffhand import net.ccbluex.liquidbounce.utils.inventory.* import net.ccbluex.liquidbounce.utils.kotlin.Priority -import net.ccbluex.liquidbounce.utils.kotlin.component1 -import net.ccbluex.liquidbounce.utils.kotlin.component2 import net.minecraft.screen.slot.SlotActionType /** - * InventoryCleaner module + * InventoryManager module * * Automatically throws away useless items and sorts them. */ -object ModuleInventoryCleaner : ClientModule("InventoryCleaner", Category.PLAYER, +object ModuleInventoryCleaner : ClientModule( + name = "InventoryCleaner", + category = Category.PLAYER, aliases = arrayOf("InventoryManager") ) { - + private val inventoryConstraints = tree(PlayerInventoryConstraints()) - private val maxBlocks by int("MaximumBlocks", 512, 0..2500) - private val maxArrows by int("MaximumArrows", 128, 0..2500) - private val maxThrowables by int("MaximumThrowables", 64, 0..600) - private val maxFoods by int("MaximumFoodPoints", 200, 0..2000) - - private val isGreedy by boolean("Greedy", true) - - private val offHandItem by enumChoice("OffHandItem", ItemSortChoice.SHIELD) - private val slotItem1 by enumChoice("SlotItem-1", ItemSortChoice.WEAPON) - private val slotItem2 by enumChoice("SlotItem-2", ItemSortChoice.BOW) - private val slotItem3 by enumChoice("SlotItem-3", ItemSortChoice.PICKAXE) - private val slotItem4 by enumChoice("SlotItem-4", ItemSortChoice.AXE) - private val slotItem5 by enumChoice("SlotItem-5", ItemSortChoice.NONE) - private val slotItem6 by enumChoice("SlotItem-6", ItemSortChoice.POTION) - private val slotItem7 by enumChoice("SlotItem-7", ItemSortChoice.FOOD) - private val slotItem8 by enumChoice("SlotItem-8", ItemSortChoice.BLOCK) - private val slotItem9 by enumChoice("SlotItem-9", ItemSortChoice.BLOCK) - - val cleanupTemplateFromSettings: CleanupPlanPlacementTemplate + @Suppress("unused") + private val inventoryPresets by inventoryPreset() + + val cleanupTemplateFromSettings: CleanupPlanTemplate get() { - val slotTargets = hashMapOf( - Pair(OffHandSlot, offHandItem), - Pair(Slots.Hotbar[0], slotItem1), - Pair(Slots.Hotbar[1], slotItem2), - Pair(Slots.Hotbar[2], slotItem3), - Pair(Slots.Hotbar[3], slotItem4), - Pair(Slots.Hotbar[4], slotItem5), - Pair(Slots.Hotbar[5], slotItem6), - Pair(Slots.Hotbar[6], slotItem7), - Pair(Slots.Hotbar[7], slotItem8), - Pair(Slots.Hotbar[8], slotItem9), - ) + val specifiedSlotTargets = this.inventoryPresets.items + val currentRestrictionMap = hashMapOf() + + val mapped = specifiedSlotTargets + .map { (slot, choice) -> + val wishes = choice.mapNotNull { + val representation = it.toBackendRepresentation() + + currentRestrictionMap.compute(slot) { _, b -> + maxOf(b ?: RestrictionType.NONE, representation.slotRestriction) + } + + representation.contentPreference + } + + slot to CleanupPlanSlotContent(wishes, 0) + } + .toTypedArray() + + val slotTargets = hashMapOf(pairs = mapped) - val forbiddenSlots = slotTargets - .filterValues { it == ItemSortChoice.IGNORE } - .keys.toHashSet() // Disallow tampering with armor slots since auto armor already handles them - forbiddenSlots += Slots.Armor + Slots.Armor.forEach { currentRestrictionMap[it] = RestrictionType.FORBID_TAMPERING } if (ModuleOffhand.isOperating()) { // Disallow tampering with off-hand slot when AutoTotem is active - forbiddenSlots.add(OffHandSlot) + currentRestrictionMap[OffHandSlot] = RestrictionType.FORBID_REPLACING } - val forbiddenSlotsToFill = setOfNotNull( - // Disallow tampering with off-hand slot when AutoTotem is active - if (ModuleOffhand.isOperating()) OffHandSlot else null - ) + val desiredItemCounts = this.inventoryPresets.itemLimitRules.map { rule -> + val converted = rule.items + .mapNotNull { item -> item.toBackendRepresentation().contentPreference } + .flatMap { preference -> + preference.subtypes.map { ItemCategory(preference.itemType, it) } + } - val constraintProvider = AmountConstraintProvider( - desiredItemsPerCategory = hashMapOf( - Pair(ItemSortChoice.BLOCK.category!!, maxBlocks), - Pair(ItemSortChoice.THROWABLES.category!!, maxThrowables), - Pair(ItemCategory(ItemType.ARROW, 0), maxArrows), - ), - desiredValuePerFunction = hashMapOf( - Pair(ItemFunction.FOOD, maxFoods), - Pair(ItemFunction.WEAPON_LIKE, 1), - ) + converted to rule.itemCount + } + + val constraintProvider = AmountItemAmountConstraintProvider( + desiredValuePerFunction = hashMapOf(), + desiredItemsInSpecificCategories = desiredItemCounts ) - return CleanupPlanPlacementTemplate( + + return CleanupPlanTemplate( slotTargets, - itemAmountConstraintProvider = constraintProvider::getConstraints, - forbiddenSlots = forbiddenSlots, - forbiddenSlotsToFill = forbiddenSlotsToFill, - isGreedy = isGreedy, + itemAmountConstraintProvider = constraintProvider, + restrictions = CleanupPlanRestrictions(currentRestrictionMap) ) } @Suppress("unused") private val handleInventorySchedule = handler { event -> - val cleanupPlan = CleanupPlanGenerator(cleanupTemplateFromSettings, findNonEmptySlotsInInventory()) - .generatePlan() + val cleanupPlan = CleanupPlanGenerator( + cleanupTemplateFromSettings, + findNonEmptySlotsInInventory() + ).plan // Step 1: Move items to the correct slots for (hotbarSwap in cleanupPlan.swaps) { @@ -163,44 +155,96 @@ object ModuleInventoryCleaner : ClientModule("InventoryCleaner", Category.PLAYER itemsInInv: List, ) = itemsInInv.filter { it !in cleanupPlan.usefulItems } - private class AmountConstraintProvider( - val desiredItemsPerCategory: Map, + private class AmountItemAmountConstraintProvider( val desiredValuePerFunction: Map, - ) { - fun getConstraints(facet: ItemFacet): ArrayList { + /** + * Contains information about specific item groups constraints like `[snowball, egg] -> 32`. + * In that example, the inventory cleaner would not start throwing out items until at least 32 items of + * snowballs or eggs are in the inventory. + */ + desiredItemsInSpecificCategories: List, Int>> + ) : ItemAmountConstraintProvider { + /** + * Contains all specific item groups in which an item is. + * + * For these rules: `[egg, snowball] -> 32, [egg, carrot] -> 64`, this list would look like this: + * - `egg` -> `[0, 1]` + * - `snowball` -> `[0]` + * - `carrot` -> `[1]` + */ + private val itemSpecificGroupMap: Map> = run { + desiredItemsInSpecificCategories + .flatMapIndexed { idx, (items, desiredAmount) -> + val group = SpecificItemGroup(id = idx, desiredAmount = desiredAmount, priority = idx) + + items.map { it to group } + } + .groupBy { it.first } + .mapValues { list -> list.value.map { it.second } } + } + + override fun getConstraints(facet: ItemFacet): ArrayList { val constraints = ArrayList() - if (facet.providedItemFunctions.isEmpty()) { + for (group in this.itemSpecificGroupMap.getOrDefault(facet.category, emptyList())) { + val info = ItemConstraintInfo( + group = SpecificItemGroupConstraintGroup( + acceptableRange = group.desiredAmount..Integer.MAX_VALUE, + priority = group.priority, + groupId = group.id + ), + amountAddedByItem = facet.itemStack.count, + default = false + ) + + constraints.add(info) + } + + for ((function, amountAdded) in facet.providedItemFunctions) { + val configuredDesiredAmount = desiredValuePerFunction[function] + + val (default, desiredAmount) = if (configuredDesiredAmount != null) { + false to configuredDesiredAmount + } else { + true to 1 + } + + val info = ItemConstraintInfo( + group = ItemFunctionCategoryConstraintGroup( + desiredAmount..Integer.MAX_VALUE, + 1000, + function + ), + amountAddedByItem = amountAdded, + default = default + ) + + constraints.add(info) + } + + if (facet.providedItemFunctions.isEmpty() && facet.category.type != GenericItemType.ANY_ITEM) { val defaultDesiredAmount = if (facet.category.type.oneIsSufficient) 1 else Integer.MAX_VALUE - val desiredAmount = this.desiredItemsPerCategory[facet.category] ?: defaultDesiredAmount val info = ItemConstraintInfo( group = ItemCategoryConstraintGroup( - desiredAmount..Integer.MAX_VALUE, - 10, + defaultDesiredAmount..Integer.MAX_VALUE, + 1000, facet.category ), - amountAddedByItem = facet.itemStack.count + amountAddedByItem = facet.itemStack.count, + default = true ) constraints.add(info) - } else { - for ((function, amountAdded) in facet.providedItemFunctions) { - val info = ItemConstraintInfo( - group = ItemFunctionCategoryConstraintGroup( - desiredValuePerFunction.getOrDefault(function, 1)..Integer.MAX_VALUE, - 10, - function - ), - amountAddedByItem = amountAdded - ) - - constraints.add(info) - } } return constraints } - } + override fun getAllocationPriority(itemGroup: ItemCategory): Int { + return -(this.itemSpecificGroupMap[itemGroup]?.maxBy { it.priority }?.priority ?: 0) + } + + private class SpecificItemGroup(val id: Int, val desiredAmount: Int, val priority: Int) + } } diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/WishOrganizer.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/WishOrganizer.kt new file mode 100644 index 00000000000..30ed817a4c5 --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/WishOrganizer.kt @@ -0,0 +1,69 @@ +package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner + +import net.ccbluex.liquidbounce.utils.inventory.ItemSlot +import net.ccbluex.liquidbounce.utils.sorting.ComparatorChain + +class WishOrganizer(template: CleanupPlanTemplate) { + val organizedWishes = ArrayList() + val itemCategoryWishGroupMap = HashMap>() + + companion object { + /** + * Decides which whish should come first. If wishA > wishB, wishA should be fulfilled first. + */ + val wishComparator = ComparatorChain( + compareByDescending { it.slotPriority }, + compareByDescending { it.indexInSlot }, + // Fill in specific items first. + // The user expects this behavior. + // For example, if there is a slot for golden apples and a slot for food, the user expects the + // golden apple slot to contain golden apples and not the food slot. + compareBy { it.wish.itemType == GenericItemType.ANY_ITEM }, + compareBy { it.wish.itemType.allocationPriority }, + ) + } + + init { + // Deduplicate wishes for performance reasons. + val wishIdMap = HashMap() + + for ((slot, content) in template.slotContentMap.entries) { + content.slotContentPreferences.forEachIndexed { wishIndexInSlot, wish -> + val id = wishIdMap.computeIfAbsent(wish) { WishItemGroupId() } + + organizedWishes.add( + OrganizedWish( + id = id, + slotPriority = content.priority, + indexInSlot = wishIndexInSlot, + targetSlot = slot, + wish = wish + ) + ) + } + } + + // Sort the wishes so that the wishes, which should be fulfilled first, are first. + organizedWishes.sortWith(wishComparator.reversed()) + + wishIdMap.forEach { (wish, itemGroupId) -> + for (subtype in wish.subtypes) { + val itemCategory = ItemCategory(wish.itemType, subtype) + + val wishItemGroups = itemCategoryWishGroupMap.computeIfAbsent(itemCategory) { ArrayList() } + + wishItemGroups.add(itemGroupId) + } + } + } + + data class OrganizedWish( + val id: WishItemGroupId, + val targetSlot: ItemSlot, + val slotPriority: Int, + val indexInSlot: Int, + val wish: CleanupPlanTemplate.SlotContentPreference + ) + + class WishItemGroupId +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ArmorItemFacet.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ArmorItemFacet.kt index 7b534682723..47a77ee2588 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ArmorItemFacet.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ArmorItemFacet.kt @@ -20,7 +20,7 @@ package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemCategory import net.ccbluex.liquidbounce.utils.inventory.ItemSlot -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemType +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.GenericItemType import net.ccbluex.liquidbounce.utils.item.ArmorComparator import net.ccbluex.liquidbounce.utils.item.ArmorPiece @@ -35,7 +35,7 @@ class ArmorItemFacet( private val armorPiece = ArmorPiece(itemSlot) override val category: ItemCategory - get() = ItemCategory(ItemType.ARMOR, armorPiece.entitySlotId) + get() = ItemCategory(GenericItemType.ARMOR, armorPiece.entitySlotId) override fun shouldKeep(): Boolean { return this.stacksToKeep.contains(this.itemSlot) diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ArrowItemFacet.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ArrowItemFacet.kt deleted file mode 100644 index c23905581be..00000000000 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ArrowItemFacet.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * This file is part of LiquidBounce (https://github.com/CCBlueX/LiquidBounce) - * - * Copyright (c) 2015 - 2025 CCBlueX - * - * LiquidBounce is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LiquidBounce is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LiquidBounce. If not, see . - */ -package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items - -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.* -import net.ccbluex.liquidbounce.utils.inventory.ItemSlot -import net.ccbluex.liquidbounce.utils.sorting.ComparatorChain - -class ArrowItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { - companion object { - private val COMPARATOR = - ComparatorChain( - compareBy { it.itemStack.count }, - PREFER_ITEMS_IN_HOTBAR, - STABILIZE_COMPARISON, - ) - } - - override val category: ItemCategory - get() = ItemCategory(ItemType.ARROW, 0) - - override fun compareTo(other: ItemFacet): Int { - return COMPARATOR.compare(this, other as ArrowItemFacet) - } -} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/BlockItemFacet.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/BlockItemFacet.kt index cbb00f6f326..f13059fd15c 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/BlockItemFacet.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/BlockItemFacet.kt @@ -34,7 +34,7 @@ class BlockItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { } override val category: ItemCategory - get() = ItemCategory(ItemType.BLOCK, 0) + get() = ItemCategory(GenericItemType.BLOCK) override fun compareTo(other: ItemFacet): Int { return COMPARATOR.compare(this, other as BlockItemFacet) diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/BowItemFacet.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/BowItemFacet.kt index 0d3882ee829..7fe2e4da9be 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/BowItemFacet.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/BowItemFacet.kt @@ -18,6 +18,7 @@ */ package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items +import it.unimi.dsi.fastutil.objects.ObjectIntPair import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.* import net.ccbluex.liquidbounce.utils.inventory.ItemSlot import net.ccbluex.liquidbounce.utils.item.EnchantmentValueEstimator @@ -30,11 +31,11 @@ class BowItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { EnchantmentValueEstimator( EnchantmentValueEstimator.WeightedEnchantment(Enchantments.POWER, 0.25f), EnchantmentValueEstimator.WeightedEnchantment(Enchantments.PUNCH, 0.33f), - EnchantmentValueEstimator.WeightedEnchantment(Enchantments.FLAME, 4.0f * 0.9f), + EnchantmentValueEstimator.WeightedEnchantment(Enchantments.FLAME, 1.25f * 0.9f), EnchantmentValueEstimator.WeightedEnchantment(Enchantments.INFINITY, 4.0f), EnchantmentValueEstimator.WeightedEnchantment(Enchantments.UNBREAKING, 0.1f), EnchantmentValueEstimator.WeightedEnchantment(Enchantments.VANISHING_CURSE, -0.1f), - EnchantmentValueEstimator.WeightedEnchantment(Enchantments.MENDING, -0.2f), + EnchantmentValueEstimator.WeightedEnchantment(Enchantments.MENDING, 0.2f), ) private val COMPARATOR = ComparatorChain( @@ -44,8 +45,11 @@ class BowItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { ) } + override val providedItemFunctions: List + get() = listOf(ProvidedFunction(ItemFunction.BOW_LIKE, 1)) + override val category: ItemCategory - get() = ItemCategory(ItemType.BOW, 0) + get() = ItemCategory(GenericItemType.BOW) override fun compareTo(other: ItemFacet): Int { return COMPARATOR.compare(this, other as BowItemFacet) diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/CrossbowItemFacet.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/CrossbowItemFacet.kt index fa977bdf85c..1f48aaf3ee5 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/CrossbowItemFacet.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/CrossbowItemFacet.kt @@ -18,7 +18,10 @@ */ package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.* +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.DEFAULT_TIE_BREAK +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.GenericItemType +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemCategory +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemFunction import net.ccbluex.liquidbounce.utils.inventory.ItemSlot import net.ccbluex.liquidbounce.utils.item.EnchantmentValueEstimator import net.ccbluex.liquidbounce.utils.sorting.ComparatorChain @@ -36,15 +39,18 @@ class CrossbowItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { EnchantmentValueEstimator.WeightedEnchantment(Enchantments.VANISHING_CURSE, -0.25f), ) private val COMPARATOR = + @Suppress("SpreadOperator") ComparatorChain( compareBy { VALUE_ESTIMATOR.estimateValue(it.itemStack) }, - PREFER_ITEMS_IN_HOTBAR, - STABILIZE_COMPARISON, + *DEFAULT_TIE_BREAK ) } + override val providedItemFunctions: List + get() = listOf(ProvidedFunction(ItemFunction.BOW_LIKE, 1)) + override val category: ItemCategory - get() = ItemCategory(ItemType.CROSSBOW, 0) + get() = ItemCategory(GenericItemType.CROSSBOW) override fun compareTo(other: ItemFacet): Int { return COMPARATOR.compare(this, other as CrossbowItemFacet) diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/FoodItemFacet.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/FoodItemFacet.kt index 2d89c1b1588..cf89576e274 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/FoodItemFacet.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/FoodItemFacet.kt @@ -18,24 +18,22 @@ */ package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items -import it.unimi.dsi.fastutil.objects.ObjectIntPair +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.DEFAULT_TIE_BREAK +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.GenericItemType import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemCategory import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemFunction import net.ccbluex.liquidbounce.utils.inventory.ItemSlot -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemType -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.PREFER_ITEMS_IN_HOTBAR -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.STABILIZE_COMPARISON import net.ccbluex.liquidbounce.utils.item.foodComponent import net.ccbluex.liquidbounce.utils.sorting.ComparatorChain -import net.ccbluex.liquidbounce.utils.sorting.compareByCondition import net.minecraft.item.Items class FoodItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { companion object { private val COMPARATOR = + @Suppress("SpreadOperator") ComparatorChain( - compareByCondition { it.itemStack.item == Items.ENCHANTED_GOLDEN_APPLE }, - compareByCondition { it.itemStack.item == Items.GOLDEN_APPLE }, + compareBy { it.itemStack.item == Items.ENCHANTED_GOLDEN_APPLE }, + compareBy { it.itemStack.item == Items.GOLDEN_APPLE }, // Nutriment compareBy { val foodComponent = it.itemStack.foodComponent!! @@ -45,16 +43,15 @@ class FoodItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { compareBy { it.itemStack.foodComponent!!.nutrition }, compareBy { it.itemStack.foodComponent!!.saturation }, compareBy { it.itemStack.count }, - PREFER_ITEMS_IN_HOTBAR, - STABILIZE_COMPARISON, + *DEFAULT_TIE_BREAK ) } - override val providedItemFunctions: List> - get() = listOf(ObjectIntPair.of(ItemFunction.FOOD, itemStack.count * itemStack.foodComponent!!.nutrition)) + override val providedItemFunctions: List + get() = listOf(ProvidedFunction(ItemFunction.FOOD, itemStack.count * itemStack.foodComponent!!.nutrition)) override val category: ItemCategory - get() = ItemCategory(ItemType.FOOD, 0) + get() = ItemCategory(GenericItemType.FOOD) override fun compareTo(other: ItemFacet): Int { return COMPARATOR.compare(this, other as FoodItemFacet) diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ItemFacet.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ItemFacet.kt index 65819a1a25e..e80080c6716 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ItemFacet.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ItemFacet.kt @@ -22,17 +22,16 @@ import it.unimi.dsi.fastutil.objects.ObjectIntPair import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemCategory import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemFunction import net.ccbluex.liquidbounce.utils.inventory.ItemSlot -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemSlotType -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemType -import net.ccbluex.liquidbounce.utils.kotlin.Priority +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.GenericItemType +import net.ccbluex.liquidbounce.utils.inventory.ItemSlot.ItemSlotType import net.ccbluex.liquidbounce.utils.sorting.compareValueByCondition import net.minecraft.item.ItemStack open class ItemFacet(val itemSlot: ItemSlot) : Comparable { open val category: ItemCategory - get() = ItemCategory(ItemType.NONE, 0) + get() = ItemCategory(GenericItemType.ANY_ITEM, itemSlot.itemStack.item) - open val providedItemFunctions: List> + open val providedItemFunctions: List get() = emptyList() val itemStack: ItemStack @@ -41,14 +40,19 @@ open class ItemFacet(val itemSlot: ItemSlot) : Comparable { val isInHotbar: Boolean get() = this.itemSlot.slotType == ItemSlotType.HOTBAR || this.itemSlot.slotType == ItemSlotType.OFFHAND - open fun isSignificantlyBetter(other: ItemFacet): Boolean { - return false - } - /** * Should this item be kept, even if it is not allocated to any slot? */ open fun shouldKeep(): Boolean = false override fun compareTo(other: ItemFacet): Int = compareValueByCondition(this, other, ItemFacet::isInHotbar) + + /** + * Example: + * - Bow -> (BOW_LIKE, 1) + * - Porkchop -> (FOOD, ) + * + * @param amount The amount of the function this item gives. + */ + data class ProvidedFunction(val type: ItemFunction, val amount: Int) } diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/MiningToolItemFacet.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/MiningToolItemFacet.kt index a2f3ab9ce0f..0b418f60f6d 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/MiningToolItemFacet.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/MiningToolItemFacet.kt @@ -22,10 +22,14 @@ import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.* import net.ccbluex.liquidbounce.utils.inventory.ItemSlot import net.ccbluex.liquidbounce.utils.item.EnchantmentValueEstimator import net.ccbluex.liquidbounce.utils.item.material -import net.ccbluex.liquidbounce.utils.item.type import net.ccbluex.liquidbounce.utils.sorting.ComparatorChain import net.minecraft.enchantment.Enchantments +import net.minecraft.item.AxeItem +import net.minecraft.item.HoeItem +import net.minecraft.item.Item import net.minecraft.item.MiningToolItem +import net.minecraft.item.PickaxeItem +import net.minecraft.item.ShovelItem class MiningToolItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { companion object { @@ -45,10 +49,29 @@ class MiningToolItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { ) } + private val subtype = ItemToolType.guessType(itemSlot.itemStack.item) + override val category: ItemCategory - get() = ItemCategory(ItemType.TOOL, (this.itemStack.item as MiningToolItem).type) + get() = ItemCategory(GenericItemType.TOOL, subtype) override fun compareTo(other: ItemFacet): Int { return COMPARATOR.compare(this, other as MiningToolItemFacet) } + + enum class ItemToolType { + AXE, + PICKAXE, + SHOVEL, + HOE; + + companion object { + fun guessType(item: Item) = when (item) { + is AxeItem -> AXE + is PickaxeItem -> PICKAXE + is ShovelItem -> SHOVEL + is HoeItem -> HOE + else -> error("Unknown tool item $item.") + } + } + } } diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/PotionItemFacet.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/PotionItemFacet.kt index 01f2de2343c..ce088e33688 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/PotionItemFacet.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/PotionItemFacet.kt @@ -16,7 +16,7 @@ import java.util.* class PotionItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { override val category: ItemCategory - get() = ItemCategory(ItemType.POTION, 0) + get() = ItemCategory(GenericItemType.POTION) companion object { private val COMPARATOR = ComparatorChain( @@ -29,18 +29,20 @@ class PotionItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { ) /** - * Prefers potions which have more status effects of higher Tier. - * For example: + * Prefers potions which have more status effects of higher Tier (S, A, B, C, etc.). + * For example, * - `S > A` * - `A + A > A + B` * - `A + A + F > A + A` * - etc. */ private object PreferHigherTierPotions : Comparator { - override fun compare(o1: PotionItemFacet, o2: PotionItemFacet): Int = compareValuesBy(o1, o2) { o -> - o.itemStack.getPotionEffects() - .mapTo(ObjectArrayList(8)) { it.effectType.value().tier } - .apply { sortDescending() } + override fun compare(o1: PotionItemFacet, o2: PotionItemFacet): Int { + return compareValuesBy(o1, o2) { o -> + o.itemStack.getPotionEffects() + .mapTo(ObjectArrayList(8)) { it.effectType.value().tier } + .apply { sortDescending() } + } } } @@ -49,10 +51,12 @@ class PotionItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { * - Anything (S-Tier) II + Anything (S-Tier) I > Anything (S-Tier) I + Anything (S-Tier) I */ private object PreferAmplifier : Comparator { - override fun compare(o1: PotionItemFacet, o2: PotionItemFacet): Int = compareValuesBy(o1, o2) { o -> - o.itemStack.getPotionEffects() - .sortedByDescending { it.effectType.value().tier } - .mapInt { it.amplifier } + override fun compare(o1: PotionItemFacet, o2: PotionItemFacet): Int { + return compareValuesBy(o1, o2) { o -> + o.itemStack.getPotionEffects() + .sortedByDescending { it.effectType.value().tier } + .mapInt { it.amplifier } + } } } @@ -61,10 +65,9 @@ class PotionItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { */ private object PreferSplashPotions : Comparator { override fun compare(o1: PotionItemFacet, o2: PotionItemFacet): Int { - val tier1 = tierOfPotionType(o1.itemStack.item as PotionItem) - val tier2 = tierOfPotionType(o2.itemStack.item as PotionItem) - - return tier1.compareTo(tier2) + return compareValuesBy(o1, o2) { + tierOfPotionType(it.itemStack.item as PotionItem) + } } fun tierOfPotionType(potionItem: PotionItem): Tier { @@ -82,10 +85,12 @@ class PotionItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { * - `S (0:30) + A (1:00) > S (1:00) + A (20:00)` */ private object PreferHigherDurationPotions : Comparator { - override fun compare(o1: PotionItemFacet, o2: PotionItemFacet): Int = compareValuesBy(o1, o2) { o -> - o.itemStack.getPotionEffects() - .sortedByDescending { it.effectType.value().tier } - .mapInt { it.duration } + override fun compare(o1: PotionItemFacet, o2: PotionItemFacet): Int { + return compareValuesBy(o1, o2) { o -> + o.itemStack.getPotionEffects() + .sortedByDescending { it.effectType.value().tier } + .mapInt { it.duration } + } } } diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/PrimitiveItemFacet.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/PrimitiveItemFacet.kt index 45a1141604b..c2953e05405 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/PrimitiveItemFacet.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/PrimitiveItemFacet.kt @@ -18,23 +18,28 @@ */ package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemCategory +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.* import net.ccbluex.liquidbounce.utils.inventory.ItemSlot -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.PREFER_ITEMS_IN_HOTBAR -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.STABILIZE_COMPARISON +import net.ccbluex.liquidbounce.utils.item.EnchantmentValueEstimator import net.ccbluex.liquidbounce.utils.sorting.ComparatorChain +import net.minecraft.enchantment.Enchantments -class PrimitiveItemFacet(itemSlot: ItemSlot, override val category: ItemCategory, val worth: Int = 0) : - ItemFacet(itemSlot) { +class PrimitiveItemFacet(itemSlot: ItemSlot, override val category: ItemCategory) : ItemFacet(itemSlot) { companion object { + private val VALUE_ESTIMATOR = + EnchantmentValueEstimator( + EnchantmentValueEstimator.WeightedEnchantment(Enchantments.UNBREAKING, 0.4f), + ) private val COMPARATOR = ComparatorChain( - compareBy { it.worth }, compareBy { it.itemStack.count }, + compareBy { VALUE_ESTIMATOR.estimateValue(it.itemStack) }, PREFER_ITEMS_IN_HOTBAR, STABILIZE_COMPARISON, ) } - override fun compareTo(other: ItemFacet): Int = COMPARATOR.compare(this, other as PrimitiveItemFacet) + override fun compareTo(other: ItemFacet): Int { + return COMPARATOR.compare(this, other as PrimitiveItemFacet) + } } diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/RodItemFacet.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/RodItemFacet.kt deleted file mode 100644 index 6e9a7eb50d1..00000000000 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/RodItemFacet.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This file is part of LiquidBounce (https://github.com/CCBlueX/LiquidBounce) - * - * Copyright (c) 2015 - 2025 CCBlueX - * - * LiquidBounce is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LiquidBounce is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LiquidBounce. If not, see . - */ -package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items - -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.* -import net.ccbluex.liquidbounce.utils.inventory.ItemSlot -import net.ccbluex.liquidbounce.utils.item.EnchantmentValueEstimator -import net.ccbluex.liquidbounce.utils.sorting.ComparatorChain -import net.minecraft.enchantment.Enchantments - -class RodItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { - companion object { - private val VALUE_ESTIMATOR = - EnchantmentValueEstimator( - EnchantmentValueEstimator.WeightedEnchantment(Enchantments.UNBREAKING, 0.4f), - ) - private val COMPARATOR = - ComparatorChain( - compareBy { VALUE_ESTIMATOR.estimateValue(it.itemStack) }, - PREFER_ITEMS_IN_HOTBAR, - STABILIZE_COMPARISON, - ) - } - - override val category: ItemCategory - get() = ItemCategory(ItemType.ROD, 0) - - override fun compareTo(other: ItemFacet): Int { - return COMPARATOR.compare(this, other as RodItemFacet) - } -} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ShieldItemFacet.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ShieldItemFacet.kt deleted file mode 100644 index 5f3b3331b8f..00000000000 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ShieldItemFacet.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This file is part of LiquidBounce (https://github.com/CCBlueX/LiquidBounce) - * - * Copyright (c) 2015 - 2025 CCBlueX - * - * LiquidBounce is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * LiquidBounce is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with LiquidBounce. If not, see . - */ -package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items - -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.* -import net.ccbluex.liquidbounce.utils.inventory.ItemSlot -import net.ccbluex.liquidbounce.utils.item.EnchantmentValueEstimator -import net.ccbluex.liquidbounce.utils.sorting.ComparatorChain -import net.minecraft.enchantment.Enchantments - -class ShieldItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { - companion object { - private val VALUE_ESTIMATOR = - EnchantmentValueEstimator( - EnchantmentValueEstimator.WeightedEnchantment(Enchantments.UNBREAKING, 0.4f), - ) - private val COMPARATOR = - ComparatorChain( - compareBy { VALUE_ESTIMATOR.estimateValue(it.itemStack) }, - PREFER_ITEMS_IN_HOTBAR, - STABILIZE_COMPARISON, - ) - } - - override val category: ItemCategory - get() = ItemCategory(ItemType.SHIELD, 0) - - override fun compareTo(other: ItemFacet): Int { - return COMPARATOR.compare(this, other as ShieldItemFacet) - } -} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/SwordItemFacet.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/SwordItemFacet.kt index b2cb2d52790..20229cf4f18 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/SwordItemFacet.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/SwordItemFacet.kt @@ -2,7 +2,7 @@ package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemCategory import net.ccbluex.liquidbounce.utils.inventory.ItemSlot -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemType +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.GenericItemType /** * Specialization of weapon type. Used in order to allow the user to specify that they want a sword and not an axe @@ -10,5 +10,5 @@ import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemTy */ class SwordItemFacet(itemSlot: ItemSlot) : WeaponItemFacet(itemSlot) { override val category: ItemCategory - get() = ItemCategory(ItemType.SWORD, 0) + get() = ItemCategory(GenericItemType.SWORD) } diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ThrowableItemFacet.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ThrowableItemFacet.kt index 77462b38cdb..986d7fb81c1 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ThrowableItemFacet.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/ThrowableItemFacet.kt @@ -36,7 +36,7 @@ class ThrowableItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { } override val category: ItemCategory - get() = ItemCategory(ItemType.THROWABLE, 0) + get() = ItemCategory(GenericItemType.THROWABLE) override fun compareTo(other: ItemFacet): Int { return COMPARATOR.compare(this, other as ThrowableItemFacet) diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/WeaponItemFacet.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/WeaponItemFacet.kt index 360a5653b5c..cfbe319d92b 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/WeaponItemFacet.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/items/WeaponItemFacet.kt @@ -18,7 +18,6 @@ */ package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items -import it.unimi.dsi.fastutil.objects.ObjectIntPair import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.* import net.ccbluex.liquidbounce.utils.inventory.ItemSlot import net.ccbluex.liquidbounce.utils.item.EnchantmentValueEstimator @@ -26,9 +25,10 @@ import net.ccbluex.liquidbounce.utils.item.attackDamage import net.ccbluex.liquidbounce.utils.item.attackSpeed import net.ccbluex.liquidbounce.utils.item.getEnchantment import net.ccbluex.liquidbounce.utils.sorting.ComparatorChain -import net.ccbluex.liquidbounce.utils.sorting.compareByCondition import net.minecraft.component.DataComponentTypes import net.minecraft.enchantment.Enchantments +import net.minecraft.item.ItemStack +import net.minecraft.item.Items import net.minecraft.item.SwordItem import kotlin.math.ceil import kotlin.math.pow @@ -54,21 +54,22 @@ open class WeaponItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { EnchantmentValueEstimator.WeightedEnchantment(Enchantments.SWEEPING_EDGE, 0.2f), EnchantmentValueEstimator.WeightedEnchantment(Enchantments.KNOCKBACK, 0.25f), ) + + @Suppress("SpreadOperator") private val COMPARATOR = ComparatorChain( - compareBy { estimateDamage(it) }, + compareBy { estimateDamage(it.itemStack) }, compareBy { SECONDARY_VALUE_ESTIMATOR.estimateValue(it.itemStack) }, - compareByCondition { it.itemStack.item is SwordItem }, + compareBy { it.itemStack.item is SwordItem }, PREFER_BETTER_DURABILITY, compareBy { it.itemStack.get(DataComponentTypes.ENCHANTABLE)?.value ?: 0 }, - PREFER_ITEMS_IN_HOTBAR, - STABILIZE_COMPARISON, + *DEFAULT_TIE_BREAK ) - private fun estimateDamage(o1: WeaponItemFacet): Double { + private fun estimateDamage(stack: ItemStack): Double { // Already contains damage enchantments like sharpness - val attackDamage = o1.itemStack.attackDamage - val attackSpeed = o1.itemStack.attackSpeed + val attackDamage = stack.attackDamage + val attackSpeed = stack.attackSpeed val p = 0.85.pow(1 / 20.0) val bigT = 20.0 / attackSpeed @@ -77,20 +78,43 @@ open class WeaponItemFacet(itemSlot: ItemSlot) : ItemFacet(itemSlot) { val speedAdjustedDamage = attackDamage * attackSpeed * probabilityAdjustmentFactor.toFloat() - val damageFromFireAspect = (o1.itemStack.getEnchantment(Enchantments.FIRE_ASPECT) * 4.0f - 1) + val damageFromFireAspect = (stack.getEnchantment(Enchantments.FIRE_ASPECT) * 4.0f - 1) .coerceAtLeast(0.0F) * 0.33F - val additionalFactor = DAMAGE_ESTIMATOR.estimateValue(o1.itemStack) + val additionalFactor = DAMAGE_ESTIMATOR.estimateValue(stack) return speedAdjustedDamage * (1.0 + additionalFactor) + damageFromFireAspect } + + /** + * Only create a new instance if the item is useful. + * + * An item is useful as a weapon if it is better than fighting with nothing. + */ + fun createIfUsefulAsWeapon(slot: ItemSlot): WeaponItemFacet? { + if (!isBetterThanNothing(slot.itemStack)) { + return null + } + + return WeaponItemFacet(slot) + } + + /** + * Decides if this item is better than fighting with nothing. + */ + private fun isBetterThanNothing(stack: ItemStack): Boolean { + val baseDamage = estimateDamage(ItemStack(Items.STICK, 1)) + val itemDamage = estimateDamage(stack) + + return itemDamage > baseDamage || SECONDARY_VALUE_ESTIMATOR.estimateValue(stack) > 0.0F + } } override val category: ItemCategory - get() = ItemCategory(ItemType.WEAPON, 0) + get() = ItemCategory(GenericItemType.WEAPON) - override val providedItemFunctions: List> - get() = listOf(ObjectIntPair.of(ItemFunction.WEAPON_LIKE, 1)) + override val providedItemFunctions: List + get() = listOf(ProvidedFunction(ItemFunction.WEAPON_LIKE, 1)) override fun compareTo(other: ItemFacet): Int { return COMPARATOR.compare(this, other as WeaponItemFacet) diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/render/ModuleZoom.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/render/ModuleZoom.kt index 313025b549a..1ad4a0b41db 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/render/ModuleZoom.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/render/ModuleZoom.kt @@ -29,6 +29,8 @@ import net.ccbluex.liquidbounce.utils.input.InputBind import net.ccbluex.liquidbounce.utils.math.Easing import net.minecraft.util.math.MathHelper import kotlin.math.abs +import kotlin.math.log +import kotlin.math.pow import kotlin.math.round /** diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/world/scaffold/ScaffoldBlockItemSelection.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/world/scaffold/ScaffoldBlockItemSelection.kt index ded748d0ddf..2f5e96611a9 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/world/scaffold/ScaffoldBlockItemSelection.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/world/scaffold/ScaffoldBlockItemSelection.kt @@ -67,7 +67,7 @@ object ScaffoldBlockItemSelection { block is BlockWithEntity -> true // We don't like slabs etc. !block.defaultState.isFullCube(ModuleScaffold.mc.world!!, BlockPos.ORIGIN) -> true - // Is there a hard coded answer? + // Is there a hard-coded answer? else -> block in UNFAVORABLE_BLOCKS_TO_PLACE } } diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/inventory/ItemSlot.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/inventory/ItemSlot.kt index 0a9d5c8e8aa..db8bec045d8 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/inventory/ItemSlot.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/utils/inventory/ItemSlot.kt @@ -18,7 +18,6 @@ */ package net.ccbluex.liquidbounce.utils.inventory -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemSlotType import net.ccbluex.liquidbounce.utils.client.mc import net.ccbluex.liquidbounce.utils.client.player import net.minecraft.client.gui.screen.ingame.GenericContainerScreen @@ -27,14 +26,14 @@ import net.minecraft.util.Hand import java.util.* /** - * Represents an inventory slot (e.g. Hotbar Slot 0, OffHand, Chestslot 5, etc.) + * Represents an inventory slot (e.g., Hotbar Slot 0, OffHand, Chestslot 5, etc.) */ abstract class ItemSlot { abstract val itemStack: ItemStack abstract val slotType: ItemSlotType /** - * Used for example for slot click packets + * Used, for example, for slot click packets */ abstract fun getIdForServer(screen: GenericContainerScreen?): Int? @@ -43,6 +42,18 @@ abstract class ItemSlot { abstract override fun hashCode(): Int abstract override fun equals(other: Any?): Boolean + + enum class ItemSlotType { + HOTBAR, + OFFHAND, + ARMOR, + INVENTORY, + + /** + * e.g. chests + */ + CONTAINER, + } } /** diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/item/ArmorPiece.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/item/ArmorPiece.kt index f36ab0d1462..e1c1a4adbcc 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/item/ArmorPiece.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/utils/item/ArmorPiece.kt @@ -19,7 +19,7 @@ package net.ccbluex.liquidbounce.utils.item import net.ccbluex.liquidbounce.utils.inventory.ItemSlot -import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemSlotType +import net.ccbluex.liquidbounce.utils.inventory.ItemSlot.ItemSlotType import net.minecraft.entity.EquipmentSlot import net.minecraft.item.ArmorItem diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/sorting/ComparatorChain.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/sorting/ComparatorChain.kt index a58d0625c8d..98115506de3 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/sorting/ComparatorChain.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/utils/sorting/ComparatorChain.kt @@ -35,14 +35,10 @@ class ComparatorChain(private vararg val comparisonFunctions: Comparator compareValueByCondition(a: T, b: T, cond: (T) -> Boolean): Int { - val condA = cond(a) - val condB = cond(b) + val valA = if (cond(a)) 1 else 0 + val valB = if (cond(b)) 1 else 0 - return when { - condA == condB -> 0 - condA -> 1 - else -> -1 - } + return valA.compareTo(valB) } inline fun compareByCondition(crossinline cond: (T) -> Boolean): Comparator { From c21222c2e026feda8a071cebbee7a2302a4c3d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?= <93977077+MukjepScarlet@users.noreply.github.com> Date: Wed, 14 Jan 2026 22:11:52 +0800 Subject: [PATCH 2/3] revert --- .../features/module/modules/player/invcleaner/ItemMerge.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemMerge.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemMerge.kt index 55509ace086..d1d34a91bcb 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemMerge.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemMerge.kt @@ -23,7 +23,7 @@ import kotlin.math.ceil object ItemMerge { /** - * Find all item stack ids that should be double-clicked to merge them + * Find all item stack ids which should be double-clicked in order to merge them */ internal fun findStacksToMerge(cleanupPlan: InventoryCleanupPlan): List { val itemsToMerge = mutableListOf() From 3c913001d2b708a81fea9aada7f16a40d2ec10e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?= <93977077+MukjepScarlet@users.noreply.github.com> Date: Wed, 14 Jan 2026 22:12:20 +0800 Subject: [PATCH 3/3] [Skip CI] revert --- .../module/modules/world/scaffold/ScaffoldBlockItemSelection.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/world/scaffold/ScaffoldBlockItemSelection.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/world/scaffold/ScaffoldBlockItemSelection.kt index 2f5e96611a9..ded748d0ddf 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/world/scaffold/ScaffoldBlockItemSelection.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/world/scaffold/ScaffoldBlockItemSelection.kt @@ -67,7 +67,7 @@ object ScaffoldBlockItemSelection { block is BlockWithEntity -> true // We don't like slabs etc. !block.defaultState.isFullCube(ModuleScaffold.mc.world!!, BlockPos.ORIGIN) -> true - // Is there a hard-coded answer? + // Is there a hard coded answer? else -> block in UNFAVORABLE_BLOCKS_TO_PLACE } }