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 }