From 563d3e4230c22dc76189e51f65e94b23412b352a Mon Sep 17 00:00:00 2001
From: kroune <123553746+kroune@users.noreply.github.com>
Date: Sat, 24 Aug 2024 23:15:47 +0300
Subject: [PATCH 1/2] upload

---
 src-theme/src/integration/types.ts            |   7 +
 .../setting/common/GenericSetting.svelte      |   5 +-
 .../routes/clickgui/setting/items/Item.svelte |  52 +++++++
 .../clickgui/setting/items/ItemSetting.svelte |  93 ++++++++++++
 .../clickgui/setting/items/VirtualList.svelte | 137 ++++++++++++++++++
 .../player/invcleaner/CleanupPlanGenerator.kt |  17 ++-
 .../modules/player/invcleaner/ItemPacker.kt   |   4 +-
 .../invcleaner/ModuleInventoryCleaner.kt      |  43 ++++++
 8 files changed, 354 insertions(+), 4 deletions(-)
 create mode 100644 src-theme/src/routes/clickgui/setting/items/Item.svelte
 create mode 100644 src-theme/src/routes/clickgui/setting/items/ItemSetting.svelte
 create mode 100644 src-theme/src/routes/clickgui/setting/items/VirtualList.svelte

diff --git a/src-theme/src/integration/types.ts b/src-theme/src/integration/types.ts
index 3a6cfd149c3..2c86cea7174 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
+    | ItemsSetting
     | KeySetting
     | BooleanSetting
     | FloatSetting
@@ -35,6 +36,12 @@ export interface BlocksSetting {
     value: string[];
 }
 
