feat(redstoner): rechercher et vider sans glissement dans le terminal #97
+80
-5
@@ -3489,7 +3489,7 @@ tasks.register("verifyMirrorLavenderDrawerRelease") {
|
||||
def drawerRecipe = file("redstoner/src/main/resources/data/redstoner/recipe/drawer.json")
|
||||
if (rootProject.pack_version != "26.2.0-alpha.221"
|
||||
|| rootProject.anotherworld_version != "0.0.0-alpha.55"
|
||||
|| rootProject.redstoner_version != "0.0.0-alpha.12"
|
||||
|| rootProject.redstoner_version != "0.0.0-alpha.13"
|
||||
|| release.pack_version != rootProject.pack_version || index.versionId != rootProject.pack_version
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.191"
|
||||
|| migration.source?.modules != [anotherworld: "0.0.0-alpha.48", redstoner: "0.0.0-alpha.9"]
|
||||
@@ -4086,6 +4086,8 @@ project(":redstoner") {
|
||||
"screen.redstoner.personal_computer.shutdown",
|
||||
"message.redstoner.personal_computer.atlas_marker_exists",
|
||||
"screen.redstoner.storage_terminal.title",
|
||||
"screen.redstoner.storage_terminal.search",
|
||||
"screen.redstoner.storage_terminal.no_results",
|
||||
"screen.redstoner.storage_terminal.summary",
|
||||
"screen.redstoner.storage_terminal.error.invalid_terminal",
|
||||
"screen.redstoner.storage_terminal.error.disconnected",
|
||||
@@ -4103,7 +4105,7 @@ project(":redstoner") {
|
||||
"src/main/resources/data/redstoner/recipe/controller.json"))
|
||||
def terminalRecipe = new JsonSlurper().parse(file(
|
||||
"src/main/resources/data/redstoner/recipe/storage_terminal.json"))
|
||||
if (project.version.toString() != "0.0.0-alpha.12"
|
||||
if (project.version.toString() != "0.0.0-alpha.13"
|
||||
|| manifest.custom?.sanctuary26?.data_version != 3
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 7
|
||||
|| terminalMigration.source?.pack_version != "26.2.0-alpha.217"
|
||||
@@ -4144,7 +4146,7 @@ project(":redstoner") {
|
||||
P: "anotherworld:silver_plate", G: "minecraft:tinted_glass",
|
||||
C: "redstoner:controller", T: "anotherworld:titanium_ingot"]
|
||||
|| terminalRecipe.result != [id: "redstoner:storage_terminal", count: 1]) {
|
||||
throw new GradleException("Red-Stoner alpha.12 Storage Terminal menu contract is incomplete")
|
||||
throw new GradleException("Red-Stoner alpha.13 Storage Terminal contract is incomplete")
|
||||
}
|
||||
if (file("src/main/resources/data/redstoner/recipe/personal_computer.json").exists()
|
||||
|| file("src/main/resources/data/redstoner/recipe/drawer.json").exists()
|
||||
@@ -6443,7 +6445,10 @@ tasks.register("verifyAlpha218Release") {
|
||||
"file = \"mods/${id}-${version}.jar\""
|
||||
}
|
||||
if (rootProject.pack_version != "26.2.0-alpha.221"
|
||||
|| currentModules.any { id, version -> rootProject.property("${id}_version") != version }
|
||||
|| rootProject.redstoner_version != "0.0.0-alpha.13"
|
||||
|| currentModules.findAll { id, version -> id != "redstoner" }.any {
|
||||
id, version -> rootProject.property("${id}_version") != version
|
||||
}
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.217"
|
||||
|| migration.source?.modules != sourceModules
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.218"
|
||||
@@ -6620,7 +6625,7 @@ tasks.register("verifyAlpha220Release") {
|
||||
def networking = file(
|
||||
"redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/RedStonerNetworking.java").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.221"
|
||||
|| rootProject.redstoner_version != "0.0.0-alpha.12"
|
||||
|| rootProject.redstoner_version != "0.0.0-alpha.13"
|
||||
|| migration.source?.pack_version != "26.2.0-alpha.219"
|
||||
|| migration.source?.modules != [redstoner: "0.0.0-alpha.11"]
|
||||
|| migration.target?.pack_version != "26.2.0-alpha.220"
|
||||
@@ -6775,6 +6780,75 @@ tasks.register("verifyAlpha221Release") {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("verifyStorageTerminalSearchFeature") {
|
||||
group = "verification"
|
||||
description = "Checks the unreleased Storage Terminal keyboard-search feature."
|
||||
inputs.file(file("pack/release.json"))
|
||||
inputs.file(file("pack/prism/modrinth.index.json"))
|
||||
inputs.file(file("pack/packwiz/pack.toml"))
|
||||
inputs.file(file("pack/packwiz/index.toml"))
|
||||
inputs.file(file("redstoner/src/main/resources/fabric.mod.json"))
|
||||
inputs.file(file("redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalMenu.java"))
|
||||
inputs.file(file("redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalNetwork.java"))
|
||||
inputs.file(file("redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalRules.java"))
|
||||
inputs.file(file("redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalLayout.java"))
|
||||
inputs.file(file("redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalSearch.java"))
|
||||
inputs.file(file("redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/gui/StorageTerminalScreen.java"))
|
||||
inputs.file(file("redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/RedStonerNetworking.java"))
|
||||
inputs.file(file("redstoner/src/test/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalSmoke.java"))
|
||||
doLast {
|
||||
def release = new JsonSlurper().parse(file("pack/release.json"))
|
||||
def prism = new JsonSlurper().parse(file("pack/prism/modrinth.index.json"))
|
||||
def packwiz = file("pack/packwiz/pack.toml").text
|
||||
def packwizIndex = file("pack/packwiz/index.toml").text
|
||||
def manifest = new JsonSlurper().parse(file("redstoner/src/main/resources/fabric.mod.json"))
|
||||
def menu = file(
|
||||
"redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalMenu.java").text
|
||||
def network = file(
|
||||
"redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalNetwork.java").text
|
||||
def rules = file(
|
||||
"redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalRules.java").text
|
||||
def layout = file(
|
||||
"redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalLayout.java").text
|
||||
def search = file(
|
||||
"redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalSearch.java").text
|
||||
def screen = file(
|
||||
"redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/client/gui/StorageTerminalScreen.java").text
|
||||
def networking = file(
|
||||
"redstoner/src/main/java/fr/koka99cab/sanctuary26/redstoner/network/RedStonerNetworking.java").text
|
||||
def smoke = file(
|
||||
"redstoner/src/test/java/fr/koka99cab/sanctuary26/redstoner/storage/StorageTerminalSmoke.java").text
|
||||
if (rootProject.pack_version != "26.2.0-alpha.221"
|
||||
|| rootProject.redstoner_version != "0.0.0-alpha.13"
|
||||
|| manifest.custom?.sanctuary26?.data_version != 3
|
||||
|| manifest.custom?.sanctuary26?.network_protocol != 7
|
||||
|| !rules.contains("MAX_SEARCH_LENGTH = 32")
|
||||
|| !rules.contains("compactCount(int count)")
|
||||
|| !layout.contains("Keeps terminal slot identities stable")
|
||||
|| !layout.contains("new StorageTerminalInventory.Entry(identity.representative(), 0)")
|
||||
|| !menu.contains("SEARCH_CODEPOINT_BUTTON_BASE")
|
||||
|| !menu.contains("isSearchCodePoint")
|
||||
|| !network.contains("StorageTerminalSearch.filter")
|
||||
|| !search.contains("Normalizer.Form.NFD")
|
||||
|| !search.contains("BuiltInRegistries.ITEM.getKey")
|
||||
|| !screen.contains("new EditBox")
|
||||
|| !screen.contains("setInitialFocus(searchBox)")
|
||||
|| !screen.contains("searchBox.setResponder(this::searchChanged)")
|
||||
|| !smoke.contains("verifySearch()")
|
||||
|| !smoke.contains("verifyStableLayout()")
|
||||
|| !networking.contains("PROTOCOL_VERSION = 7")
|
||||
|| networking.contains("storage_terminal_search")
|
||||
|| release.pack_version != "26.2.0-alpha.221"
|
||||
|| release.modrinth?.primary_file != "sanctuary-26.2.0-alpha.221.mrpack"
|
||||
|| prism.versionId != "26.2.0-alpha.221"
|
||||
|| !packwiz.contains('version = "26.2.0-alpha.221"')
|
||||
|| !packwizIndex.contains('file = "mods/redstoner-0.0.0-alpha.12.jar"')
|
||||
|| packwizIndex.contains('file = "mods/redstoner-0.0.0-alpha.13.jar"')) {
|
||||
throw new GradleException("The unreleased Storage Terminal keyboard-search feature is incomplete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("verifyWorkspace") {
|
||||
dependsOn tasks.named("verifyAlpha213Release")
|
||||
dependsOn tasks.named("verifyAlpha214Release")
|
||||
@@ -6785,6 +6859,7 @@ tasks.named("verifyWorkspace") {
|
||||
dependsOn tasks.named("verifyAlpha219Release")
|
||||
dependsOn tasks.named("verifyAlpha220Release")
|
||||
dependsOn tasks.named("verifyAlpha221Release")
|
||||
dependsOn tasks.named("verifyStorageTerminalSearchFeature")
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ mod_version=0.0.0-alpha.0
|
||||
module_lifecycle=standby
|
||||
ambiance_version=0.0.0-alpha.22
|
||||
ambiance_lifecycle=active
|
||||
redstoner_version=0.0.0-alpha.12
|
||||
redstoner_version=0.0.0-alpha.13
|
||||
redstoner_lifecycle=active
|
||||
anotherworld_version=0.0.0-alpha.55
|
||||
anotherworld_lifecycle=active
|
||||
|
||||
+51
-8
@@ -1,12 +1,15 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.client.gui;
|
||||
|
||||
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalMenu;
|
||||
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalRules;
|
||||
import fr.koka99cab.sanctuary26.redstoner.storage.StorageTerminalStatus;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
@@ -24,6 +27,9 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
private static final int SCROLL_X = 171;
|
||||
private static final int SCROLL_WIDTH = 4;
|
||||
private int requestedPage;
|
||||
private EditBox searchBox;
|
||||
private String searchValue = "";
|
||||
private String sentSearchValue = "";
|
||||
|
||||
public StorageTerminalScreen(StorageTerminalMenu menu, Inventory inventory, Component title) {
|
||||
super(menu, inventory, title, 176, 222);
|
||||
@@ -33,8 +39,14 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
@Override
|
||||
protected void init() {
|
||||
super.init();
|
||||
titleLabelX = 8;
|
||||
titleLabelY = 6;
|
||||
searchBox = addRenderableWidget(new EditBox(font, leftPos + 8, topPos + 2, 126, 14,
|
||||
Component.translatable("screen.redstoner.storage_terminal.search")));
|
||||
searchBox.setMaxLength(StorageTerminalRules.MAX_SEARCH_LENGTH);
|
||||
searchBox.setHint(Component.translatable("screen.redstoner.storage_terminal.search"));
|
||||
searchBox.setValue(searchValue);
|
||||
searchBox.setResponder(this::searchChanged);
|
||||
setInitialFocus(searchBox);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -47,11 +59,13 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
|
||||
@Override
|
||||
protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
graphics.text(font, title, titleLabelX, titleLabelY, 0xFF404040, false);
|
||||
Component pageLabel = Component.literal((menu.page() + 1) + "/" + menu.pageCount());
|
||||
graphics.text(font, pageLabel, 168 - font.width(pageLabel), titleLabelY, 0xFF404040, false);
|
||||
if (menu.status() != StorageTerminalStatus.READY) {
|
||||
graphics.centeredText(font, menu.statusMessage(), imageWidth / 2, 68, 0xFFAA3333);
|
||||
} else if (!searchValue.isBlank() && menu.totalEntries() == 0) {
|
||||
graphics.centeredText(font, Component.translatable(
|
||||
"screen.redstoner.storage_terminal.no_results"), imageWidth / 2, 68, 0xFF777777);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +75,7 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
if (slot.index < StorageTerminalMenu.TERMINAL_SLOT_COUNT && slot.hasItem()) {
|
||||
int total = menu.totalAt(slot.index);
|
||||
if (total > 1) {
|
||||
String label = compactCount(total);
|
||||
String label = StorageTerminalRules.compactCount(total);
|
||||
graphics.text(font, label, slot.x + 17 - font.width(label), slot.y + 9,
|
||||
0xFFFFFFFF, true);
|
||||
}
|
||||
@@ -111,6 +125,15 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
return super.mouseClicked(event, doubleClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keyPressed(KeyEvent event) {
|
||||
if (searchBox != null && searchBox.isFocused() && event.key() != 256) {
|
||||
searchBox.keyPressed(event);
|
||||
return true;
|
||||
}
|
||||
return super.keyPressed(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void containerTick() {
|
||||
requestedPage = menu.page();
|
||||
@@ -125,6 +148,31 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
}
|
||||
}
|
||||
|
||||
private void searchChanged(String value) {
|
||||
String previous = sentSearchValue;
|
||||
searchValue = value;
|
||||
requestedPage = 0;
|
||||
if (value.startsWith(previous)
|
||||
&& value.codePointCount(previous.length(), value.length()) == 1) {
|
||||
sendMenuButton(StorageTerminalMenu.SEARCH_CODEPOINT_BUTTON_BASE
|
||||
+ value.codePointAt(previous.length()));
|
||||
} else if (previous.startsWith(value)
|
||||
&& previous.codePointCount(value.length(), previous.length()) == 1) {
|
||||
sendMenuButton(StorageTerminalMenu.SEARCH_BACKSPACE_BUTTON);
|
||||
} else {
|
||||
sendMenuButton(StorageTerminalMenu.SEARCH_CLEAR_BUTTON);
|
||||
value.codePoints().forEach(codePoint -> sendMenuButton(
|
||||
StorageTerminalMenu.SEARCH_CODEPOINT_BUTTON_BASE + codePoint));
|
||||
}
|
||||
sentSearchValue = value;
|
||||
}
|
||||
|
||||
private void sendMenuButton(int buttonId) {
|
||||
if (minecraft != null && minecraft.gameMode != null) {
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId, buttonId);
|
||||
}
|
||||
}
|
||||
|
||||
private void extractScrollBar(GuiGraphicsExtractor graphics) {
|
||||
int x = leftPos + SCROLL_X;
|
||||
int y = topPos + TERMINAL_TOP;
|
||||
@@ -137,9 +185,4 @@ public final class StorageTerminalScreen extends AbstractContainerScreen<Storage
|
||||
graphics.outline(x, y, SCROLL_WIDTH, TERMINAL_HEIGHT, 0xFF373737);
|
||||
}
|
||||
|
||||
private static String compactCount(int count) {
|
||||
if (count >= 1_000_000) return (count / 1_000_000) + "m";
|
||||
if (count >= 10_000) return (count / 1_000) + "k";
|
||||
return Integer.toString(count);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-6
@@ -12,12 +12,12 @@ public final class StorageTerminalInventory {
|
||||
}
|
||||
|
||||
public static List<Entry> aggregate(List<? extends Container> containers) {
|
||||
Map<StackKey, MutableEntry> grouped = new LinkedHashMap<>();
|
||||
Map<StackIdentity, MutableEntry> grouped = new LinkedHashMap<>();
|
||||
for (Container container : containers) {
|
||||
for (int slot = 0; slot < container.getContainerSize(); slot++) {
|
||||
ItemStack stack = container.getItem(slot);
|
||||
if (stack.isEmpty()) continue;
|
||||
StackKey key = new StackKey(stack);
|
||||
StackIdentity key = identity(stack);
|
||||
MutableEntry entry = grouped.computeIfAbsent(key, ignored -> new MutableEntry(key.stack));
|
||||
entry.total = saturatedAdd(entry.total, stack.getCount());
|
||||
}
|
||||
@@ -84,10 +84,14 @@ public final class StorageTerminalInventory {
|
||||
public record Entry(ItemStack stack, int total) {
|
||||
public Entry {
|
||||
stack = stack == null || stack.isEmpty() ? ItemStack.EMPTY : stack.copyWithCount(1);
|
||||
total = stack.isEmpty() ? 0 : Math.max(1, total);
|
||||
total = stack.isEmpty() ? 0 : Math.max(0, total);
|
||||
}
|
||||
}
|
||||
|
||||
static StackIdentity identity(ItemStack stack) {
|
||||
return new StackIdentity(stack);
|
||||
}
|
||||
|
||||
private static final class MutableEntry {
|
||||
private final ItemStack stack;
|
||||
private int total;
|
||||
@@ -97,11 +101,11 @@ public final class StorageTerminalInventory {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class StackKey {
|
||||
static final class StackIdentity {
|
||||
private final ItemStack stack;
|
||||
private final int hash;
|
||||
|
||||
private StackKey(ItemStack stack) {
|
||||
private StackIdentity(ItemStack stack) {
|
||||
this.stack = stack.copyWithCount(1);
|
||||
this.hash = ItemStack.hashItemAndComponents(this.stack);
|
||||
}
|
||||
@@ -113,7 +117,12 @@ public final class StorageTerminalInventory {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return other instanceof StackKey key && ItemStack.isSameItemSameComponents(stack, key.stack);
|
||||
return other instanceof StackIdentity identity
|
||||
&& ItemStack.isSameItemSameComponents(stack, identity.stack);
|
||||
}
|
||||
|
||||
ItemStack representative() {
|
||||
return stack.copyWithCount(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.storage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Keeps terminal slot identities stable for the lifetime of one search layout. */
|
||||
final class StorageTerminalLayout {
|
||||
private static final StorageTerminalInventory.Entry EMPTY_ENTRY =
|
||||
new StorageTerminalInventory.Entry(ItemStack.EMPTY, 0);
|
||||
private final List<StorageTerminalInventory.StackIdentity> order = new ArrayList<>();
|
||||
private Map<StorageTerminalInventory.StackIdentity, StorageTerminalInventory.Entry> liveByIdentity = Map.of();
|
||||
|
||||
void update(List<StorageTerminalInventory.Entry> liveEntries, boolean resetOrder) {
|
||||
Map<StorageTerminalInventory.StackIdentity, StorageTerminalInventory.Entry> live =
|
||||
new LinkedHashMap<>();
|
||||
for (StorageTerminalInventory.Entry entry : liveEntries) {
|
||||
live.put(StorageTerminalInventory.identity(entry.stack()), entry);
|
||||
}
|
||||
if (resetOrder) order.clear();
|
||||
Set<StorageTerminalInventory.StackIdentity> known = new HashSet<>(order);
|
||||
for (StorageTerminalInventory.StackIdentity identity : live.keySet()) {
|
||||
if (known.add(identity)) order.add(identity);
|
||||
}
|
||||
liveByIdentity = Map.copyOf(live);
|
||||
}
|
||||
|
||||
int size() {
|
||||
return order.size();
|
||||
}
|
||||
|
||||
List<StorageTerminalInventory.Entry> page(int requestedPage) {
|
||||
int start = StorageTerminalRules.pageStart(requestedPage, order.size());
|
||||
List<StorageTerminalInventory.Entry> entries = new ArrayList<>(StorageTerminalRules.PAGE_SIZE);
|
||||
for (int slot = 0; slot < StorageTerminalRules.PAGE_SIZE; slot++) {
|
||||
int orderedIndex = start + slot;
|
||||
if (orderedIndex >= order.size()) {
|
||||
entries.add(EMPTY_ENTRY);
|
||||
continue;
|
||||
}
|
||||
StorageTerminalInventory.StackIdentity identity = order.get(orderedIndex);
|
||||
StorageTerminalInventory.Entry live = liveByIdentity.get(identity);
|
||||
entries.add(live == null
|
||||
? new StorageTerminalInventory.Entry(identity.representative(), 0) : live);
|
||||
}
|
||||
return List.copyOf(entries);
|
||||
}
|
||||
}
|
||||
+56
-18
@@ -21,10 +21,13 @@ import net.minecraft.world.inventory.Slot;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** A server-authoritative, read-only aggregate view over the connected physical storage. */
|
||||
/** A server-authoritative aggregate view over the connected physical storage. */
|
||||
public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
public static final int TERMINAL_SLOT_COUNT = StorageTerminalRules.PAGE_SIZE;
|
||||
public static final int PAGE_BUTTON_BASE = 10;
|
||||
public static final int SEARCH_CLEAR_BUTTON = 100;
|
||||
public static final int SEARCH_BACKSPACE_BUTTON = 101;
|
||||
public static final int SEARCH_CODEPOINT_BUTTON_BASE = 1_000;
|
||||
private static final int PLAYER_INVENTORY_Y = 140;
|
||||
private static final int REFRESH_INTERVAL = 20;
|
||||
private static final double MAX_DISTANCE_SQUARED = 64.0D;
|
||||
@@ -43,7 +46,9 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
private final BlockPos terminalPos;
|
||||
private StorageTerminalNetwork.Discovery discovery =
|
||||
StorageTerminalNetwork.Discovery.error(StorageTerminalStatus.INVALID_TERMINAL);
|
||||
private final StorageTerminalLayout layout = new StorageTerminalLayout();
|
||||
private List<StorageTerminalInventory.Entry> entries = List.of();
|
||||
private String searchQuery = "";
|
||||
private int refreshCooldown = REFRESH_INTERVAL;
|
||||
|
||||
public StorageTerminalMenu(int containerId, Inventory inventory) {
|
||||
@@ -52,7 +57,7 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
|
||||
public StorageTerminalMenu(int containerId, Inventory inventory, ServerPlayer player, BlockPos pos) {
|
||||
this(containerId, inventory, new SimpleContainerData(DATA_COUNT), player, pos.immutable());
|
||||
refresh(0);
|
||||
refresh(0, true);
|
||||
}
|
||||
|
||||
private StorageTerminalMenu(int containerId, Inventory inventory, ContainerData data,
|
||||
@@ -115,8 +120,25 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
public boolean clickMenuButton(Player player, int buttonId) {
|
||||
if (serverPlayer == null || player != serverPlayer || !stillValid(player)) return false;
|
||||
int requestedPage = buttonId - PAGE_BUTTON_BASE;
|
||||
if (requestedPage < 0 || requestedPage >= StorageTerminalRules.MAX_PAGE_COUNT) return false;
|
||||
refresh(requestedPage);
|
||||
if (requestedPage >= 0 && requestedPage < StorageTerminalRules.MAX_PAGE_COUNT) {
|
||||
refresh(requestedPage, false);
|
||||
broadcastChanges();
|
||||
return true;
|
||||
}
|
||||
if (buttonId == SEARCH_CLEAR_BUTTON) {
|
||||
searchQuery = "";
|
||||
} else if (buttonId == SEARCH_BACKSPACE_BUTTON) {
|
||||
if (searchQuery.isEmpty()) return true;
|
||||
searchQuery = searchQuery.substring(0,
|
||||
searchQuery.offsetByCodePoints(searchQuery.length(), -1));
|
||||
} else {
|
||||
int codePoint = buttonId - SEARCH_CODEPOINT_BUTTON_BASE;
|
||||
if (!isSearchCodePoint(codePoint)) return false;
|
||||
String appended = new String(Character.toChars(codePoint));
|
||||
if (searchQuery.length() + appended.length() > StorageTerminalRules.MAX_SEARCH_LENGTH) return false;
|
||||
searchQuery += appended;
|
||||
}
|
||||
refresh(0, true);
|
||||
broadcastChanges();
|
||||
return true;
|
||||
}
|
||||
@@ -156,7 +178,7 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
if (source.isEmpty()) sourceSlot.setByPlayer(ItemStack.EMPTY);
|
||||
else sourceSlot.setChanged();
|
||||
sourceSlot.onTake(player, source);
|
||||
refresh(page());
|
||||
refresh(page(), false);
|
||||
broadcastChanges();
|
||||
return original;
|
||||
}
|
||||
@@ -171,7 +193,7 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
|
||||
@Override
|
||||
public void broadcastChanges() {
|
||||
if (serverPlayer != null && --refreshCooldown <= 0) refresh(page());
|
||||
if (serverPlayer != null && --refreshCooldown <= 0) refresh(page(), false);
|
||||
super.broadcastChanges();
|
||||
}
|
||||
|
||||
@@ -185,19 +207,27 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
return !(slot instanceof TerminalDisplaySlot) && super.canTakeItemForPickAll(stack, slot);
|
||||
}
|
||||
|
||||
private void refresh(int requestedPage) {
|
||||
private void refresh(int requestedPage, boolean resetOrder) {
|
||||
if (serverPlayer == null || terminalPos == null) return;
|
||||
discovery = StorageTerminalNetwork.discover(serverPlayer, terminalPos);
|
||||
StorageTerminalNetwork.Snapshot snapshot = StorageTerminalNetwork.snapshot(discovery, requestedPage);
|
||||
entries = snapshot.entries();
|
||||
data.set(STATUS_DATA, snapshot.status().ordinal());
|
||||
data.set(PAGE_DATA, snapshot.page());
|
||||
data.set(PAGE_COUNT_DATA, snapshot.pageCount());
|
||||
data.set(CONTAINER_COUNT_DATA, snapshot.containerCount());
|
||||
data.set(TOTAL_ENTRIES_DATA, snapshot.totalEntries());
|
||||
List<StorageTerminalInventory.Entry> liveEntries =
|
||||
StorageTerminalNetwork.filteredEntries(discovery, searchQuery);
|
||||
layout.update(liveEntries, resetOrder);
|
||||
|
||||
int page = discovery.status() == StorageTerminalStatus.READY
|
||||
? StorageTerminalRules.clampPage(requestedPage, layout.size()) : 0;
|
||||
int pageCount = discovery.status() == StorageTerminalStatus.READY
|
||||
? StorageTerminalRules.pageCount(layout.size()) : 1;
|
||||
entries = discovery.status() == StorageTerminalStatus.READY ? layout.page(page) : List.of();
|
||||
data.set(STATUS_DATA, discovery.status().ordinal());
|
||||
data.set(PAGE_DATA, page);
|
||||
data.set(PAGE_COUNT_DATA, pageCount);
|
||||
data.set(CONTAINER_COUNT_DATA, discovery.containers().size());
|
||||
data.set(TOTAL_ENTRIES_DATA, liveEntries.size());
|
||||
for (int slot = 0; slot < TERMINAL_SLOT_COUNT; slot++) {
|
||||
StorageTerminalInventory.Entry entry = slot < entries.size() ? entries.get(slot) : null;
|
||||
display.setItem(slot, entry == null ? ItemStack.EMPTY : entry.stack().copyWithCount(1));
|
||||
display.setItem(slot, entry == null || entry.total() <= 0
|
||||
? ItemStack.EMPTY : entry.stack().copyWithCount(1));
|
||||
int total = entry == null ? 0 : entry.total();
|
||||
int base = TOTALS_DATA + slot * WORDS_PER_TOTAL;
|
||||
data.set(base, StorageTerminalRules.totalLowWord(total));
|
||||
@@ -206,6 +236,12 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
refreshCooldown = REFRESH_INTERVAL;
|
||||
}
|
||||
|
||||
private static boolean isSearchCodePoint(int codePoint) {
|
||||
return Character.isValidCodePoint(codePoint)
|
||||
&& !(codePoint >= Character.MIN_SURROGATE && codePoint <= Character.MAX_SURROGATE)
|
||||
&& !Character.isISOControl(codePoint);
|
||||
}
|
||||
|
||||
private boolean ready() {
|
||||
if (serverPlayer == null || terminalPos == null) return false;
|
||||
discovery = StorageTerminalNetwork.discover(serverPlayer, terminalPos);
|
||||
@@ -215,12 +251,13 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
private void takeToCarried(int slot, boolean single) {
|
||||
if (!ready() || slot >= entries.size()) return;
|
||||
StorageTerminalInventory.Entry entry = entries.get(slot);
|
||||
if (entry.stack().isEmpty() || entry.total() <= 0) return;
|
||||
int requested = single ? 1 : entry.stack().getMaxStackSize();
|
||||
ItemStack extracted = StorageTerminalInventory.take(discovery.inventoryContainers(), entry.stack(),
|
||||
Math.min(entry.total(), requested));
|
||||
if (extracted.isEmpty()) return;
|
||||
setCarried(extracted);
|
||||
refresh(page());
|
||||
refresh(page(), false);
|
||||
broadcastChanges();
|
||||
}
|
||||
|
||||
@@ -233,13 +270,14 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
if (moved <= 0) return;
|
||||
carried.shrink(moved);
|
||||
if (carried.isEmpty()) setCarried(ItemStack.EMPTY);
|
||||
refresh(page());
|
||||
refresh(page(), false);
|
||||
broadcastChanges();
|
||||
}
|
||||
|
||||
private ItemStack takeIntoPlayer(int slot) {
|
||||
if (!ready() || slot >= entries.size()) return ItemStack.EMPTY;
|
||||
StorageTerminalInventory.Entry entry = entries.get(slot);
|
||||
if (entry.stack().isEmpty() || entry.total() <= 0) return ItemStack.EMPTY;
|
||||
int requested = Math.min(entry.total(), entry.stack().getMaxStackSize());
|
||||
ItemStack extracted = StorageTerminalInventory.take(discovery.inventoryContainers(), entry.stack(), requested);
|
||||
if (extracted.isEmpty()) return ItemStack.EMPTY;
|
||||
@@ -247,7 +285,7 @@ public final class StorageTerminalMenu extends AbstractContainerMenu {
|
||||
int inserted = insertPlayer(serverPlayer, extracted);
|
||||
if (!extracted.isEmpty()) StorageTerminalInventory.insert(discovery.inventoryContainers(), extracted);
|
||||
if (inserted <= 0) return ItemStack.EMPTY;
|
||||
refresh(page());
|
||||
refresh(page(), false);
|
||||
broadcastChanges();
|
||||
return original.copyWithCount(inserted);
|
||||
}
|
||||
|
||||
+5
-18
@@ -48,16 +48,11 @@ public final class StorageTerminalNetwork {
|
||||
return new Discovery(StorageTerminalStatus.READY, containers);
|
||||
}
|
||||
|
||||
public static Snapshot snapshot(Discovery discovery, int requestedPage) {
|
||||
if (discovery.status() != StorageTerminalStatus.READY) {
|
||||
return new Snapshot(discovery.status(), 0, 1, 0, 0, List.of());
|
||||
}
|
||||
List<StorageTerminalInventory.Entry> all = StorageTerminalInventory.aggregate(discovery.containers());
|
||||
int page = StorageTerminalRules.clampPage(requestedPage, all.size());
|
||||
int start = StorageTerminalRules.pageStart(page, all.size());
|
||||
int end = Math.min(all.size(), start + StorageTerminalRules.PAGE_SIZE);
|
||||
return new Snapshot(StorageTerminalStatus.READY, page, StorageTerminalRules.pageCount(all.size()),
|
||||
discovery.containers().size(), all.size(), all.subList(start, end));
|
||||
public static List<StorageTerminalInventory.Entry> filteredEntries(
|
||||
Discovery discovery, String searchQuery) {
|
||||
if (discovery.status() != StorageTerminalStatus.READY) return List.of();
|
||||
return StorageTerminalSearch.filter(
|
||||
StorageTerminalInventory.aggregate(discovery.containers()), searchQuery);
|
||||
}
|
||||
|
||||
private static BaseContainerBlockEntity storageEntity(ServerLevel level, BlockPos pos) {
|
||||
@@ -81,12 +76,4 @@ public final class StorageTerminalNetwork {
|
||||
return containers;
|
||||
}
|
||||
}
|
||||
|
||||
public record Snapshot(StorageTerminalStatus status, int page, int pageCount, int containerCount,
|
||||
int totalEntries, List<StorageTerminalInventory.Entry> entries) {
|
||||
public Snapshot {
|
||||
status = status == null ? StorageTerminalStatus.INVALID_TERMINAL : status;
|
||||
entries = entries == null ? List.of() : List.copyOf(entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -6,6 +6,7 @@ public final class StorageTerminalRules {
|
||||
public static final int SLOTS_PER_CONTAINER = 27;
|
||||
public static final int MAX_ENTRIES = MAX_CONTAINERS * SLOTS_PER_CONTAINER;
|
||||
public static final int MAX_PAGE_COUNT = MAX_ENTRIES / PAGE_SIZE;
|
||||
public static final int MAX_SEARCH_LENGTH = 32;
|
||||
|
||||
private StorageTerminalRules() {
|
||||
}
|
||||
@@ -34,4 +35,20 @@ public final class StorageTerminalRules {
|
||||
public static int totalFromWords(int low, int high) {
|
||||
return (low & 0xFFFF) | (high & 0xFFFF) << 16;
|
||||
}
|
||||
|
||||
public static String compactCount(int count) {
|
||||
int bounded = Math.max(0, count);
|
||||
if (bounded < 1_000) return Integer.toString(bounded);
|
||||
if (bounded < 10_000) return decimalUnit(bounded, 1_000, "K");
|
||||
if (bounded < 1_000_000) return (bounded / 1_000) + "K";
|
||||
if (bounded < 10_000_000) return decimalUnit(bounded, 1_000_000, "M");
|
||||
return (bounded / 1_000_000) + "M";
|
||||
}
|
||||
|
||||
private static String decimalUnit(int count, int unit, String suffix) {
|
||||
int tenths = count / (unit / 10);
|
||||
int decimal = tenths % 10;
|
||||
return decimal == 0 ? (tenths / 10) + suffix
|
||||
: (tenths / 10) + "." + decimal + suffix;
|
||||
}
|
||||
}
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package fr.koka99cab.sanctuary26.redstoner.storage;
|
||||
|
||||
import java.text.Normalizer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** Server-side matching for the bounded terminal search field. */
|
||||
public final class StorageTerminalSearch {
|
||||
private static final Pattern COMBINING_MARKS = Pattern.compile("\\p{M}+");
|
||||
private static final Pattern NON_ALPHANUMERIC = Pattern.compile("[^\\p{L}\\p{N}]+");
|
||||
|
||||
private StorageTerminalSearch() {
|
||||
}
|
||||
|
||||
public static List<StorageTerminalInventory.Entry> filter(
|
||||
List<StorageTerminalInventory.Entry> entries, String query) {
|
||||
String normalizedQuery = normalize(query);
|
||||
if (normalizedQuery.isEmpty()) return List.copyOf(entries);
|
||||
String[] terms = normalizedQuery.split(" ");
|
||||
List<StorageTerminalInventory.Entry> matches = new ArrayList<>();
|
||||
for (StorageTerminalInventory.Entry entry : entries) {
|
||||
if (matches(entry.stack(), terms)) matches.add(entry);
|
||||
}
|
||||
return List.copyOf(matches);
|
||||
}
|
||||
|
||||
static String normalize(String value) {
|
||||
if (value == null || value.isBlank()) return "";
|
||||
String withoutMarks = COMBINING_MARKS.matcher(
|
||||
Normalizer.normalize(value, Normalizer.Form.NFD)).replaceAll("");
|
||||
return NON_ALPHANUMERIC.matcher(withoutMarks.toLowerCase(Locale.ROOT))
|
||||
.replaceAll(" ").strip();
|
||||
}
|
||||
|
||||
private static boolean matches(ItemStack stack, String[] terms) {
|
||||
StringBuilder searchable = new StringBuilder(stack.getHoverName().getString());
|
||||
Identifier id = BuiltInRegistries.ITEM.getKey(stack.getItem());
|
||||
if (id != null) searchable.append(' ').append(id);
|
||||
searchable.append(' ').append(stack.getItem().getDescriptionId());
|
||||
String normalized = normalize(searchable.toString());
|
||||
for (String term : terms) {
|
||||
if (!normalized.contains(term)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,8 @@
|
||||
"tooltip.redstoner.particuleur": "Insert a catalyst, then power the block with redstone.",
|
||||
"block.redstoner.storage_terminal": "Storage Terminal",
|
||||
"screen.redstoner.storage_terminal.title": "Storage Terminal",
|
||||
"screen.redstoner.storage_terminal.search": "Search…",
|
||||
"screen.redstoner.storage_terminal.no_results": "No items found.",
|
||||
"screen.redstoner.storage_terminal.summary": "%s/%s · %s blocks · %s types",
|
||||
"screen.redstoner.storage_terminal.error.invalid_terminal": "The terminal is no longer accessible.",
|
||||
"screen.redstoner.storage_terminal.error.disconnected": "No connected chest or barrel.",
|
||||
|
||||
@@ -52,6 +52,8 @@
|
||||
"tooltip.redstoner.particuleur": "Insérez un catalyseur puis alimentez le bloc en redstone.",
|
||||
"block.redstoner.storage_terminal": "Terminal de stockage",
|
||||
"screen.redstoner.storage_terminal.title": "Terminal de stockage",
|
||||
"screen.redstoner.storage_terminal.search": "Rechercher…",
|
||||
"screen.redstoner.storage_terminal.no_results": "Aucun objet trouvé.",
|
||||
"screen.redstoner.storage_terminal.summary": "%s/%s · %s blocs · %s types",
|
||||
"screen.redstoner.storage_terminal.error.invalid_terminal": "Le terminal n'est plus accessible.",
|
||||
"screen.redstoner.storage_terminal.error.disconnected": "Aucun coffre ou baril connecté.",
|
||||
|
||||
@@ -52,6 +52,8 @@
|
||||
"tooltip.redstoner.particuleur": "Вставьте катализатор и подайте сигнал редстоуна.",
|
||||
"block.redstoner.storage_terminal": "Терминал хранилища",
|
||||
"screen.redstoner.storage_terminal.title": "Терминал хранилища",
|
||||
"screen.redstoner.storage_terminal.search": "Поиск…",
|
||||
"screen.redstoner.storage_terminal.no_results": "Предметы не найдены.",
|
||||
"screen.redstoner.storage_terminal.summary": "%s/%s · блоков: %s · типов: %s",
|
||||
"screen.redstoner.storage_terminal.error.invalid_terminal": "Терминал больше недоступен.",
|
||||
"screen.redstoner.storage_terminal.error.disconnected": "Нет подключённых сундуков или бочек.",
|
||||
|
||||
+57
@@ -25,6 +25,8 @@ public final class StorageTerminalSmoke {
|
||||
verifyTopology();
|
||||
verifyPagination();
|
||||
verifyAggregationAndTransfers();
|
||||
verifySearch();
|
||||
verifyStableLayout();
|
||||
}
|
||||
|
||||
private static void verifyTopology() {
|
||||
@@ -58,6 +60,12 @@ public final class StorageTerminalSmoke {
|
||||
(short) StorageTerminalRules.totalLowWord(networkMaximum),
|
||||
(short) StorageTerminalRules.totalHighWord(networkMaximum)) == networkMaximum,
|
||||
"A large aggregated quantity was truncated by menu data synchronization");
|
||||
require(StorageTerminalRules.compactCount(999).equals("999")
|
||||
&& StorageTerminalRules.compactCount(1_000).equals("1K")
|
||||
&& StorageTerminalRules.compactCount(1_250).equals("1.2K")
|
||||
&& StorageTerminalRules.compactCount(12_345).equals("12K")
|
||||
&& StorageTerminalRules.compactCount(1_250_000).equals("1.2M"),
|
||||
"Terminal quantities were not compacted without overstating their value");
|
||||
}
|
||||
|
||||
private static void verifyAggregationAndTransfers() {
|
||||
@@ -98,6 +106,55 @@ public final class StorageTerminalSmoke {
|
||||
.findFirst().orElseThrow().total() == 6, "Terminal extraction did not update physical slots");
|
||||
}
|
||||
|
||||
private static void verifySearch() {
|
||||
SimpleContainer container = new SimpleContainer(4);
|
||||
container.setItem(0, new ItemStack(stackable(Items.COBBLESTONE), 12));
|
||||
ItemStack accented = new ItemStack(stackable(Items.DIAMOND), 2);
|
||||
accented.set(DataComponents.CUSTOM_NAME, Component.literal("Élément spécial"));
|
||||
container.setItem(1, accented);
|
||||
ItemStack russian = new ItemStack(stackable(Items.REDSTONE), 3);
|
||||
russian.set(DataComponents.CUSTOM_NAME, Component.literal("Красный камень"));
|
||||
container.setItem(2, russian);
|
||||
|
||||
List<StorageTerminalInventory.Entry> entries = StorageTerminalInventory.aggregate(List.of(container));
|
||||
require(StorageTerminalSearch.filter(entries, "minecraft:cobblestone").size() == 1,
|
||||
"Terminal search did not match a namespaced item id");
|
||||
require(StorageTerminalSearch.filter(entries, "cobble stone").size() == 1,
|
||||
"Terminal search did not combine multiple terms");
|
||||
require(StorageTerminalSearch.filter(entries, "element special").size() == 1,
|
||||
"Terminal search did not ignore accents in a custom name");
|
||||
require(StorageTerminalSearch.filter(entries, "красный камень").size() == 1,
|
||||
"Terminal search did not preserve non-Latin keyboard input");
|
||||
require(StorageTerminalSearch.filter(entries, "missing").isEmpty(),
|
||||
"Terminal search retained an unrelated item");
|
||||
}
|
||||
|
||||
private static Holder<Item> stackable(Item item) {
|
||||
return new Holder.Direct<>(item, DataComponentMap.builder()
|
||||
.set(DataComponents.MAX_STACK_SIZE, 64).build());
|
||||
}
|
||||
|
||||
private static void verifyStableLayout() {
|
||||
StorageTerminalInventory.Entry stone = new StorageTerminalInventory.Entry(
|
||||
new ItemStack(stackable(Items.STONE)), 64);
|
||||
StorageTerminalInventory.Entry dirt = new StorageTerminalInventory.Entry(
|
||||
new ItemStack(stackable(Items.DIRT)), 64);
|
||||
StorageTerminalInventory.Entry diamond = new StorageTerminalInventory.Entry(
|
||||
new ItemStack(stackable(Items.DIAMOND)), 8);
|
||||
StorageTerminalLayout layout = new StorageTerminalLayout();
|
||||
layout.update(List.of(stone, dirt), true);
|
||||
layout.update(List.of(dirt, diamond), false);
|
||||
List<StorageTerminalInventory.Entry> page = layout.page(0);
|
||||
require(page.get(0).total() == 0 && ItemStack.isSameItemSameComponents(page.get(0).stack(), stone.stack()),
|
||||
"An exhausted terminal resource moved another item under the cursor");
|
||||
require(ItemStack.isSameItemSameComponents(page.get(1).stack(), dirt.stack())
|
||||
&& ItemStack.isSameItemSameComponents(page.get(2).stack(), diamond.stack()),
|
||||
"A terminal refresh did not preserve existing slots or append a new resource");
|
||||
layout.update(List.of(dirt, diamond), true);
|
||||
require(ItemStack.isSameItemSameComponents(layout.page(0).get(0).stack(), dirt.stack()),
|
||||
"A deliberate terminal layout reset did not compact the visible resources");
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) throw new IllegalStateException(message);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user