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 00000000000..10da0be3549
Binary files /dev/null and b/src-theme/public/img/clickgui/icon-arrow.png differ
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 00000000000..749add68861
Binary files /dev/null and b/src-theme/public/img/clickgui/icon-axe.png differ
diff --git a/src-theme/public/img/clickgui/icon-blocks.png b/src-theme/public/img/clickgui/icon-blocks.png
new file mode 100644
index 00000000000..0bc7286b4c1
Binary files /dev/null and b/src-theme/public/img/clickgui/icon-blocks.png differ
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 00000000000..c254f4b725f
Binary files /dev/null and b/src-theme/public/img/clickgui/icon-egg.png differ
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 00000000000..a62f6529a18
Binary files /dev/null and b/src-theme/public/img/clickgui/icon-food.png differ
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 00000000000..7acfad74a33
Binary files /dev/null and b/src-theme/public/img/clickgui/icon-hoe.png differ
diff --git a/src-theme/public/img/clickgui/icon-ignore.svg b/src-theme/public/img/clickgui/icon-ignore.svg
new file mode 100644
index 00000000000..082ce6d9d47
--- /dev/null
+++ b/src-theme/public/img/clickgui/icon-ignore.svg
@@ -0,0 +1,7 @@
+
+
+
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 00000000000..d669fb13e9c
Binary files /dev/null and b/src-theme/public/img/clickgui/icon-pickaxe.png differ
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 00000000000..63e33c88ab8
Binary files /dev/null and b/src-theme/public/img/clickgui/icon-potion.png differ
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 00000000000..b77d5bd843e
Binary files /dev/null and b/src-theme/public/img/clickgui/icon-shovel.png differ
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 00000000000..5f6f29bd2ae
Binary files /dev/null and b/src-theme/public/img/clickgui/icon-sword.png differ
diff --git a/src-theme/public/img/clickgui/icon-value-none.svg b/src-theme/public/img/clickgui/icon-value-none.svg
new file mode 100644
index 00000000000..14b2bf9cb62
--- /dev/null
+++ b/src-theme/public/img/clickgui/icon-value-none.svg
@@ -0,0 +1,11 @@
+
+
+
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"}
+
+{:else if item.type === "GROUP"}
+
+{:else if item.type === "IGNORE"}
+
+{:else if item.type === "ANY"}
+
+{:else}
+
+{/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}>
+

+
+
+ {#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 @@
+
+
+
+
+
+
+
+ Preset editor
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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}
+
+
+
+
+
+
+

+
+
+
+
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
+
+
await setTyping(true)}
+ on:focusout={async () => await setTyping(false)}
+ spellcheck="false">
+
+

+
+
+
+
+
+ {#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.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/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/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 {