+export interface ItemsSetting {
+    valueType: string;
+    name: string;
+    value: string;
+}
+
 export interface TextSetting {
     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 ef5b1b6447c..013316f217f 100644
--- a/src-theme/src/routes/clickgui/setting/common/GenericSetting.svelte
+++ b/src-theme/src/routes/clickgui/setting/common/GenericSetting.svelte
@@ -12,6 +12,7 @@
     import ColorSetting from "../ColorSetting.svelte";
     import TextSetting from "../TextSetting.svelte";
     import KeySetting from "../KeySetting.svelte";
+    import ItemsSetting from "../items/ItemSetting.svelte";
     import BlocksSetting from "../blocks/BlocksSetting.svelte";
     import {slide} from "svelte/transition";
     import {onMount} from "svelte";
@@ -56,6 +57,8 @@
             <TextSetting bind:setting={setting} on:change/>
         {:else if setting.valueType === "KEY"}
             <KeySetting bind:setting={setting} on:change/>
+        {:else if setting.valueType === "ITEMS"}
+            <ItemsSetting bind:setting={setting} on:change/>
         {:else if setting.valueType === "BLOCKS"}
             <BlocksSetting bind:setting={setting} on:change/>
         {:else if setting.valueType === "TEXT_ARRAY"}
@@ -64,4 +67,4 @@
             <div style="color: white">Unsupported setting {setting.valueType}</div>
         {/if}
     </div>
-{/if}
\ No newline at end of file
+{/if}
diff --git a/src-theme/src/routes/clickgui/setting/items/Item.svelte b/src-theme/src/routes/clickgui/setting/items/Item.svelte
new file mode 100644
index 00000000000..d3449462362
--- /dev/null
+++ b/src-theme/src/routes/clickgui/setting/items/Item.svelte
@@ -0,0 +1,52 @@
+<script lang="ts">
+    import {REST_BASE} from "../../../../integration/host";
+    import {createEventDispatcher} from "svelte";
+
+    const dispatch = createEventDispatcher<{
+        toggle: { identifier: string, enabled: boolean }
+    }>();
+
+    export let identifier: string;
+    export let name: string;
+    export let enabled: boolean;
+</script>
+
+<!-- svelte-ignore a11y-no-static-element-interactions -->
+<!-- svelte-ignore a11y-click-events-have-key-events -->
+<div class="item" on:click={() => dispatch("toggle", {enabled: !enabled, identifier})}>
+    <img class="icon" src="{REST_BASE}/api/v1/client/resource/itemTexture?id={identifier}" alt={identifier}/>
+    <div class="name">{name}</div>
+    <div class="tick">
+        {#if enabled}
+            <img src="img/clickgui/icon-tick-checked.svg" alt="enabled">
+        {:else}
+            <img src="img/clickgui/icon-tick.svg" alt="disabled">
+        {/if}
+    </div>
+</div>
+
+<style lang="scss">
+  @import "../../../../colors.scss";
+
+  .item {
+    display: grid;
+    grid-template-columns: max-content 1fr max-content;
+    align-items: center;
+    column-gap: 5px;
+    cursor: pointer;
+    margin: 2px 5px 2px 0;
+  }
+
+  .icon {
+    height: 25px;
+    width: 25px;
+  }
+
+  .name {
+    font-size: 12px;
+    color: $clickgui-text-color;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    overflow: hidden;
+  }
+</style>
diff --git a/src-theme/src/routes/clickgui/setting/items/ItemSetting.svelte b/src-theme/src/routes/clickgui/setting/items/ItemSetting.svelte
new file mode 100644
index 00000000000..c26bcc6ee83
--- /dev/null
+++ b/src-theme/src/routes/clickgui/setting/items/ItemSetting.svelte
@@ -0,0 +1,93 @@
+<script lang="ts">
+    import {createEventDispatcher, onMount} from "svelte";
+    import type {ItemsSetting, ModuleSetting} from "../../../../integration/types";
+    import {getRegistries} from "../../../../integration/rest";
+    import Item from "./Item.svelte";
+    import VirtualList from "./VirtualList.svelte";
+    import {convertToSpacedString, spaceSeperatedNames} from "../../../../theme/theme_config";
+
+    export let setting: ModuleSetting;
+
+    const cSetting = setting as ItemsSetting;
+
+    interface Item {
+        name: string;
+        identifier: string;
+    }
+
+    const dispatch = createEventDispatcher();
+    let items: Item[] = [];
+    let renderedItems: Item[] = items;
+    let searchQuery = "";
+
+    $: {
+        let filteredItems = items;
+        if (searchQuery) {
+            filteredItems = filteredItems.filter(b => b.name.toLowerCase().includes(searchQuery.toLowerCase()));
+        }
+        renderedItems = filteredItems;
+    }
+
+    onMount(async () => {
+        let i = (await getRegistries()).items;
+
+        if (i !== undefined) {
+            items = i.sort((a, b) => a.identifier.localeCompare(b.identifier));
+        }
+    });
+
+    function handleItemToggle(e: CustomEvent<{ identifier: string, enabled: boolean }>) {
+        console.log(e);
+        if (e.detail.enabled) {
+            cSetting.value = [...cSetting.value, e.detail.identifier];
+        } else {
+            cSetting.value = cSetting.value.filter(b => b !== e.detail.identifier);
+        }
+
+        setting = { ...cSetting };
+        dispatch("change");
+    }
+</script>
+
+<div class="setting">
+    <div class="name">{$spaceSeperatedNames ? convertToSpacedString(cSetting.name) : cSetting.name}</div>
+    <input type="text" placeholder="Search" class="search-input" bind:value={searchQuery} spellcheck="false">
+    <div class="results">
+        <VirtualList items={renderedItems} let:item>
+            <Item identifier={item.identifier} name={item.name} enabled={cSetting.value.includes(item.identifier)} on:toggle={handleItemToggle}/>
+        </VirtualList>
+    </div>
+</div>
+
+<style lang="scss">
+  @import "../../../../colors.scss";
+
+  .setting {
+    padding: 7px 0;
+  }
+
+  .results {
+    height: 200px;
+    overflow-y: auto;
+    overflow-x: hidden;
+  }
+
+  .name {
+    color: $clickgui-text-color;
+    font-size: 12px;
+    font-weight: 500;
+    margin-bottom: 5px;
+  }
+
+  .search-input {
+    width: 100%;
+    border: none;
+    border-bottom: solid 1px $accent-color;
+    font-family: "Inter", sans-serif;
+    font-size: 12px;
+    padding: 5px;
+    color: $clickgui-text-color;
+    margin-bottom: 5px;
+    background-color: rgba($clickgui-base-color, .36);
+  }
+</style>
diff --git a/src-theme/src/routes/clickgui/setting/items/VirtualList.svelte b/src-theme/src/routes/clickgui/setting/items/VirtualList.svelte
new file mode 100644
index 00000000000..d41b0e5d5cf
--- /dev/null
+++ b/src-theme/src/routes/clickgui/setting/items/VirtualList.svelte
@@ -0,0 +1,137 @@
+<!-- Adapted from https://github.com/sveltejs/svelte-virtual-list -->
+<script lang="js">
+    import {onMount, tick} from 'svelte';
+    // props
+    export let items;
+    export let height = '100%';
+    export let itemHeight = undefined;
+    // read-only, but visible to consumers via bind:start
+    export let start = 0;
+    export let end = 0;
+    // local state
+    let height_map = [];
+    let rows;
+    let viewport;
+    let contents;
+    let viewport_height = 0;
+    let visible;
+    let mounted;
+    let top = 0;
+    let bottom = 0;
+    let average_height;
+    $: visible = items.slice(start, end).map((data, i) => {
+        return { index: i + start, data };
+    });
+    // whenever `items` changes, invalidate the current heightmap
+    $: if (mounted) refresh(items, viewport_height, itemHeight);
+    async function refresh(items, viewport_height, itemHeight) {
+        const { scrollTop } = viewport;
+        await tick(); // wait until the DOM is up to date
+        let content_height = top - scrollTop;
+        let i = start;
+        while (content_height < viewport_height && i < items.length) {
+            let row = rows[i - start];
+            if (!row) {
+                end = i + 1;
+                await tick(); // render the newly visible row
+                row = rows[i - start];
+            }
+            const row_height = height_map[i] = itemHeight || row.offsetHeight;
+            content_height += row_height;
+            i += 1;
+        }
+        end = i;
+        const remaining = items.length - end;
+        average_height = (top + content_height) / end;
+        bottom = remaining * average_height;
+        height_map.length = items.length;
+
+        setTimeout(() => {
+            viewport.scrollTop = 0;
+        }, 100);
+    }
+    async function handle_scroll() {
+        const { scrollTop } = viewport;
+        const old_start = start;
+        for (let v = 0; v < rows.length; v += 1) {
+            height_map[start + v] = itemHeight || rows[v].offsetHeight;
+        }
+        let i = 0;
+        let y = 0;
+        while (i < items.length) {
+            const row_height = height_map[i] || average_height;
+            if (y + row_height > scrollTop) {
+                start = i;
+                top = y;
+                break;
+            }
+            y += row_height;
+            i += 1;
+        }
+        while (i < items.length) {
+            y += height_map[i] || average_height;
+            i += 1;
+            if (y > scrollTop + viewport_height) break;
+        }
+        end = i;
+        const remaining = items.length - end;
+        average_height = y / end;
+        while (i < items.length) height_map[i++] = average_height;
+        bottom = remaining * average_height;
+        // prevent jumping if we scrolled up into unknown territory
+        if (start < old_start) {
+            await tick();
+            let expected_height = 0;
+            let actual_height = 0;
+            for (let i = start; i < old_start; i +=1) {
+                if (rows[i - start]) {
+                    expected_height += height_map[i];
+                    actual_height += itemHeight || rows[i - start].offsetHeight;
+                }
+            }
+            const d = actual_height - expected_height;
+            viewport.scrollTo(0, scrollTop + d);
+        }
+        // TODO if we overestimated the space these
+        // rows would occupy we may need to add some
+        // more. maybe we can just call handle_scroll again?
+    }
+    // trigger initial refresh
+    onMount(() => {
+        rows = contents.getElementsByTagName('svelte-virtual-list-row');
+        mounted = true;
+    });
+</script>
+
+<style>
+    svelte-virtual-list-viewport {
+        position: relative;
+        overflow-y: auto;
+        -webkit-overflow-scrolling:touch;
+        display: block;
+    }
+    svelte-virtual-list-contents, svelte-virtual-list-row {
+        display: block;
+    }
+    svelte-virtual-list-row {
+        overflow: hidden;
+    }
+</style>
+
+<svelte-virtual-list-viewport
+        bind:this={viewport}
+        bind:offsetHeight={viewport_height}
+        on:scroll={handle_scroll}
+        style="height: {height};"
+>
+    <svelte-virtual-list-contents
+            bind:this={contents}
+            style="padding-top: {top}px; padding-bottom: {bottom}px;"
+    >
+        {#each visible as row (row.index)}
+            <svelte-virtual-list-row>
+                <slot item={row.data}>Missing template</slot>
+            </svelte-virtual-list-row>
+        {/each}
+    </svelte-virtual-list-contents>
+</svelte-virtual-list-viewport>
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 9c4133fd34b..7b29e4d76eb 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
@@ -19,7 +19,9 @@
 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.client.logger
 import net.ccbluex.liquidbounce.utils.item.isNothing
+import net.minecraft.item.Item
 
 class CleanupPlanGenerator(
     private val template: CleanupPlanPlacementTemplate,
@@ -58,6 +60,18 @@ class CleanupPlanGenerator(
             processItemCategory(category, availableItems)
         }
 
+        availableItems.forEach { slot ->
+            val limit = template.itemLimitPerItem[slot.itemStack.item] ?: Int.MAX_VALUE
+            var itemCount = packer.usefulItems.count { it.itemStack.item == slot.itemStack.item }
+            packer.usefulItems.removeIf {
+                if (itemCount > limit) {
+                    logger.info("removed item from useful items list")
+                    itemCount--
+                    return@removeIf true
+                }
+                false
+            }
+        }
         // We aren't allowed to touch those, so we just consider them as useful.
         packer.usefulItems.addAll(this.template.forbiddenSlots)
 
@@ -74,7 +88,7 @@ class CleanupPlanGenerator(
     ) {
         val maxItemCount =
             if (category.type.oneIsSufficient) {
-                if (this.packer.alreadyProviededFunctions.contains(category.type.providedFunction)) 0 else 1
+                if (this.packer.alreadyProvidedFunctions.contains(category.type.providedFunction)) 0 else 1
             } else {
                 template.itemLimitPerCategory[category] ?: Int.MAX_VALUE
             }
@@ -132,6 +146,7 @@ class CleanupPlanPlacementTemplate(
      * If an item is not in this map, there is no limit.
      */
     val itemLimitPerCategory: Map<ItemCategory, Int>,
+    val itemLimitPerItem: Map<Item, Int>,
     /**
      * If false, slots which also contains items of that category, those items are not replaced with other items.
      */
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
index 5e34ba6a742..e4901731f52 100644
--- 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
@@ -21,7 +21,7 @@ class ItemPacker {
      */
     val usefulItems = HashSet<ItemSlot>()
 
-    val alreadyProviededFunctions = HashSet<ItemFunction>()
+    val alreadyProvidedFunctions = HashSet<ItemFunction>()
 
     /**
      * Takes items from the [itemsToFillIn] list until it collected [maxItemCount] items is and [requiredStackCount]
@@ -61,7 +61,7 @@ class ItemPacker {
 
             usefulItems.add(filledInItemSlot)
 
-            alreadyProviededFunctions.addAll(filledInItem.providedItemFunctions)
+            alreadyProvidedFunctions.addAll(filledInItem.providedItemFunctions)
 
             currentItemCount += filledInItem.itemStack.count
             currentStackCount++
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 c768599eacb..937e7af6660 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
@@ -18,15 +18,21 @@
  */
 package net.ccbluex.liquidbounce.features.module.modules.player.invcleaner
 
+import net.ccbluex.liquidbounce.config.Value
 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.Module
+import net.ccbluex.liquidbounce.utils.client.logger
 import net.ccbluex.liquidbounce.utils.inventory.ClickInventoryAction
 import net.ccbluex.liquidbounce.utils.inventory.PlayerInventoryConstraints
 import net.ccbluex.liquidbounce.utils.inventory.findNonEmptySlotsInInventory
+import net.minecraft.block.Blocks
+import net.minecraft.item.Item
+import net.minecraft.item.Items
 import net.minecraft.screen.slot.SlotActionType
 import java.util.HashMap
+import kotlin.math.min
 
 /**
  * InventoryCleaner module
@@ -43,6 +49,42 @@ object ModuleInventoryCleaner : Module("InventoryCleaner", Category.PLAYER) {
 
     private val isGreedy by boolean("Greedy", true)
 
+    var itemLimits: Map<Item, Int> = mapOf()
+    val presentSettings: MutableList<Pair<Value<MutableList<Item>>, Value<Int>>> = mutableListOf()
+
+    private fun recount() {
+        val limits = mutableMapOf<Item, Int>()
+        presentSettings.forEach { (itemsValue, countValue) ->
+            val count = countValue.get()
+            itemsValue.get().forEach { item ->
+                limits[item].let {
+                    if (it == null) {
+                        limits[item] = count
+                    } else {
+                        limits[item] = min(count, it)
+                    }
+                }
+            }
+        }
+        logger.info("recount")
+        itemLimits = limits
+    }
+
+    private val addNewFilter by boolean("AddNewFilter", false).onChange {
+        val itemType: Value<MutableList<Item>> = items("Items", mutableListOf()).onChanged {
+            logger.info("changed xd")
+            recount()
+        }
+
+        val itemLimit: Value<Int> = int("ItemLimit", 0, 0..200).onChanged {
+            logger.info("changed xd2")
+            recount()
+        }
+        presentSettings.add(Pair(itemType, itemLimit))
+        recount()
+        false
+    }
+
     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)
@@ -87,6 +129,7 @@ object ModuleInventoryCleaner : Module("InventoryCleaner", Category.PLAYER) {
                     Pair(ItemSortChoice.THROWABLES.category!!, maxThrowables),
                     Pair(ItemCategory(ItemType.ARROW, 0), maxArrows),
                 ),
+                itemLimitPerItem = itemLimits,
                 forbiddenSlots = forbiddenSlots,
                 isGreedy = isGreedy,
             )

From 8ac0406f8067b861c06555dbce5c3ebe402adcfd Mon Sep 17 00:00:00 2001
From: kroune <123553746+kroune@users.noreply.github.com>
Date: Sun, 25 Aug 2024 00:39:57 +0300
Subject: [PATCH 2/2] update

---
 .../invcleaner/ModuleInventoryCleaner.kt      | 34 +++++++++++--------
 1 file changed, 20 insertions(+), 14 deletions(-)

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 937e7af6660..2f637a1328a 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
@@ -49,38 +49,44 @@ object ModuleInventoryCleaner : Module("InventoryCleaner", Category.PLAYER) {
 
     private val isGreedy by boolean("Greedy", true)
 
-    var itemLimits: Map<Item, Int> = mapOf()
-    val presentSettings: MutableList<Pair<Value<MutableList<Item>>, Value<Int>>> = mutableListOf()
+    private var itemLimits: Map<Item, Int> = mapOf()
+    private val presentSettings: MutableList<Pair<Value<MutableList<Item>>, Value<Int>>> = mutableListOf()
 
     private fun recount() {
         val limits = mutableMapOf<Item, Int>()
         presentSettings.forEach { (itemsValue, countValue) ->
             val count = countValue.get()
             itemsValue.get().forEach { item ->
-                limits[item].let {
-                    if (it == null) {
-                        limits[item] = count
-                    } else {
-                        limits[item] = min(count, it)
-                    }
+                val limitState = limits[item]
+                // we just follow the lowest filter
+                limits[item] = if (limitState == null) {
+                    count
+                } else {
+                    min(count, limitState)
                 }
             }
         }
-        logger.info("recount")
         itemLimits = limits
     }
 
+    @Suppress("UnusedPrivateProperty")
     private val addNewFilter by boolean("AddNewFilter", false).onChange {
-        val itemType: Value<MutableList<Item>> = items("Items", mutableListOf()).onChanged {
-            logger.info("changed xd")
+        val itemType: Value<MutableList<Item>> = items("ItemsToLimit", mutableListOf()).onChanged {
             recount()
         }
-
-        val itemLimit: Value<Int> = int("ItemLimit", 0, 0..200).onChanged {
-            logger.info("changed xd2")
+        val itemLimit: Value<Int> = int("MaxItemSlots", 0, 0..40).onChanged {
             recount()
         }
         presentSettings.add(Pair(itemType, itemLimit))
+        false
+    }
+
+    @Suppress("UnusedPrivateProperty")
+    private val deleteFilter by boolean("DeleteFilter", false).onChange {
+        listOf("ItemsToLimit", "MaxItemSlots").forEach { name ->
+            val index = inner.indexOfFirst { it.name == name }
+            inner.removeAt(index)
+        }
         recount()
         false
     }