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"} + {item.item}/ +{:else if item.type === "GROUP"} + {item.group} +{:else if item.type === "IGNORE"} + Tools +{:else if item.type === "ANY"} + Any +{:else} + Any +{/if} + + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/ItemPreview.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/ItemPreview.svelte new file mode 100644 index 00000000000..1495e1ccc92 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/ItemPreview.svelte @@ -0,0 +1,42 @@ + + +
+ {#if rendered.length > 0} +
+ +
+ {/if} +
+ + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/ItemGroupSelector.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/ItemGroupSelector.svelte new file mode 100644 index 00000000000..ed7e245a903 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/ItemGroupSelector.svelte @@ -0,0 +1,186 @@ + + + + + +
+
+
+
expanded = !expanded}> + Add +
+ + {#if expanded} +
expanded = false} + use:portal + > + !choiceItems.includes(it)} + /> +
+ {/if} +
+ + {#each items as item, idx} +
removeItem(idx)}> +
+ +
+
+ {/each} +
+
+ + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/PresetModal.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/PresetModal.svelte new file mode 100644 index 00000000000..4b58aa36890 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/PresetModal.svelte @@ -0,0 +1,172 @@ + + + + + + + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/PresetTooltip.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/PresetTooltip.svelte new file mode 100644 index 00000000000..dc6523c3992 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/PresetTooltip.svelte @@ -0,0 +1,137 @@ + + + hovered = true} + onmouseleave={() => hovered = false} +> + + {#if hovered} + + {text} + + {/if} + + + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/item.scss b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/item.scss new file mode 100644 index 00000000000..36e2d41181b --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/item.scss @@ -0,0 +1,11 @@ +@use "sass:color"; +@use "../../../../../colors.scss" as *; + +.item-background { + background-color: rgba($clickgui-base-color, 0.85); + outline: 1px solid color.adjust($clickgui-text-color, $lightness: -85%); + + &:hover { + outline: 1px solid color.adjust($clickgui-text-color, $lightness: -70%); + } +} diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/maxStacks/MaxStacksContainer.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/maxStacks/MaxStacksContainer.svelte new file mode 100644 index 00000000000..1eea9a0a824 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/maxStacks/MaxStacksContainer.svelte @@ -0,0 +1,57 @@ + + + + +{#if groups.length > 0} +
+
+ {#each groups as group, idx} + handleDelete(idx)} + /> + {/each} +
+
+{/if} + + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/maxStacks/StackContainer.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/maxStacks/StackContainer.svelte new file mode 100644 index 00000000000..60f46cdc8d2 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/maxStacks/StackContainer.svelte @@ -0,0 +1,158 @@ + + + + + +
+
+ + +
+
+ Limit +
+ {#if group.itemCount < 9 * 4 * 64} + 64 x + updateItemCount(e.detail.value, nItems)}/> + + + updateItemCount(nStacks, e.detail.value)}/> + {:else} + + {/if} +
+
+
+
+
+
+ exit +
+
+ + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupComponent.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupComponent.svelte new file mode 100644 index 00000000000..c09e096545b --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupComponent.svelte @@ -0,0 +1,126 @@ + + + + +
+
expanded = !expanded} + > +
+ +
+
+ + {#if expanded} +
+ + +
+ {idx === 0 ? "Offhand" : idx} +
+
+ {/if} +
+ + + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupPreferenceSelector.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupPreferenceSelector.svelte new file mode 100644 index 00000000000..200a43e6c70 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupPreferenceSelector.svelte @@ -0,0 +1,141 @@ + + +
+
+ Preference +
+ +
+ + + + + +
+ + {#if showingItems} + + + {/if} +
+ + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupPreview.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupPreview.svelte new file mode 100644 index 00000000000..f2fe177d62f --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/GroupPreview.svelte @@ -0,0 +1,48 @@ + + +
+ {#if group.length === 0} +
+ +
+ {:else} + {#each group.slice(0, 3) as item} +
+ +
+ {/each} + + {#if group.length > 3} +
+{Math.min(9, group.length - 3)}
+ {/if} + {/if} +
+ + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/Items.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/Items.svelte new file mode 100644 index 00000000000..769847ecd7b --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/Items.svelte @@ -0,0 +1,92 @@ + + + + +
+ {#each items as group, idx (idx)} +
handleDragStart(e, idx)} + on:dragover={(e) => handleDragOver(e, idx)} + on:dragend={handleDragEnd} + > + +
+ + {#if idx === 0} +
+ {/if} + {/each} +
+ + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/PresetItemSelector.svelte b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/PresetItemSelector.svelte new file mode 100644 index 00000000000..390ebcfe4cd --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/presetItem/PresetItemSelector.svelte @@ -0,0 +1,305 @@ + + + + +
+
+ {searchQuery === "" ? "Select Items" : "Search"} +
+ + {#if searchQuery === ""} +
+ Quick Select +
+ {#each commonItems as commonItem} +
setItemProxy(commonItem)}> +
+ +
+
+ {/each} +
+
+ {/if} +
+ Specific Items + + +
+ + {#if searchQuery === ""} +
+ Item Groups +
+ +
setItemProxy(item.item)}> +
+
+ +
+
+ {$spaceSeperatedNames ? convertToSpacedString(item.name) : item.name} + {#if item.tooltip != null} + + {/if} +
+
+
+
+ {:else} + {#if renderedItems.length > 0} +
+ +
setItemProxy({type: "SINGLE", item: item.identifier})}> +
+ {item.identifier}/ +
+ + + {item.name} + +
+
+
+ {:else} + No Results + {/if} + {/if} +
+ + diff --git a/src-theme/src/routes/clickgui/setting/inventoryPreset/config/select.scss b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/select.scss new file mode 100644 index 00000000000..0dfefc64f97 --- /dev/null +++ b/src-theme/src/routes/clickgui/setting/inventoryPreset/config/select.scss @@ -0,0 +1,121 @@ +@use "sass:color"; +@use "../../../../../colors.scss" as *; + +.selector-container-wrapper { + z-index: 9999; + position: absolute; + width: 250px; + max-height: 450px; + min-height: 0; + padding: 20px; + background-color: rgba($clickgui-base-color, 0.95); + outline: 1px solid color.adjust($clickgui-text-color, $lightness: -85%); + border-radius: 3px; +} + + +.select-title { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + font-weight: 500; + font-size: 16px; + color: $clickgui-text-color; +} + +.select-selector { + display: flex; + flex-direction: column; + gap: 20px; + height: 100%; +} + +.icon-wrapper { + transition: background-color 0.3s ease; + background-color: rgba($clickgui-base-color, 0.85); + border: 1px solid color.adjust($clickgui-text-color, $lightness: -85%); + border-radius: 3px; + min-width: 30px; + min-height: 30px; + display: flex; + align-items: center; + justify-content: center; +} + +.result-item { + display: flex; + height: 40px; + align-items: center; + gap: 10px; + position: relative; + cursor: pointer; + + &:hover { + .name { + color: $clickgui-text-color; + } + + .icon-wrapper { + background-color: color.adjust($clickgui-text-color, $lightness: -80%); + } + } +} + +.icon { + width: 20px; + height: 20px; +} + +.name { + transition: color 0.3s ease; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: $clickgui-text-dimmed-color; +} + +.search { + position: relative; +} + +.search-input { + width: 100%; + height: 35px; + border-radius: 3px; + border: none; + background: transparent; + padding-left: 35px; + outline: solid 1px color.adjust($clickgui-text-color, $lightness: -90%); + color: white; +} + +.search-icon { + position: absolute; + left: 0; + top: 0; + width: 35px; + height: 35px; + display: flex; + align-items: center; + justify-content: center; + + & > img { + width: 20px; + height: 20px; + } +} + +.results { + height: 100%; + overflow-y: auto; + overflow-x: hidden; +} + +.items-group-title { + font-size: 12px; + color: rgba($clickgui-text-dimmed-color, 0.6); + font-weight: 600; + margin-left: 5px; + text-transform: uppercase; +} diff --git a/src-theme/src/util/utils.ts b/src-theme/src/util/utils.ts new file mode 100644 index 00000000000..526271114d8 --- /dev/null +++ b/src-theme/src/util/utils.ts @@ -0,0 +1,32 @@ +export function portal(node: HTMLElement, target: HTMLElement = document.body) { + target.appendChild(node); + return { + destroy() { + if (node.parentNode) node.parentNode.removeChild(node); + } + }; +} + +export function clickOutside(node: HTMLElement, callback: (event: MouseEvent) => void) { + const handleClick = (event: MouseEvent) => { + if (!node.contains(event.target as Node)) { + callback(event); + } + }; + + const handleDrag = (event: DragEvent) => { + if (!node.contains(event.target as Node)) { + callback(event); + } + }; + + document.addEventListener('click', handleClick, true); + document.addEventListener('dragstart', handleDrag, true); + + return { + destroy() { + document.removeEventListener('click', handleClick, true); + document.removeEventListener('dragstart', handleDrag, true); + } + }; +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/GsonInstance.kt b/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/GsonInstance.kt index 92be4fba61a..dc3071cd48a 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/GsonInstance.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/GsonInstance.kt @@ -30,6 +30,8 @@ import net.ccbluex.liquidbounce.config.gson.serializer.minecraft.* import net.ccbluex.liquidbounce.config.gson.stategies.ExcludeStrategy import net.ccbluex.liquidbounce.config.gson.stategies.ProtocolExcludeStrategy import net.ccbluex.liquidbounce.config.types.NamedChoice +import net.ccbluex.liquidbounce.features.inventoryPreset.InventoryPreset +import net.ccbluex.liquidbounce.features.inventoryPreset.FrontendSlotPreference import net.ccbluex.liquidbounce.config.types.nesting.ChoiceConfigurable import net.ccbluex.liquidbounce.config.types.nesting.Configurable import net.ccbluex.liquidbounce.integration.theme.component.Component @@ -147,6 +149,8 @@ internal fun GsonBuilder.registerCommonTypeAdapters() = .registerTypeHierarchyAdapter(ClosedRange::class.javaObjectType, RangeAdapter) .registerTypeHierarchyAdapter(IntRange::class.javaObjectType, IntRangeAdapter) .registerTypeHierarchyAdapter(Item::class.javaObjectType, ItemAdapter) + .registerTypeHierarchyAdapter(InventoryPreset::class.java, InventoryPresetAdapter) + .registerTypeHierarchyAdapter(FrontendSlotPreference::class.java, FrontendSlotPreferenceAdapter) .registerTypeHierarchyAdapter(SoundEvent::class.javaObjectType, SoundEventAdapter) .registerTypeHierarchyAdapter(StatusEffect::class.javaObjectType, StatusEffectAdapter) .registerTypeHierarchyAdapter(Color4b::class.javaObjectType, ColorAdapter) diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/adapter/FrontendSlotPreferenceAdapter.kt b/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/adapter/FrontendSlotPreferenceAdapter.kt new file mode 100644 index 00000000000..2dd047eec94 --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/adapter/FrontendSlotPreferenceAdapter.kt @@ -0,0 +1,46 @@ +@file:Suppress("WildcardImport") + +package net.ccbluex.liquidbounce.config.gson.adapter + +import com.google.gson.* +import net.ccbluex.liquidbounce.features.inventoryPreset.FrontendSlotPreference +import net.ccbluex.liquidbounce.features.inventoryPreset.FrontendSlotPreference.GroupSlotPreference.ItemGroupType +import net.minecraft.item.Item +import java.lang.reflect.Type + +object FrontendSlotPreferenceAdapter : + JsonSerializer, JsonDeserializer { + override fun serialize( + src: FrontendSlotPreference, + typeOfSrc: Type?, + context: JsonSerializationContext + ): JsonObject { + return src.serialize(context) + } + + override fun deserialize( + json: JsonElement, + typeOfT: Type?, + context: JsonDeserializationContext + ): FrontendSlotPreference { + val obj = json.asJsonObject + + return when (obj["type"].asString) { + "SINGLE" -> FrontendSlotPreference.SingleSlotPreference( + context.deserialize( + obj["item"], + Item::class.java + ) + ) + "GROUP" -> FrontendSlotPreference.GroupSlotPreference( + context.deserialize( + obj["group"], + ItemGroupType::class.java + ) + ) + "IGNORE" -> FrontendSlotPreference.IgnoreSlotPreference + "ANY" -> FrontendSlotPreference.AnySlotPreference + else -> error("Unknown slot preference ${obj["type"]}") + } + } +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/adapter/InventoryPresetAdapter.kt b/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/adapter/InventoryPresetAdapter.kt new file mode 100644 index 00000000000..cfea2304665 --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/config/gson/adapter/InventoryPresetAdapter.kt @@ -0,0 +1,35 @@ +package net.ccbluex.liquidbounce.config.gson.adapter + +import com.google.gson.* +import com.google.gson.reflect.TypeToken +import net.ccbluex.liquidbounce.features.inventoryPreset.InventoryPreset +import net.ccbluex.liquidbounce.features.inventoryPreset.FrontendSlotPreference +import net.ccbluex.liquidbounce.features.inventoryPreset.FrontendItemLimitRules +import net.ccbluex.liquidbounce.utils.kotlin.mapArray +import java.lang.reflect.Type + +object InventoryPresetAdapter : JsonSerializer, JsonDeserializer { + override fun serialize( + src: InventoryPreset, + typeOfSrc: Type, + context: JsonSerializationContext + ): JsonElement = JsonObject().apply { + add("items", context.serialize(src.itemRulesToArray())) + add("maxStacks", context.serialize(src.itemLimitRules)) + } + + override fun deserialize( + json: JsonElement, + typeOfT: Type, + context: JsonDeserializationContext + ): InventoryPreset = with (json.asJsonObject) { + val items = getAsJsonArray("items").map { context.decode>(it) } + val throws = getAsJsonArray("maxStacks").map { context.decode(it) } + + return InventoryPreset(items.toTypedArray(), throws) + } + + private inline fun JsonDeserializationContext.decode(element: JsonElement): T { + return deserialize(element, object: TypeToken() {}.type) + } +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/config/types/Value.kt b/src/main/kotlin/net/ccbluex/liquidbounce/config/types/Value.kt index f42309ea9c2..63f29687b62 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/config/types/Value.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/config/types/Value.kt @@ -29,6 +29,8 @@ import net.ccbluex.liquidbounce.config.types.nesting.ChoiceConfigurable import net.ccbluex.liquidbounce.config.util.AutoCompletionProvider import net.ccbluex.liquidbounce.event.EventManager import net.ccbluex.liquidbounce.event.events.ValueChangedEvent +import net.ccbluex.liquidbounce.features.inventoryPreset.InventoryPreset +import net.ccbluex.liquidbounce.features.misc.FriendManager import net.ccbluex.liquidbounce.lang.translation import net.ccbluex.liquidbounce.script.ScriptApiRequired import net.ccbluex.liquidbounce.script.asArray @@ -312,6 +314,11 @@ open class Value( } +class InventoryPresetValue : Value("InventoryPreset", + defaultValue = InventoryPreset(), + valueType = ValueType.INVENTORY_PRESET, +) + /** * Order by name of [Value] (ignoreCase) */ @@ -428,6 +435,7 @@ enum class ValueType( SERVER_PACKET, KEY(HumanInputDeserializer.keyDeserializer), BIND, + INVENTORY_PRESET, VECTOR_I, VECTOR_D, diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/config/types/nesting/Configurable.kt b/src/main/kotlin/net/ccbluex/liquidbounce/config/types/nesting/Configurable.kt index 8e2c15a2369..b2fad645ef7 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/config/types/nesting/Configurable.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/config/types/nesting/Configurable.kt @@ -20,6 +20,7 @@ package net.ccbluex.liquidbounce.config.types.nesting import net.ccbluex.liquidbounce.config.types.* import net.ccbluex.liquidbounce.event.EventListener +import net.ccbluex.liquidbounce.features.module.ClientModule import net.ccbluex.liquidbounce.render.engine.type.Color4b import net.ccbluex.liquidbounce.utils.client.toLowerCamelCase import net.ccbluex.liquidbounce.utils.input.InputBind @@ -305,6 +306,21 @@ open class Configurable( fun > serverPackets(name: String, default: C) = registryList(name, default, ValueType.SERVER_PACKET) + fun inventoryPreset() = InventoryPresetValue().apply { + require(this@Configurable is ClientModule) { + "Requires that it only be in a module, " + + "it can't be a child of anything else because the design might go " + + "wrong (maybe this will be resolved in a future implementation, " + + "but for now it is like this)" + } + + require(this@Configurable.inner.find { it is InventoryPresetValue } == null) { + "It can only be one for, it is not possible to specify it twice yet." + } + + this@Configurable.inner.add(this) + } + inline fun multiEnumChoice( name: String, vararg default: T, diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/FrontendItemLimitRules.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/FrontendItemLimitRules.kt new file mode 100644 index 00000000000..cfc0415195f --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/FrontendItemLimitRules.kt @@ -0,0 +1,15 @@ +package net.ccbluex.liquidbounce.features.inventoryPreset + +/** + * Represents a group restriction limiting the maximum total stacks for specific items. + */ +class FrontendItemLimitRules( + val itemCount: Int, + val items: Set = emptySet() +) { + init { + require(items.find { it == FrontendSlotPreference.IgnoreSlotPreference } == null) { + "An item in limits cannot be ignored." + } + } +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/FrontendSlotPreference.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/FrontendSlotPreference.kt new file mode 100644 index 00000000000..0f5b02521e8 --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/FrontendSlotPreference.kt @@ -0,0 +1,144 @@ +package net.ccbluex.liquidbounce.features.inventoryPreset + +import com.google.gson.JsonObject +import com.google.gson.JsonSerializationContext +import com.google.gson.annotations.SerializedName +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.CleanupPlanTemplate +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.CleanupPlanTemplate.CleanupPlanRestrictions.RestrictionType +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.GenericItemType +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items.MiningToolItemFacet +import net.minecraft.item.Item +import net.minecraft.item.Items + +/** + * Contains the frontend representation of the user defined preference of what should a slot contain. + */ +sealed class FrontendSlotPreference { + /** + * Converts the frontend representation of the user + * configured preset into a version + * which the [net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.CleanupPlanGenerator] understands. + */ + abstract fun toBackendRepresentation(): ConvertedSlotPreference + abstract fun serialize(context: JsonSerializationContext): JsonObject + + class SingleSlotPreference(private val item: Item) : FrontendSlotPreference() { + companion object { + /** + * Some items like bow or crossbow represent an item type with additional sorting logic. + * Those items must be remapped. + */ + val itemSpecialTypeMap = mapOf( + Items.BOW to CleanupPlanTemplate.SlotContentPreference(GenericItemType.BOW), + Items.CROSSBOW to CleanupPlanTemplate.SlotContentPreference(GenericItemType.CROSSBOW), + ) + } + + override fun toBackendRepresentation(): ConvertedSlotPreference { + val specialType = itemSpecialTypeMap[item] + + if (specialType != null) { + return ConvertedSlotPreference(specialType) + } + + val contentPreference = CleanupPlanTemplate.SlotContentPreference( + itemType = GenericItemType.ANY_ITEM, + subtypes = setOf(item) + ) + + return ConvertedSlotPreference(contentPreference) + } + + override fun serialize(context: JsonSerializationContext) = JsonObject().apply { + addProperty("type", "SINGLE") + + add("item", context.serialize(item)) + } + } + + class GroupSlotPreference(private val itemGroupType: ItemGroupType) : FrontendSlotPreference() { + override fun toBackendRepresentation(): ConvertedSlotPreference { + return ConvertedSlotPreference(itemGroupType.preference) + } + + /** + * Enum representing item categories used for preset item classification. + */ + @Suppress("UNUSED") + enum class ItemGroupType(val preference: CleanupPlanTemplate.SlotContentPreference) { + @SerializedName("ARROWS") + ARROWS(CleanupPlanTemplate.SlotContentPreference(GenericItemType.ARROW)), + @SerializedName("SWORD") + SWORD(CleanupPlanTemplate.SlotContentPreference(GenericItemType.SWORD)), + @SerializedName("WEAPON") + WEAPON(CleanupPlanTemplate.SlotContentPreference(GenericItemType.WEAPON)), + @SerializedName("AXE") + AXE_TOOL( + CleanupPlanTemplate.SlotContentPreference( + GenericItemType.TOOL, + setOf(MiningToolItemFacet.ItemToolType.AXE) + ) + ), + @SerializedName("HOE") + HOE_TOOL( + CleanupPlanTemplate.SlotContentPreference( + GenericItemType.TOOL, + setOf(MiningToolItemFacet.ItemToolType.HOE) + ) + ), + @SerializedName("SHOVEL") + SHOVEL_TOOL( + CleanupPlanTemplate.SlotContentPreference( + GenericItemType.TOOL, + setOf(MiningToolItemFacet.ItemToolType.SHOVEL) + ) + ), + @SerializedName("PICKAXE") + PICKAXE_TOOL( + CleanupPlanTemplate.SlotContentPreference( + GenericItemType.TOOL, + setOf(MiningToolItemFacet.ItemToolType.PICKAXE) + ) + ), + @SerializedName("FOOD") + FOOD(CleanupPlanTemplate.SlotContentPreference(GenericItemType.FOOD)), + @SerializedName("POTION") + POTION(CleanupPlanTemplate.SlotContentPreference(GenericItemType.POTION)), + @SerializedName("BLOCK") + BLOCK(CleanupPlanTemplate.SlotContentPreference(GenericItemType.BLOCK)), + @SerializedName("THROWABLE") + THROWABLE(CleanupPlanTemplate.SlotContentPreference(GenericItemType.THROWABLE)) + } + + override fun serialize(context: JsonSerializationContext) = JsonObject().apply { + addProperty("type", "GROUP") + + add("group", context.serialize(itemGroupType)) + } + } + + data object IgnoreSlotPreference : FrontendSlotPreference() { + override fun toBackendRepresentation(): ConvertedSlotPreference { + return ConvertedSlotPreference(null, RestrictionType.FORBID_TAMPERING) + } + + override fun serialize(context: JsonSerializationContext) = JsonObject().apply { + addProperty("type", "IGNORE") + } + } + + data object AnySlotPreference : FrontendSlotPreference() { + override fun toBackendRepresentation(): ConvertedSlotPreference { + return ConvertedSlotPreference(null, RestrictionType.NONE) + } + + override fun serialize(context: JsonSerializationContext) = JsonObject().apply { + addProperty("type", "ANY") + } + } + + data class ConvertedSlotPreference( + val contentPreference: CleanupPlanTemplate.SlotContentPreference?, + val slotRestriction: RestrictionType = RestrictionType.NONE + ) +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/InventoryPreset.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/InventoryPreset.kt new file mode 100644 index 00000000000..87f3f03cde2 --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/inventoryPreset/InventoryPreset.kt @@ -0,0 +1,71 @@ +@file:Suppress("WildcardImport") + +package net.ccbluex.liquidbounce.features.inventoryPreset + +import net.ccbluex.liquidbounce.utils.inventory.HotbarItemSlot +import net.ccbluex.liquidbounce.utils.inventory.OffHandSlot + +/** + * Represents an inventory preset configuration defining item groups for specific slots and stack limitations. + * + * This preset maintains a strict relationship between array indices and inventory slots: + * - The [items] array is guaranteed to contain exactly 10 elements. + * - Index 0 always represents the [OffHandSlot] + * - Indices 1-9 correspond to hotbar slots 0-8 respectively (index -1 adjustment) + * + * @property itemLimitRules Array of stack limitation groups applying to the entire inventory + * @param items Initial item group configuration (must contain exactly 10 elements). + * Each array position maps to: + * - [OffHandSlot] for index 0 + * - [HotbarItemSlot] (0-8) for indices 1-9 + * + * @throws IllegalArgumentException if item array size isn't exactly 10 during initialization + */ +@Suppress("MagicNumber") +class InventoryPreset( + items: Array> = Array(10) { listOf() }, + val itemLimitRules: List = emptyList() +) { + val items: Map> + + init { + // Required because the frontend would break if there weren't exactly 10 entries... + require(items.size == 10) + + require(items.flatMap { it }.find { it == FrontendSlotPreference.AnySlotPreference } == null) { + "For an item to be Any, the list must be empty." + } + + items.forEach { preferences -> + val ignoreCount = preferences.count { it == FrontendSlotPreference.IgnoreSlotPreference } + require(ignoreCount == 0 || (ignoreCount == 1 && preferences.size == 1)) { + "If you use IgnoreSlotPreference, it must be the ONLY element in the list" + } + } + + val itemMap = items + .mapIndexed { index, item -> getSlotForIndex(index) to item } + .associate { it } + + this.items = itemMap + } + + private fun getSlotForIndex(idx: Int): HotbarItemSlot { + return when (idx) { + 0 -> OffHandSlot + else -> HotbarItemSlot(idx - 1) + } + } + + fun itemRulesToArray(): Array> { + return Array(10) { + val preferences = items[getSlotForIndex(it)] + + if (preferences.isNullOrEmpty()) { + return@Array listOf() + } + + preferences + } + } +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/misc/ModuleElytraSwap.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/misc/ModuleElytraSwap.kt index b8a9d7d944e..627664e990f 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/misc/ModuleElytraSwap.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/misc/ModuleElytraSwap.kt @@ -88,7 +88,7 @@ object ModuleElytraSwap : ClientModule( schedule(constraints, actions) } - private fun Item.isChestplate() = this is ArmorItem && type() == EquipmentType.CHESTPLATE + private fun Item.isChestplate() = this is ArmorItem && this.type() == EquipmentType.CHESTPLATE private fun ItemStack.isElytra() = this.item == Items.ELYTRA diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/cheststealer/ModuleChestStealer.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/cheststealer/ModuleChestStealer.kt index acf1813b473..aac1a4db018 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/cheststealer/ModuleChestStealer.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/cheststealer/ModuleChestStealer.kt @@ -28,6 +28,8 @@ import net.ccbluex.liquidbounce.features.module.ClientModule import net.ccbluex.liquidbounce.features.module.modules.player.cheststealer.features.FeatureChestAura import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.* import net.ccbluex.liquidbounce.utils.inventory.* +import net.ccbluex.liquidbounce.utils.inventory.ItemSlot.ItemSlotType +import net.ccbluex.liquidbounce.utils.item.* import net.minecraft.client.gui.screen.ingame.GenericContainerScreen import net.minecraft.text.Text import kotlin.math.ceil @@ -212,7 +214,7 @@ object ModuleChestStealer : ClientModule("ChestStealer", Category.PLAYER) { } else { val availableItems = findNonEmptySlotsInInventory() + findItemsInContainer(screen) - CleanupPlanGenerator(ModuleInventoryCleaner.cleanupTemplateFromSettings, availableItems).generatePlan() + CleanupPlanGenerator(ModuleInventoryCleaner.cleanupTemplateFromSettings, availableItems).plan } return cleanupPlan diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/CleanupPlanGenerator.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/CleanupPlanGenerator.kt index 25a85ab2c26..69c8d476085 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/CleanupPlanGenerator.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/CleanupPlanGenerator.kt @@ -18,81 +18,120 @@ */ package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.CleanupPlanTemplate.CleanupPlanRestrictions.RestrictionType +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.ItemNumberConstraintEnforcer.SatisfactionStatus import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items.ItemFacet import net.ccbluex.liquidbounce.utils.inventory.ItemSlot import net.ccbluex.liquidbounce.utils.item.isNothing -class CleanupPlanGenerator( - private val template: CleanupPlanPlacementTemplate, - private val availableItems: List, -) : ItemPacker.ItemAmountContraintProvider { - private val hotbarSwaps: ArrayList = ArrayList() +class CleanupPlanGenerator(private val template: CleanupPlanTemplate, private val availableItems: List) { + private val wishOrganizer = WishOrganizer(this.template) + private val constraintEnforcer = ItemNumberConstraintEnforcer(template) - private val packer = ItemPacker() + val plan: InventoryCleanupPlan - private val currentLimit = HashMap() + init { + val allItemFacets = discoverItemFacets() + // All slots the cleaner may swap into other slots + val availableItemFacets = allItemFacets.filter { + this.template.restrictions.getRestrictionFor(it.itemSlot) < RestrictionType.FORBID_REPLACING + } - // TODO Implement greedy check - /** - * Keeps track of where a specific type of item should be placed. e.g. BLOCK -> [Hotbar 7, Hotbar 8] - */ - private val categoryToSlotsMap: Map> = - template.slotContentMap.entries - .filter { (_, itemType) -> itemType.category != null } - .groupBy { (_, itemType) -> itemType.category!! } - .mapValues { (_, entries) -> entries.map { (slot, _) -> slot } } + val itemDispenserRack = ItemDispenserRack(this.wishOrganizer, availableItemFacets) - fun generatePlan(): InventoryCleanupPlan { - val categorizer = ItemCategorization(availableItems) + val usefulItems = HashSet() - // Contains all facets that the available items represent. i.e. if we have an axe in slot 5, this would be - // (Axe(Slot 5), Weapon(Slot 5)) since the axe can also function as a weapon. - val itemFacets = availableItems.flatMap { categorizer.getItemFacets(it).asIterable() } + // Consider all slots that may not be touched at all as useful. + usefulItems.addAll(template.restrictions.getSlotsWithAtLeast(RestrictionType.FORBID_TAMPERING)) - // i.e. BLOCK -> [Block(Slot 5), Block(Slot 6)] - // Keep priority in mind (Tool slots are processed before weapon slots) - val facetsGroupedByType = - itemFacets - .groupBy { it.category } - .entries - .sortedByDescending { it.key.type.allocationPriority } + val swaps = generateSwaps(itemDispenserRack, usefulItems) - for ((category, availableItems) in facetsGroupedByType) { - processItemCategory(category, availableItems) - } - - // We aren't allowed to touch those, so we just consider them as useful. - packer.usefulItems.addAll(this.template.forbiddenSlots) + findOtherUsefulItems(usefulItems, allItemFacets) - return InventoryCleanupPlan( - usefulItems = packer.usefulItems, - swaps = hotbarSwaps, + this.plan = InventoryCleanupPlan( + usefulItems = usefulItems, + swaps = swaps, mergeableItems = groupItemsByType(), ) } - private fun processItemCategory( - category: ItemCategory, - availableItems: List, - ) { - val hotbarSlotsToFill = this.categoryToSlotsMap[category] + /** + * This function marks all useful items that aren't filled into hotbar slots (i.e., arrows) as useful. + */ + private fun findOtherUsefulItems(usefulItems: HashSet, allItemFacets: List) { + val facetsGroupedByCategory = allItemFacets + .groupBy { it.category } + .entries + .sortedBy { this.template.itemAmountConstraintProvider.getAllocationPriority(it.key) } + + for ((_, facetsInCategory) in facetsGroupedByCategory) { + for (facet in facetsInCategory.sortedDescending()) { + val satisfactionStatus = this.constraintEnforcer.getSatisfactionStatus(facet) + + when (satisfactionStatus) { + SatisfactionStatus.NOT_SATISFIED -> { + this.constraintEnforcer.addItem(facet) + + usefulItems.add(facet.itemSlot) + } + SatisfactionStatus.SATISFIED -> {} + SatisfactionStatus.OVERSATURATED -> { + throw IllegalArgumentException("Oversaturated behavior is currently not implemented.") + } + } + } + } + } + + private fun generateSwaps( + itemDispenserRack: ItemDispenserRack, + usefulItems: HashSet + ): ArrayList { + val finishedSlots = HashSet() + + // Consider all slots that we aren't allowed to change as done. + finishedSlots.addAll(template.restrictions.getSlotsWithAtLeast(RestrictionType.FORBID_REPLACING)) - // We need to fill all hotbar slots with this item type. + val swaps: ArrayList = ArrayList() - // Use a descending sort order so that we can fill the slots with the best items first. - val prioritizedItemList = availableItems.sortedDescending() + for (wish in this.wishOrganizer.organizedWishes) { + // If a better wish was already fulfilled, skip this second wish. + if (wish.targetSlot in finishedSlots) { + continue + } + + val availableItem = itemDispenserRack.nextItemForGroup(wish.id) + + if (availableItem == null) { + continue + } + + finishedSlots.add(wish.targetSlot) + usefulItems.add(availableItem.itemSlot) + + // Move the item to the target slot if necessary. + if (availableItem.itemSlot != wish.targetSlot) { + swaps.add( + InventorySwap( + from = availableItem.itemSlot, + to = wish.targetSlot, + priority = availableItem.category.type.allocationPriority + ) + ) + } + } + return swaps + } + + /** + * Discovers all facets from [availableItems]. Filters out any slot that has been restricted + */ + private fun discoverItemFacets(): List { + val categorizer = ItemCategorization(availableItems) - // Decide where the items should go. - val requiredMoves = - this.packer.packItems( - itemsToFillIn = prioritizedItemList, - hotbarSlotsToFill = hotbarSlotsToFill, - contraintProvider = this, - forbiddenSlots = this.template.forbiddenSlots, - forbiddenSlotsToFill = this.template.forbiddenSlotsToFill - ) + val availableItemFacets = availableItems.flatMap { categorizer.getItemFacets(it).asIterable() } - this.hotbarSwaps.addAll(requiredMoves) + return availableItemFacets } private fun groupItemsByType(): HashMap> { @@ -116,62 +155,4 @@ class CleanupPlanGenerator( return itemsByType } - - override fun getSatisfactionStatus(item: ItemFacet): ItemPacker.ItemAmountContraintProvider.SatisfactionStatus { - val constraints = this.template.itemAmountConstraintProvider(item) - - constraints.sortBy { it.group.priority } - - for (constraintInfo in constraints) { - val currentCount = this.currentLimit[constraintInfo.group] ?: 0 - - if (currentCount > constraintInfo.group.acceptableRange.last) { - return ItemPacker.ItemAmountContraintProvider.SatisfactionStatus.OVERSATURATED - } else if (currentCount < constraintInfo.group.acceptableRange.first) { - return ItemPacker.ItemAmountContraintProvider.SatisfactionStatus.NOT_SATISFIED - } - } - - return ItemPacker.ItemAmountContraintProvider.SatisfactionStatus.SATISFIED - } - - override fun addItem(item: ItemFacet) { - val constraints = this.template.itemAmountConstraintProvider(item) - - for (constraintInfo in constraints) { - val current = this.currentLimit.getOrDefault(constraintInfo.group, 0) - - this.currentLimit[constraintInfo.group] = current + constraintInfo.amountAddedByItem - } - } -} - -class CleanupPlanPlacementTemplate( - /** - * Contains requests for each slot (e.g. Slot 1 -> SWORD, Slot 8 -> BLOCK, etc.) - */ - val slotContentMap: Map, - /** - * A function which provides constraint groups for each item category and the number which the item counts against - * the given constraint. More info on how constraints work at [ItemNumberContraintGroup]. - */ - val itemAmountConstraintProvider: (ItemFacet) -> ArrayList, - /** - * If false, slots which also contains items of that category, those items are not replaced with other items. - */ - val isGreedy: Boolean, - val forbiddenSlots: Set, - val forbiddenSlotsToFill: Set -) - -enum class ItemSlotType { - HOTBAR, - OFFHAND, - ARMOR, - INVENTORY, - - /** - * e.g. chests - */ - CONTAINER, } diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/CleanupPlanTemplate.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/CleanupPlanTemplate.kt new file mode 100644 index 00000000000..308f3795cf3 --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/CleanupPlanTemplate.kt @@ -0,0 +1,85 @@ +package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner + +import net.ccbluex.liquidbounce.utils.inventory.ItemSlot + +class CleanupPlanTemplate( + /** + * Contains requests for each slot (e.g. Slot 1 -> SWORD, Slot 8 -> BLOCK, etc.) + */ + val slotContentMap: Map, + /** + * A function which provides constraint groups for each item category and the number which the item counts against + * the given constraint. More info on how constraints work at [ItemNumberContraintGroup]. + */ + val itemAmountConstraintProvider: ItemAmountConstraintProvider, + /** + * See [CleanupPlanRestrictions] + */ + val restrictions: CleanupPlanRestrictions, +) { + + class CleanupPlanSlotContent( + /** + * Content wishes for the target slot. + * + * ## Example: + * - Configuration for the slot: `[(sword), (snowball, egg), (apple)]` + * - Available items: `[1x sword, 3x snowball, 16x egg, 64x apple]` + * + * Behaviour: + * 1. The slot would be filled in + * 2. If the sword wasn't available, the next wish is considered. + * So it searches for the best snowball or egg in the list. + * Since 16 eggs are better than 3 snowballs, it will prefer those. + * 3. If the eggs weren't available or the snowballs were more, it would fill the slot with the snowball stack. + * 4. If no eggs and snowballs are available either, the apples would be filled in. + */ + val slotContentPreferences: List, + val priority: Int, + ) + + data class SlotContentPreference( + val itemType: GenericItemType, + val subtypes: Set = setOf(Unit), + ) + + /** + * Contains all information about what the inv cleaner is *not allowed* to do. + */ + class CleanupPlanRestrictions( + private val slotRestrictionMap: Map, + ) { + + fun getRestrictionFor(slot: ItemSlot): RestrictionType { + return this.slotRestrictionMap.getOrDefault(slot, RestrictionType.NONE) + } + + fun getSlotsWithAtLeast(type: RestrictionType): List { + return this.slotRestrictionMap.entries + .filter { it.value >= type } + .map { it.key } + } + + enum class RestrictionType { + NONE, + + /** + * Forbids the inventory cleaner from replacing the item in that slot with another item according to + * the current template. + * The inventory cleaner may still decide that the current content of the slot is useless and throw it out. + * + * Used for preventing the replacement of items that + * [net.ccbluex.liquidbounce.features.module.modules.player.offhand.ModuleOffhand] placed in the offhand. + */ + FORBID_REPLACING, + + /** + * Prevents the invcleaner from touching those slots at all. + * + * This used to be user-configurable for specific hotbar slots. + * Currently, this is used to prevent inv cleaner from tampering with the armor slots. + */ + FORBID_TAMPERING + } + } +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemAmountConstraintProvider.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemAmountConstraintProvider.kt new file mode 100644 index 00000000000..41f170ea1ca --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemAmountConstraintProvider.kt @@ -0,0 +1,32 @@ +package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner + +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items.ItemFacet + +interface ItemAmountConstraintProvider { + fun getConstraints(item: ItemFacet): ArrayList + + /** + * Returns the priority of the given item category. + * Categories with values are processed first. + * + * This is useful when it comes to finding the minimal number of items required to fulfill the constraints. + * For example, if the constraints were `egg -> 64, egg, snowball -> 32`, it would be important to process the eggs + * first so that no snowballs are kept when having > 32 eggs. + */ + fun getAllocationPriority(itemGroup: ItemCategory): Int + + /** + * Filters out not applying default configurations. + * + * See [ItemConstraintInfo.default] for further information on that. + */ + fun getApplyingConstraints(item: ItemFacet): ArrayList { + val constraints = getConstraints(item) + + if (constraints.any { !it.default }) { + constraints.removeIf { it.default } + } + + return constraints + } +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemCategorization.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemCategorization.kt index 030529dd496..605003b15ab 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemCategorization.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemCategorization.kt @@ -18,32 +18,35 @@ */ package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner -import net.ccbluex.liquidbounce.config.types.NamedChoice import net.ccbluex.liquidbounce.features.module.modules.combat.autoarmor.ArmorEvaluation import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items.* import net.ccbluex.liquidbounce.features.module.modules.world.scaffold.ScaffoldBlockItemSelection import net.ccbluex.liquidbounce.utils.inventory.ItemSlot +import net.ccbluex.liquidbounce.utils.inventory.ItemSlot.ItemSlotType import net.ccbluex.liquidbounce.utils.inventory.VirtualItemSlot import net.ccbluex.liquidbounce.utils.item.* import net.ccbluex.liquidbounce.utils.kotlin.Priority import net.ccbluex.liquidbounce.utils.sorting.compareByCondition import net.minecraft.entity.EquipmentSlot -import net.minecraft.fluid.LavaFluid -import net.minecraft.fluid.WaterFluid import net.minecraft.item.* -import java.util.function.Predicate val PREFER_ITEMS_IN_HOTBAR: Comparator = compareByCondition(ItemFacet::isInHotbar) val STABILIZE_COMPARISON: Comparator = Comparator.comparingInt { it.itemStack.hashCode() } + val PREFER_BETTER_DURABILITY: Comparator = Comparator.comparingInt { it.itemStack.maxDamage - it.itemStack.damage } -data class ItemCategory(val type: ItemType, val subtype: Int) +val DEFAULT_TIE_BREAK: Array> = arrayOf( + PREFER_ITEMS_IN_HOTBAR, + STABILIZE_COMPARISON, +) + +data class ItemCategory(val type: GenericItemType, val subtype: Any = Unit) -enum class ItemType( +enum class GenericItemType( val oneIsSufficient: Boolean, /** * Higher priority means the item category is filled in first. @@ -55,76 +58,35 @@ enum class ItemType( * ## Used values * - Specialization (see above): 10 per level */ - val allocationPriority: Priority = Priority.NORMAL, - /** - * The user maybe wants to filter the items by a specific type. But the we don't need all versions of the item. - * To stop the invcleaner from keeping items of every type, we can specify what function a specific item serves. - * If that function is already served, we can just ignore it. - */ - val providedFunction: ItemFunction? = null + val allocationPriority: Priority = Priority.NORMAL ) { ARMOR(true, allocationPriority = Priority.IMPORTANT_FOR_PLAYER_LIFE), - SWORD(true, allocationPriority = Priority.IMPORTANT_FOR_USAGE_3, providedFunction = ItemFunction.WEAPON_LIKE), - WEAPON(true, allocationPriority = Priority.IMPORTANT_FOR_USAGE_2, providedFunction = ItemFunction.WEAPON_LIKE), + SWORD(true, allocationPriority = Priority.IMPORTANT_FOR_USAGE_3), + WEAPON(true, allocationPriority = Priority.IMPORTANT_FOR_USAGE_2), BOW(true), CROSSBOW(true), ARROW(true), TOOL(true, allocationPriority = Priority.IMPORTANT_FOR_USAGE_1), - ROD(true), THROWABLE(false), - SHIELD(true), FOOD(false), - BUCKET(false), - PEARL(false, allocationPriority = Priority.IMPORTANT_FOR_USAGE_1), - GAPPLE(false, allocationPriority = Priority.IMPORTANT_FOR_USAGE_1), POTION(false), BLOCK(false), - NONE(false), + /** + * Represents any item. Every item in the inventory has this type. + */ + ANY_ITEM(true), } enum class ItemFunction { WEAPON_LIKE, - FOOD, -} -enum class ItemSortChoice( - override val choiceName: String, - val category: ItemCategory?, /** - * This is the function that is used for the greedy check. - * - * IF IT WAS IMPLEMENTED + * Crossbows and bows. */ - val satisfactionCheck: Predicate? = null, -) : NamedChoice { - SWORD("Sword", ItemCategory(ItemType.SWORD, 0)), - WEAPON("Weapon", ItemCategory(ItemType.WEAPON, 0)), - BOW("Bow", ItemCategory(ItemType.BOW, 0)), - CROSSBOW("Crossbow", ItemCategory(ItemType.CROSSBOW, 0)), - AXE("Axe", ItemCategory(ItemType.TOOL, 0)), - PICKAXE("Pickaxe", ItemCategory(ItemType.TOOL, 1)), - ROD("Rod", ItemCategory(ItemType.ROD, 0)), - SHIELD("Shield", ItemCategory(ItemType.SHIELD, 0)), - WATER("Water", ItemCategory(ItemType.BUCKET, 0)), - LAVA("Lava", ItemCategory(ItemType.BUCKET, 1)), - MILK("Milk", ItemCategory(ItemType.BUCKET, 2)), - PEARL("Pearl", ItemCategory(ItemType.PEARL, 0), { it.item == Items.ENDER_PEARL }), - GAPPLE( - "Gapple", - ItemCategory(ItemType.GAPPLE, 0), - { it.item == Items.GOLDEN_APPLE || it.item == Items.ENCHANTED_GOLDEN_APPLE }, - ), - FOOD("Food", ItemCategory(ItemType.FOOD, 0), { it.foodComponent != null }), - POTION("Potion", ItemCategory(ItemType.POTION, 0)), - BLOCK("Block", ItemCategory(ItemType.BLOCK, 0), { it.item is BlockItem }), - THROWABLES("Throwables", ItemCategory(ItemType.THROWABLE, 0)), - IGNORE("Ignore", null), - NONE("None", null), + BOW_LIKE, + FOOD, } -/** - * @param expectedFullArmor what is the expected armor material when we have full armor (full iron, full dia, etc.) - */ class ItemCategorization( availableItems: List, ) { @@ -147,9 +109,9 @@ class ItemCategorization( } /** - * Sometimes there are situations where armor pieces are not the best ones with the current armor, but become + * Sometimes there are situations where armor pieces aren’t the best ones with the current armor, but become * the best ones as soon as we upgrade one of the other armor pieces. - * In those cases we don't want to miss out on this armor piece in the future thus we keep it. + * In those cases, we don't want to miss out on this armor piece in the future, thus we keep it. */ private val futureArmorToKeep: List private val armorComparator: ArmorComparator @@ -180,32 +142,24 @@ class ItemCategorization( return emptyArray() } - val specificItemFacets: Array = when (val item = slot.itemStack.item) { + val item = slot.itemStack.item + + val specificItemFacets: Array = when (item) { // Treat animal armor as a normal item - is AnimalArmorItem -> arrayOf(ItemFacet(slot)) is ArmorItem -> arrayOf(ArmorItemFacet(slot, this.futureArmorToKeep, this.armorComparator)) is SwordItem -> arrayOf(SwordItemFacet(slot)) is BowItem -> arrayOf(BowItemFacet(slot)) is CrossbowItem -> arrayOf(CrossbowItemFacet(slot)) - is ArrowItem -> arrayOf(ArrowItemFacet(slot)) + is ArrowItem -> arrayOf(PrimitiveItemFacet(slot, ItemCategory(GenericItemType.ARROW))) is MiningToolItem -> arrayOf(MiningToolItemFacet(slot)) - is FishingRodItem -> arrayOf(RodItemFacet(slot)) - is ShieldItem -> arrayOf(ShieldItemFacet(slot)) is BlockItem -> { - if (ScaffoldBlockItemSelection.isValidBlock(slot.itemStack) - && !ScaffoldBlockItemSelection.isBlockUnfavourable(slot.itemStack) - ) { + val isUsableBlock = (ScaffoldBlockItemSelection.isValidBlock(slot.itemStack) + && !ScaffoldBlockItemSelection.isBlockUnfavourable(slot.itemStack)) + + if (isUsableBlock) { arrayOf(BlockItemFacet(slot)) } else { - arrayOf(ItemFacet(slot)) - } - } - Items.MILK_BUCKET -> arrayOf(PrimitiveItemFacet(slot, ItemCategory(ItemType.BUCKET, 2))) - is BucketItem -> { - when (item.fluid) { - is WaterFluid -> arrayOf(PrimitiveItemFacet(slot, ItemCategory(ItemType.BUCKET, 0))) - is LavaFluid -> arrayOf(PrimitiveItemFacet(slot, ItemCategory(ItemType.BUCKET, 1))) - else -> arrayOf(PrimitiveItemFacet(slot, ItemCategory(ItemType.BUCKET, 3))) + emptyArray() } } is PotionItem -> { @@ -216,33 +170,25 @@ class ItemCategorization( if (areAllEffectsGood) { arrayOf(PotionItemFacet(slot)) } else { - arrayOf(ItemFacet(slot)) + emptyArray() } } - is EnderPearlItem -> arrayOf(PrimitiveItemFacet(slot, ItemCategory(ItemType.PEARL, 0))) - Items.GOLDEN_APPLE -> { - arrayOf( - FoodItemFacet(slot), - PrimitiveItemFacet(slot, ItemCategory(ItemType.GAPPLE, 0)), - ) - } - Items.ENCHANTED_GOLDEN_APPLE -> { - arrayOf( - FoodItemFacet(slot), - PrimitiveItemFacet(slot, ItemCategory(ItemType.GAPPLE, 0), 1), - ) - } Items.SNOWBALL, Items.EGG, Items.WIND_CHARGE -> arrayOf(ThrowableItemFacet(slot)) else -> { if (slot.itemStack.isFood) { arrayOf(FoodItemFacet(slot)) } else { - arrayOf(ItemFacet(slot)) + emptyArray() } } } - // Everything could be a weapon (i.e. a stick with Knochback II should be considered a weapon) - return specificItemFacets + WeaponItemFacet(slot) + val commonFacets = listOfNotNull( + PrimitiveItemFacet(slot, ItemCategory(GenericItemType.ANY_ITEM, item)), + // Everything could be a weapon (i.e. a stick with Knockback II should be preferred over a stick) + WeaponItemFacet.createIfUsefulAsWeapon(slot) + ) + + return specificItemFacets + commonFacets } } diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemDispenserRack.kt b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemDispenserRack.kt new file mode 100644 index 00000000000..5fb80aabac9 --- /dev/null +++ b/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/ItemDispenserRack.kt @@ -0,0 +1,45 @@ +package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner + +import net.ccbluex.liquidbounce.features.module.modules.player.invcleaner.items.ItemFacet +import net.ccbluex.liquidbounce.utils.inventory.ItemSlot + +class ItemDispenserRack(wishOrganizer: WishOrganizer, itemFacets: List) { + private val dispensersForType: Map + private val alreadyDispensedItemSlots = HashSet() + + init { + val wishGroupAvailableFacetMap = HashMap>() + + for (facet in itemFacets) { + val wishGroupsForFacet = wishOrganizer.itemCategoryWishGroupMap[facet.category] ?: continue + + for (id in wishGroupsForFacet) { + wishGroupAvailableFacetMap.computeIfAbsent(id) { ArrayList() }.add(facet) + } + } + + wishGroupAvailableFacetMap.values.forEach { facetList -> facetList.sortDescending() } + + this.dispensersForType = wishGroupAvailableFacetMap.mapValues { ItemDispenser(it.value) } + } + + fun nextItemForGroup(id: WishOrganizer.WishItemGroupId) = this.dispensersForType[id]?.nextItem() + + private inner class ItemDispenser(itemList: List) { + private val itemListIterable: Iterator = itemList.iterator() + + fun nextItem(): ItemFacet? { + while (this.itemListIterable.hasNext()) { + val currentItem = this.itemListIterable.next() + + // Check if this item slot has already been dispensed. + // This is possible as an item might appear in multiple dispensers. + if (alreadyDispensedItemSlots.add(currentItem.itemSlot)) { + return currentItem + } + } + + return null + } + } +} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/features/module/modules/player/invcleaner/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 {