From b8b69591f02ce57d3d90d8b2c83dff9227a13eb8 Mon Sep 17 00:00:00 2001 From: st0rm_kr <963689810@qq.com> Date: Wed, 5 Aug 2026 12:01:35 +0800 Subject: [PATCH 1/7] test: add MatterOpedia query primitives --- build.gradle | 6 +++ .../matteropedia/MatterOpediaQuery.java | 27 +++++++++++ .../matteropedia/MatterOpediaResult.java | 45 +++++++++++++++++++ .../matteropedia/MatterOpediaResultTest.java | 32 +++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaQuery.java create mode 100644 src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaResult.java create mode 100644 src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaResultTest.java diff --git a/build.gradle b/build.gradle index 61de51c..39bdc98 100644 --- a/build.gradle +++ b/build.gradle @@ -83,6 +83,8 @@ repositories { } dependencies { + testImplementation platform('org.junit:junit-bom:5.11.4') + testImplementation 'org.junit.jupiter:junit-jupiter' compileOnly "mezz.jei:jei-1.21.1-common-api:$jei_version" compileOnly "mezz.jei:jei-1.21.1-neoforge-api:$jei_version" runtimeOnly "mezz.jei:jei-1.21.1-neoforge:$jei_version" @@ -122,6 +124,10 @@ tasks.withType(JavaCompile).configureEach { options.compilerArgs << '-Xmaxerrs' << '2000' } +tasks.named('test', Test) { + useJUnitPlatform() +} + publishing { publications { mavenJava(MavenPublication) { diff --git a/src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaQuery.java b/src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaQuery.java new file mode 100644 index 0000000..116d007 --- /dev/null +++ b/src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaQuery.java @@ -0,0 +1,27 @@ +package com.buuz135.replication.calculation.client.matteropedia; + +import net.minecraft.resources.ResourceLocation; + +import java.util.Objects; + +public record MatterOpediaQuery( + ResourceLocation matterType, + FilterMode filterMode, + int amount, + SortType sortType, + boolean descending +) { + public MatterOpediaQuery { + Objects.requireNonNull(matterType, "matterType"); + Objects.requireNonNull(filterMode, "filterMode"); + Objects.requireNonNull(sortType, "sortType"); + } + + public enum FilterMode { + NONE, AMOUNT_EQUAL, AMOUNT_LESS, AMOUNT_GREATER, DOESNT_HAVE, ONLY_HAS + } + + public enum SortType { + AMOUNT, DISPLAY_NAME + } +} diff --git a/src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaResult.java b/src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaResult.java new file mode 100644 index 0000000..017cb5f --- /dev/null +++ b/src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaResult.java @@ -0,0 +1,45 @@ +package com.buuz135.replication.calculation.client.matteropedia; + +import java.util.Objects; + +public final class MatterOpediaResult { + private static final MatterOpediaResult EMPTY = new MatterOpediaResult(new int[0], 0, 0); + + private final int[] entryIds; + private final int fromInclusive; + private final int toExclusive; + + private MatterOpediaResult(int[] entryIds, int fromInclusive, int toExclusive) { + this.entryIds = entryIds; + this.fromInclusive = fromInclusive; + this.toExclusive = toExclusive; + } + + public static MatterOpediaResult empty() { + return EMPTY; + } + + public static MatterOpediaResult all(int[] entryIds) { + return range(entryIds, 0, entryIds.length); + } + + public static MatterOpediaResult range(int[] entryIds, int fromInclusive, int toExclusive) { + Objects.requireNonNull(entryIds, "entryIds"); + Objects.checkFromToIndex(fromInclusive, toExclusive, entryIds.length); + return fromInclusive == toExclusive ? EMPTY + : new MatterOpediaResult(entryIds, fromInclusive, toExclusive); + } + + public int size() { + return toExclusive - fromInclusive; + } + + public boolean isEmpty() { + return size() == 0; + } + + public int entryIdAt(int visibleIndex) { + Objects.checkIndex(visibleIndex, size()); + return entryIds[fromInclusive + visibleIndex]; + } +} diff --git a/src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaResultTest.java b/src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaResultTest.java new file mode 100644 index 0000000..c363195 --- /dev/null +++ b/src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaResultTest.java @@ -0,0 +1,32 @@ +package com.buuz135.replication.calculation.client.matteropedia; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class MatterOpediaResultTest { + @Test + void exposesForwardRangeWithoutCopyingOrder() { + var result = MatterOpediaResult.range(new int[]{4, 7, 9, 12}, 1, 4); + + assertEquals(3, result.size()); + assertEquals(7, result.entryIdAt(0)); + assertEquals(12, result.entryIdAt(2)); + } + + @Test + void preservesTheOrderProvidedByTheSelectedIndex() { + var result = MatterOpediaResult.range(new int[]{12, 9, 7, 4}, 1, 4); + + assertEquals(9, result.entryIdAt(0)); + assertEquals(4, result.entryIdAt(2)); + } + + @Test + void rejectsInvalidRangesAndVisibleIndexes() { + assertThrows(IndexOutOfBoundsException.class, + () -> MatterOpediaResult.range(new int[]{1, 2}, -1, 2)); + var result = MatterOpediaResult.all(new int[]{1, 2}); + assertThrows(IndexOutOfBoundsException.class, () -> result.entryIdAt(2)); + } +} From 3e874e0ffbe46d30319af6cad0c22e76f2ff2865 Mon Sep 17 00:00:00 2001 From: st0rm_kr <963689810@qq.com> Date: Wed, 5 Aug 2026 12:13:40 +0800 Subject: [PATCH 2/7] feat: index MatterOpedia entries by matter --- build.gradle | 2 + .../matteropedia/MatterOpediaCatalog.java | 489 ++++++++++++++++++ .../matteropedia/MatterOpediaCatalogTest.java | 201 +++++++ 3 files changed, 692 insertions(+) create mode 100644 src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalog.java create mode 100644 src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalogTest.java diff --git a/build.gradle b/build.gradle index 39bdc98..f9c0bf2 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,8 @@ neoForge { // Include resources generated by data generators. sourceSets.main.resources { srcDir 'src/generated/resources' } +configurations.testImplementation.extendsFrom(configurations.compileOnly) + repositories { maven { name 'jared maven' diff --git a/src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalog.java b/src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalog.java new file mode 100644 index 0000000..ac25adb --- /dev/null +++ b/src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalog.java @@ -0,0 +1,489 @@ +package com.buuz135.replication.calculation.client.matteropedia; + +import com.buuz135.replication.calculation.MatterCompound; +import net.minecraft.resources.ResourceLocation; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.IntPredicate; + +public final class MatterOpediaCatalog { + private final List entries; + private final Map indexes; + private final long generation; + private final long languageGeneration; + private final Map queryCache = + new LinkedHashMap<>(16, 0.75F, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > 16; + } + }; + + private MatterOpediaCatalog( + List entries, + Map indexes, + long generation, + long languageGeneration + ) { + this.entries = List.copyOf(entries); + this.indexes = Map.copyOf(indexes); + this.generation = generation; + this.languageGeneration = languageGeneration; + } + + public static MatterOpediaCatalog fromSeeds( + List seeds, + Set matterTypes, + long catalogGeneration, + long languageGeneration + ) { + Objects.requireNonNull(seeds, "seeds"); + Objects.requireNonNull(matterTypes, "matterTypes"); + + List entries = new ArrayList<>(seeds.size()); + for (int entryId = 0; entryId < seeds.size(); entryId++) { + Seed seed = Objects.requireNonNull(seeds.get(entryId), "seed"); + entries.add(new Entry( + entryId, + seed.itemId(), + seed.compound(), + seed.amounts(), + seed.matterTypeCount(), + seed.displayNameKey() + )); + } + List immutableEntries = List.copyOf(entries); + + Map indexes = new LinkedHashMap<>(); + for (ResourceLocation matterType : matterTypes) { + ResourceLocation key = Objects.requireNonNull(matterType, "matterType"); + indexes.put(key, MatterIndex.build(key, immutableEntries)); + } + return new MatterOpediaCatalog( + immutableEntries, indexes, catalogGeneration, languageGeneration); + } + + public static MatterOpediaCatalog empty(long catalogGeneration, long languageGeneration) { + return new MatterOpediaCatalog(List.of(), Map.of(), catalogGeneration, languageGeneration); + } + + public MatterOpediaResult query(MatterOpediaQuery query) { + Objects.requireNonNull(query, "query"); + MatterIndex index = indexes.get(query.matterType()); + if (index == null) { + return MatterOpediaResult.empty(); + } + + CacheKey cacheKey = new CacheKey(query, generation, languageGeneration); + MatterOpediaResult cached = queryCache.get(cacheKey); + if (cached != null) { + return cached; + } + + MatterOpediaResult result = index.query(query); + queryCache.put(cacheKey, result); + return result; + } + + public Entry entry(int entryId) { + return entries.get(entryId); + } + + public long generation() { + return generation; + } + + public long languageGeneration() { + return languageGeneration; + } + + public MatterOpediaCatalog withDisplayNames( + Map displayNames, + long nextLanguageGeneration + ) { + Objects.requireNonNull(displayNames, "displayNames"); + + List renamedEntries = new ArrayList<>(entries.size()); + for (Entry entry : entries) { + renamedEntries.add(entry.withDisplayNameKey(displayNames.get(entry.itemId()))); + } + List immutableEntries = List.copyOf(renamedEntries); + + Map renamedIndexes = new LinkedHashMap<>(); + for (Map.Entry index : indexes.entrySet()) { + renamedIndexes.put(index.getKey(), index.getValue().withEntries(immutableEntries)); + } + return new MatterOpediaCatalog( + immutableEntries, renamedIndexes, generation, nextLanguageGeneration); + } + + public record Seed( + ResourceLocation itemId, + MatterCompound compound, + Map amounts, + int matterTypeCount, + String displayNameKey + ) { + public Seed { + Objects.requireNonNull(itemId, "itemId"); + Objects.requireNonNull(compound, "compound"); + amounts = Map.copyOf(Objects.requireNonNull(amounts, "amounts")); + displayNameKey = normalizeDisplayName(displayNameKey); + } + } + + public record Entry( + int entryId, + ResourceLocation itemId, + MatterCompound compound, + Map amounts, + int matterTypeCount, + String displayNameKey + ) { + public Entry { + Objects.requireNonNull(itemId, "itemId"); + Objects.requireNonNull(compound, "compound"); + amounts = Map.copyOf(Objects.requireNonNull(amounts, "amounts")); + displayNameKey = normalizeDisplayName(displayNameKey); + } + + public double amount(ResourceLocation matterType) { + return amounts.getOrDefault(matterType, 0D); + } + + private Entry withDisplayNameKey(String nextDisplayNameKey) { + return new Entry( + entryId, itemId, compound, amounts, matterTypeCount, nextDisplayNameKey); + } + } + + private static String normalizeDisplayName(String displayNameKey) { + return displayNameKey == null ? "" : displayNameKey; + } + + private record CacheKey( + MatterOpediaQuery query, + long catalogGeneration, + long languageGeneration + ) { + } + + private static final class MatterIndex { + private final ResourceLocation matterType; + private final double[] amountsByEntryId; + private final int[] allByAmountAscending; + private final int[] allByAmountDescending; + private final int[] allByNameAscending; + private final int[] allByNameDescending; + private final int[] positiveByAmountAscending; + private final int[] positiveByAmountDescending; + private final int[] positiveByNameAscending; + private final int[] positiveByNameDescending; + private final int[] onlyHasByAmountAscending; + private final int[] onlyHasByAmountDescending; + private final int[] onlyHasByNameAscending; + private final int[] onlyHasByNameDescending; + private final int[] zeroStable; + private final int[] zeroByNameAscending; + private final int[] zeroByNameDescending; + + private MatterIndex( + ResourceLocation matterType, + double[] amountsByEntryId, + int[] allByAmountAscending, + int[] allByAmountDescending, + int[] allByNameAscending, + int[] allByNameDescending, + int[] positiveByAmountAscending, + int[] positiveByAmountDescending, + int[] positiveByNameAscending, + int[] positiveByNameDescending, + int[] onlyHasByAmountAscending, + int[] onlyHasByAmountDescending, + int[] onlyHasByNameAscending, + int[] onlyHasByNameDescending, + int[] zeroStable, + int[] zeroByNameAscending, + int[] zeroByNameDescending + ) { + this.matterType = matterType; + this.amountsByEntryId = amountsByEntryId; + this.allByAmountAscending = allByAmountAscending; + this.allByAmountDescending = allByAmountDescending; + this.allByNameAscending = allByNameAscending; + this.allByNameDescending = allByNameDescending; + this.positiveByAmountAscending = positiveByAmountAscending; + this.positiveByAmountDescending = positiveByAmountDescending; + this.positiveByNameAscending = positiveByNameAscending; + this.positiveByNameDescending = positiveByNameDescending; + this.onlyHasByAmountAscending = onlyHasByAmountAscending; + this.onlyHasByAmountDescending = onlyHasByAmountDescending; + this.onlyHasByNameAscending = onlyHasByNameAscending; + this.onlyHasByNameDescending = onlyHasByNameDescending; + this.zeroStable = zeroStable; + this.zeroByNameAscending = zeroByNameAscending; + this.zeroByNameDescending = zeroByNameDescending; + } + + private static MatterIndex build(ResourceLocation matterType, List entries) { + Comparator amountAscending = Comparator + .comparingDouble((Integer id) -> entries.get(id).amount(matterType)) + .thenComparingInt(Integer::intValue); + Comparator amountDescending = Comparator + .comparingDouble(id -> entries.get(id).amount(matterType)) + .reversed() + .thenComparingInt(Integer::intValue); + Comparator nameAscending = Comparator + .comparing((Integer id) -> entries.get(id).displayNameKey()) + .thenComparingInt(Integer::intValue); + Comparator nameDescending = Comparator + .comparing( + (Integer id) -> entries.get(id).displayNameKey(), + Comparator.reverseOrder()) + .thenComparingInt(Integer::intValue); + + int entryCount = entries.size(); + IntPredicate positive = id -> entries.get(id).amount(matterType) > 0; + IntPredicate onlyHas = id -> positive.test(id) + && entries.get(id).matterTypeCount() == 1; + IntPredicate zero = id -> entries.get(id).amount(matterType) == 0; + double[] amountsByEntryId = new double[entryCount]; + for (int entryId = 0; entryId < entryCount; entryId++) { + amountsByEntryId[entryId] = entries.get(entryId).amount(matterType); + } + + return new MatterIndex( + matterType, + amountsByEntryId, + sortedEntryIds(entryCount, ignored -> true, amountAscending), + sortedEntryIds(entryCount, ignored -> true, amountDescending), + sortedEntryIds(entryCount, ignored -> true, nameAscending), + sortedEntryIds(entryCount, ignored -> true, nameDescending), + sortedEntryIds(entryCount, positive, amountAscending), + sortedEntryIds(entryCount, positive, amountDescending), + sortedEntryIds(entryCount, positive, nameAscending), + sortedEntryIds(entryCount, positive, nameDescending), + sortedEntryIds(entryCount, onlyHas, amountAscending), + sortedEntryIds(entryCount, onlyHas, amountDescending), + sortedEntryIds(entryCount, onlyHas, nameAscending), + sortedEntryIds(entryCount, onlyHas, nameDescending), + stableEntryIds(entryCount, zero), + sortedEntryIds(entryCount, zero, nameAscending), + sortedEntryIds(entryCount, zero, nameDescending) + ); + } + + private MatterIndex withEntries(List renamedEntries) { + Comparator nameAscending = Comparator + .comparing((Integer id) -> renamedEntries.get(id).displayNameKey()) + .thenComparingInt(Integer::intValue); + Comparator nameDescending = Comparator + .comparing( + (Integer id) -> renamedEntries.get(id).displayNameKey(), + Comparator.reverseOrder()) + .thenComparingInt(Integer::intValue); + IntPredicate positive = id -> amountsByEntryId[id] > 0; + IntPredicate onlyHas = id -> positive.test(id) + && renamedEntries.get(id).matterTypeCount() == 1; + IntPredicate zero = id -> amountsByEntryId[id] == 0; + + return new MatterIndex( + matterType, + amountsByEntryId, + allByAmountAscending, + allByAmountDescending, + sortedEntryIds(renamedEntries.size(), ignored -> true, nameAscending), + sortedEntryIds(renamedEntries.size(), ignored -> true, nameDescending), + positiveByAmountAscending, + positiveByAmountDescending, + sortedEntryIds(renamedEntries.size(), positive, nameAscending), + sortedEntryIds(renamedEntries.size(), positive, nameDescending), + onlyHasByAmountAscending, + onlyHasByAmountDescending, + sortedEntryIds(renamedEntries.size(), onlyHas, nameAscending), + sortedEntryIds(renamedEntries.size(), onlyHas, nameDescending), + zeroStable, + sortedEntryIds(renamedEntries.size(), zero, nameAscending), + sortedEntryIds(renamedEntries.size(), zero, nameDescending) + ); + } + + private MatterOpediaResult query(MatterOpediaQuery query) { + return switch (query.filterMode()) { + case NONE -> directResult( + query.sortType(), query.descending(), + positiveByAmountAscending, positiveByAmountDescending, + positiveByNameAscending, positiveByNameDescending); + case ONLY_HAS -> directResult( + query.sortType(), query.descending(), + onlyHasByAmountAscending, onlyHasByAmountDescending, + onlyHasByNameAscending, onlyHasByNameDescending); + case DOESNT_HAVE -> query.sortType() == MatterOpediaQuery.SortType.AMOUNT + ? MatterOpediaResult.all(zeroStable) + : MatterOpediaResult.all(query.descending() + ? zeroByNameDescending : zeroByNameAscending); + case AMOUNT_EQUAL, AMOUNT_LESS, AMOUNT_GREATER -> + query.sortType() == MatterOpediaQuery.SortType.AMOUNT + ? amountRange(query) + : scanNameOrder(query); + }; + } + + private MatterOpediaResult directResult( + MatterOpediaQuery.SortType sortType, + boolean descending, + int[] amountAscending, + int[] amountDescending, + int[] nameAscending, + int[] nameDescending + ) { + if (sortType == MatterOpediaQuery.SortType.AMOUNT) { + return MatterOpediaResult.all(descending ? amountDescending : amountAscending); + } + return MatterOpediaResult.all(descending ? nameDescending : nameAscending); + } + + private MatterOpediaResult amountRange(MatterOpediaQuery query) { + int from; + int to; + switch (query.filterMode()) { + case AMOUNT_EQUAL -> { + from = lowerBound(query.amount()); + to = upperBound(query.amount()); + } + case AMOUNT_LESS -> { + from = 0; + to = lowerBound(query.amount()); + } + case AMOUNT_GREATER -> { + from = upperBound(query.amount()); + to = allByAmountAscending.length; + } + default -> throw new IllegalArgumentException( + "Filter mode does not define an amount range: " + query.filterMode()); + } + + if (!query.descending()) { + return MatterOpediaResult.range(allByAmountAscending, from, to); + } + int entryCount = allByAmountAscending.length; + return MatterOpediaResult.range( + allByAmountDescending, entryCount - to, entryCount - from); + } + + private int lowerBound(double target) { + int low = 0; + int high = allByAmountAscending.length; + while (low < high) { + int mid = (low + high) >>> 1; + if (amount(allByAmountAscending[mid]) < target) { + low = mid + 1; + } else { + high = mid; + } + } + return low; + } + + private int upperBound(double target) { + int low = 0; + int high = allByAmountAscending.length; + while (low < high) { + int mid = (low + high) >>> 1; + if (amount(allByAmountAscending[mid]) <= target) { + low = mid + 1; + } else { + high = mid; + } + } + return low; + } + + private MatterOpediaResult scanNameOrder(MatterOpediaQuery query) { + int[] nameOrder = query.descending() + ? allByNameDescending : allByNameAscending; + IntBuffer matchingEntryIds = new IntBuffer(nameOrder.length); + for (int entryId : nameOrder) { + double value = amount(entryId); + boolean matches = switch (query.filterMode()) { + case AMOUNT_EQUAL -> value == query.amount(); + case AMOUNT_LESS -> value < query.amount(); + case AMOUNT_GREATER -> value > query.amount(); + default -> throw new IllegalArgumentException( + "Filter mode does not define an amount predicate: " + + query.filterMode()); + }; + if (matches) { + matchingEntryIds.add(entryId); + } + } + return MatterOpediaResult.all(matchingEntryIds.toArray()); + } + + private double amount(int entryId) { + return amountsByEntryId[entryId]; + } + + private static int[] sortedEntryIds( + int entryCount, + IntPredicate inclusion, + Comparator comparator + ) { + List entryIds = new ArrayList<>(entryCount); + for (int entryId = 0; entryId < entryCount; entryId++) { + if (inclusion.test(entryId)) { + entryIds.add(entryId); + } + } + entryIds.sort(comparator); + return toPrimitiveArray(entryIds); + } + + private static int[] stableEntryIds(int entryCount, IntPredicate inclusion) { + IntBuffer entryIds = new IntBuffer(entryCount); + for (int entryId = 0; entryId < entryCount; entryId++) { + if (inclusion.test(entryId)) { + entryIds.add(entryId); + } + } + return entryIds.toArray(); + } + + private static int[] toPrimitiveArray(List entryIds) { + int[] result = new int[entryIds.size()]; + for (int index = 0; index < entryIds.size(); index++) { + result[index] = entryIds.get(index); + } + return result; + } + } + + private static final class IntBuffer { + private int[] values; + private int size; + + private IntBuffer(int maximumSize) { + values = new int[Math.min(maximumSize, 16)]; + } + + private void add(int value) { + if (size == values.length) { + int nextLength = values.length == 0 + ? 1 : Math.min(values.length * 2, Integer.MAX_VALUE - 8); + values = Arrays.copyOf(values, nextLength); + } + values[size++] = value; + } + + private int[] toArray() { + return Arrays.copyOf(values, size); + } + } +} diff --git a/src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalogTest.java b/src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalogTest.java new file mode 100644 index 0000000..3770105 --- /dev/null +++ b/src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalogTest.java @@ -0,0 +1,201 @@ +package com.buuz135.replication.calculation.client.matteropedia; + +import com.buuz135.replication.calculation.MatterCompound; +import net.minecraft.resources.ResourceLocation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import static com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery.FilterMode.AMOUNT_EQUAL; +import static com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery.FilterMode.AMOUNT_GREATER; +import static com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery.FilterMode.AMOUNT_LESS; +import static com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery.FilterMode.DOESNT_HAVE; +import static com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery.FilterMode.NONE; +import static com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery.FilterMode.ONLY_HAS; +import static com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery.SortType.AMOUNT; +import static com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery.SortType.DISPLAY_NAME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class MatterOpediaCatalogTest { + private static final ResourceLocation EARTH = ResourceLocation.parse("test:earth"); + private static final ResourceLocation METAL = ResourceLocation.parse("test:metal"); + private static final ResourceLocation UNKNOWN = ResourceLocation.parse("test:unknown"); + + private static final ResourceLocation ALPHA_ID = ResourceLocation.parse("test:alpha"); + private static final ResourceLocation BETA_ID = ResourceLocation.parse("test:beta"); + private static final ResourceLocation GAMMA_ID = ResourceLocation.parse("test:gamma"); + private static final ResourceLocation DELTA_ID = ResourceLocation.parse("test:delta"); + + private static final int ALPHA = 0; + private static final int BETA = 1; + private static final int GAMMA = 2; + private static final int DELTA = 3; + + private MatterOpediaCatalog catalog; + + @BeforeEach + void setUp() { + catalog = MatterOpediaCatalog.fromSeeds(List.of( + seed("test:alpha", "Zulu", Map.of(EARTH, 10D), 1), + seed("test:beta", "Alpha", Map.of(EARTH, 5D, METAL, 2D), 2), + seed("test:gamma", "Mike", Map.of(EARTH, 10D), 1), + seed("test:delta", "Beta", Map.of(METAL, 8D), 1) + ), Set.of(EARTH, METAL), 7, 11); + } + + @Test + void usesPrebuiltAmountAndNameOrders() { + assertIds(query(EARTH, NONE, 0, AMOUNT, false), BETA, ALPHA, GAMMA); + assertIds(query(EARTH, NONE, 0, AMOUNT, true), ALPHA, GAMMA, BETA); + assertIds(query(EARTH, NONE, 0, DISPLAY_NAME, false), BETA, GAMMA, ALPHA); + assertIds(query(EARTH, NONE, 0, DISPLAY_NAME, true), ALPHA, GAMMA, BETA); + } + + @Test + void usesDedicatedOnlyHasAndMissingIndexes() { + assertIds(query(EARTH, ONLY_HAS, 0, AMOUNT, false), ALPHA, GAMMA); + assertIds(query(EARTH, DOESNT_HAVE, 0, DISPLAY_NAME, false), DELTA); + assertIds(query(EARTH, DOESNT_HAVE, 0, AMOUNT, true), DELTA); + } + + @Test + void appliesBinaryBoundariesForAmountOrder() { + assertIds(query(EARTH, AMOUNT_EQUAL, 10, AMOUNT, false), ALPHA, GAMMA); + assertIds(query(EARTH, AMOUNT_EQUAL, 10, AMOUNT, true), ALPHA, GAMMA); + assertIds(query(EARTH, AMOUNT_EQUAL, 0, AMOUNT, false), DELTA); + assertIds(query(EARTH, AMOUNT_LESS, 10, AMOUNT, false), DELTA, BETA); + assertIds(query(EARTH, AMOUNT_GREATER, 5, AMOUNT, false), ALPHA, GAMMA); + assertIds(query(EARTH, AMOUNT_LESS, 10, AMOUNT, true), BETA, DELTA); + } + + @Test + void scansNameOrderWithoutResortingForThresholdQueries() { + assertIds(query(EARTH, AMOUNT_GREATER, 5, DISPLAY_NAME, false), GAMMA, ALPHA); + assertIds(query(EARTH, AMOUNT_LESS, 10, DISPLAY_NAME, false), BETA, DELTA); + assertIds(query(EARTH, AMOUNT_GREATER, 5, DISPLAY_NAME, true), ALPHA, GAMMA); + } + + @Test + void returnsCanonicalEmptyResultForUnknownMatterTypes() { + assertSame(MatterOpediaResult.empty(), query(UNKNOWN, NONE, 0, AMOUNT, false)); + } + + @Test + void keepsEqualAmountTiesInEntryIdOrderForBothDirections() { + assertIds(query(EARTH, AMOUNT_EQUAL, 10, AMOUNT, false), ALPHA, GAMMA); + assertIds(query(EARTH, AMOUNT_EQUAL, 10, AMOUNT, true), ALPHA, GAMMA); + } + + @Test + void keepsNegativeAmountsOnlyInAllEntryThresholdQueries() { + MatterOpediaCatalog negativeCatalog = MatterOpediaCatalog.fromSeeds(List.of( + seed("test:negative", "Negative", Map.of(EARTH, -2D), 1), + seed("test:zero", "Zero", Map.of(), 0), + seed("test:positive", "Positive", Map.of(EARTH, 3D), 1) + ), Set.of(EARTH), 1, 1); + + assertIds(negativeCatalog.query(queryOf(EARTH, NONE, 0, AMOUNT, false)), 2); + assertIds(negativeCatalog.query(queryOf(EARTH, DOESNT_HAVE, 0, AMOUNT, false)), 1); + assertIds(negativeCatalog.query(queryOf(EARTH, AMOUNT_EQUAL, -2, AMOUNT, false)), 0); + assertIds(negativeCatalog.query(queryOf(EARTH, AMOUNT_GREATER, -1, AMOUNT, false)), 1, 2); + } + + @Test + void snapshotsSeedAndEntryDataAndNormalizesMissingNames() { + Map mutableAmounts = new HashMap<>(); + mutableAmounts.put(EARTH, 4D); + MatterOpediaCatalog.Seed seed = new MatterOpediaCatalog.Seed( + ALPHA_ID, new MatterCompound(), mutableAmounts, 1, null); + mutableAmounts.put(EARTH, 99D); + + MatterOpediaCatalog snapshot = MatterOpediaCatalog.fromSeeds( + List.of(seed), Set.of(EARTH), 23, 29); + MatterOpediaCatalog.Entry entry = snapshot.entry(0); + + assertEquals(ALPHA_ID, entry.itemId()); + assertEquals(4D, entry.amount(EARTH)); + assertEquals(0D, entry.amount(METAL)); + assertEquals("", entry.displayNameKey()); + assertEquals(23, snapshot.generation()); + assertEquals(29, snapshot.languageGeneration()); + assertThrows(UnsupportedOperationException.class, () -> entry.amounts().put(METAL, 2D)); + } + + @Test + void evictsAndRecomputesTheOldestOfSeventeenDistinctQueries() { + MatterOpediaQuery oldestQuery = queryOf(EARTH, AMOUNT_LESS, 1, AMOUNT, false); + MatterOpediaResult oldestResult = catalog.query(oldestQuery); + assertSame(oldestResult, catalog.query(oldestQuery)); + for (int threshold = 2; threshold <= 17; threshold++) { + catalog.query(queryOf(EARTH, AMOUNT_LESS, threshold, AMOUNT, false)); + } + + MatterOpediaResult recomputed = catalog.query(oldestQuery); + + assertNotSame(oldestResult, recomputed); + assertIds(recomputed, DELTA); + } + + @Test + void replacesNameIndexesAndDoesNotReuseOldLanguageCacheEntries() { + MatterOpediaQuery nameQuery = queryOf(EARTH, NONE, 0, DISPLAY_NAME, false); + MatterOpediaResult oldResult = catalog.query(nameQuery); + MatterOpediaCatalog replacement = catalog.withDisplayNames(Map.of( + ALPHA_ID, "aardvark", + BETA_ID, "zulu", + GAMMA_ID, "mike", + DELTA_ID, "beta" + ), 12); + + MatterOpediaResult replacementResult = replacement.query(nameQuery); + + assertIds(oldResult, BETA, GAMMA, ALPHA); + assertIds(replacementResult, ALPHA, GAMMA, BETA); + assertEquals(7, replacement.generation()); + assertEquals(12, replacement.languageGeneration()); + assertNotSame(oldResult, replacementResult); + } + + private MatterOpediaResult query( + ResourceLocation matterType, + MatterOpediaQuery.FilterMode filterMode, + int amount, + MatterOpediaQuery.SortType sortType, + boolean descending + ) { + return catalog.query(queryOf(matterType, filterMode, amount, sortType, descending)); + } + + private static MatterOpediaQuery queryOf( + ResourceLocation matterType, + MatterOpediaQuery.FilterMode filterMode, + int amount, + MatterOpediaQuery.SortType sortType, + boolean descending + ) { + return new MatterOpediaQuery(matterType, filterMode, amount, sortType, descending); + } + + private static MatterOpediaCatalog.Seed seed( + String itemId, String name, Map amounts, int matterTypeCount + ) { + return new MatterOpediaCatalog.Seed( + ResourceLocation.parse(itemId), new MatterCompound(), amounts, matterTypeCount, + name.toLowerCase(Locale.ROOT)); + } + + private static void assertIds(MatterOpediaResult result, int... expectedIds) { + assertEquals(expectedIds.length, result.size()); + for (int index = 0; index < expectedIds.length; index++) { + assertEquals(expectedIds[index], result.entryIdAt(index), "entry at visible index " + index); + } + } +} From 2c4d3f20baea421c5d2ffc0e42c957c134603ca3 Mon Sep 17 00:00:00 2001 From: st0rm_kr <963689810@qq.com> Date: Wed, 5 Aug 2026 12:26:07 +0800 Subject: [PATCH 3/7] feat: manage MatterOpedia catalog lifecycle --- .../client/ClientReplicationCalculation.java | 102 ++++++++++++++++++ .../replication/client/ClientEvents.java | 10 ++ .../matteropedia/MatterOpediaCatalogTest.java | 20 ++++ 3 files changed, 132 insertions(+) diff --git a/src/main/java/com/buuz135/replication/calculation/client/ClientReplicationCalculation.java b/src/main/java/com/buuz135/replication/calculation/client/ClientReplicationCalculation.java index c215107..7d533cb 100644 --- a/src/main/java/com/buuz135/replication/calculation/client/ClientReplicationCalculation.java +++ b/src/main/java/com/buuz135/replication/calculation/client/ClientReplicationCalculation.java @@ -1,18 +1,29 @@ package com.buuz135.replication.calculation.client; +import com.buuz135.replication.ReplicationRegistry; import com.buuz135.replication.calculation.MatterCompound; import com.buuz135.replication.calculation.ReplicationCalculation; +import com.buuz135.replication.calculation.client.matteropedia.MatterOpediaCatalog; import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; import net.minecraft.world.item.ItemStack; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; public class ClientReplicationCalculation { public static HashMap DEFAULT_MATTER_COMPOUND = new HashMap(); + private static volatile MatterOpediaCatalog matterOpediaCatalog = MatterOpediaCatalog.empty(0, 0); @Nullable public static MatterCompound getMatterCompound(ItemStack stack) { @@ -29,5 +40,96 @@ public static void acceptData(HolderLookup.Provider provider, CompoundTag compou matter.deserializeNBT(provider, compoundTag.getCompound(allKey)); DEFAULT_MATTER_COMPOUND.put(allKey, matter); } + + List seeds = new ArrayList<>(DEFAULT_MATTER_COMPOUND.size()); + DEFAULT_MATTER_COMPOUND.forEach((itemName, compound) -> { + ResourceLocation itemId = ResourceLocation.parse(itemName); + BuiltInRegistries.ITEM.getOptional(itemId).ifPresent(item -> { + ItemStack stack = item.getDefaultInstance(); + if (stack.isEmpty()) { + return; + } + + Map amounts = new LinkedHashMap<>(); + compound.getValues().values().forEach(value -> { + ResourceLocation matterKey = ReplicationRegistry.MATTER_TYPES_REGISTRY.getKey(value.getMatter()); + if (matterKey != null) { + amounts.put(matterKey, value.getAmount()); + } + }); + seeds.add(new MatterOpediaCatalog.Seed( + itemId, + compound, + amounts, + amounts.size(), + stack.getDisplayName().getString().toLowerCase(Locale.ROOT) + )); + }); + }); + + MatterOpediaCatalog current = matterOpediaCatalog; + MatterOpediaCatalog replacement = MatterOpediaCatalog.fromSeeds( + seeds, + getMatterTypeKeys(), + current.generation() + 1, + current.languageGeneration() + ); + matterOpediaCatalog = replacement; + } + + public static MatterOpediaCatalog getMatterOpediaCatalog() { + return matterOpediaCatalog; + } + + public static void rebuildMatterOpediaNameIndexes() { + if (DEFAULT_MATTER_COMPOUND.isEmpty()) { + return; + } + + Map displayNames = new LinkedHashMap<>(); + for (String itemName : DEFAULT_MATTER_COMPOUND.keySet()) { + ResourceLocation itemId = ResourceLocation.parse(itemName); + BuiltInRegistries.ITEM.getOptional(itemId).ifPresent(item -> { + ItemStack stack = item.getDefaultInstance(); + if (!stack.isEmpty()) { + displayNames.put( + itemId, + stack.getDisplayName().getString().toLowerCase(Locale.ROOT) + ); + } + }); + } + if (displayNames.isEmpty()) { + return; + } + + MatterOpediaCatalog current = matterOpediaCatalog; + MatterOpediaCatalog replacement = current.withDisplayNames( + displayNames, + current.languageGeneration() + 1 + ); + matterOpediaCatalog = replacement; + } + + public static void clearClientData() { + DEFAULT_MATTER_COMPOUND.clear(); + MatterOpediaCatalog current = matterOpediaCatalog; + matterOpediaCatalog = MatterOpediaCatalog.empty( + current.generation() + 1, + current.languageGeneration() + ); + } + + private static Set getMatterTypeKeys() { + Set matterTypeKeys = new LinkedHashSet<>(); + ReplicationRegistry.MATTER_TYPES_REGISTRY.forEach(matterType -> { + if (matterType != ReplicationRegistry.Matter.EMPTY.get()) { + ResourceLocation matterKey = ReplicationRegistry.MATTER_TYPES_REGISTRY.getKey(matterType); + if (matterKey != null) { + matterTypeKeys.add(matterKey); + } + } + }); + return matterTypeKeys; } } diff --git a/src/main/java/com/buuz135/replication/client/ClientEvents.java b/src/main/java/com/buuz135/replication/client/ClientEvents.java index ba6e548..76ee5f6 100644 --- a/src/main/java/com/buuz135/replication/client/ClientEvents.java +++ b/src/main/java/com/buuz135/replication/client/ClientEvents.java @@ -28,6 +28,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; import net.minecraft.util.Mth; import net.minecraft.world.inventory.MenuType; import net.minecraft.world.item.ItemStack; @@ -98,6 +99,15 @@ public static void init(){ EventManager.mod(RegisterShadersEvent.class).process(ClientEvents::registerShaders).subscribe(); + EventManager.mod(RegisterClientReloadListenersEvent.class).process(event -> + event.registerReloadListener((ResourceManagerReloadListener) resourceManager -> + Minecraft.getInstance().execute( + ClientReplicationCalculation::rebuildMatterOpediaNameIndexes))) + .subscribe(); + + EventManager.forge(ClientPlayerNetworkEvent.LoggingOut.class).process(event -> + ClientReplicationCalculation.clearClientData()).subscribe(); + EventManager.mod(EntityRenderersEvent.AddLayers.class).process(event -> { for (PlayerSkin.Model skin : event.getSkins()) { var renderer = event.getSkin(skin); diff --git a/src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalogTest.java b/src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalogTest.java index 3770105..9eb8093 100644 --- a/src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalogTest.java +++ b/src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalogTest.java @@ -164,6 +164,26 @@ void replacesNameIndexesAndDoesNotReuseOldLanguageCacheEntries() { assertNotSame(oldResult, replacementResult); } + @Test + void replacingTheCatalogDoesNotMutateThePreviousSnapshot() { + MatterOpediaCatalog original = catalog; + MatterOpediaQuery nameQuery = queryOf(EARTH, NONE, 0, DISPLAY_NAME, false); + MatterOpediaResult originalResult = original.query(nameQuery); + MatterOpediaCatalog replacement = MatterOpediaCatalog.fromSeeds(List.of( + seed("test:epsilon", "Able", Map.of(EARTH, 3D), 1), + seed("test:zeta", "Baker", Map.of(EARTH, 6D), 1) + ), Set.of(EARTH), 8, 11); + + MatterOpediaResult replacementResult = replacement.query(nameQuery); + + assertIds(originalResult, BETA, GAMMA, ALPHA); + assertIds(original.query(nameQuery), BETA, GAMMA, ALPHA); + assertIds(replacementResult, 0, 1); + assertEquals(7, original.generation()); + assertEquals(8, replacement.generation()); + assertNotSame(originalResult, replacementResult); + } + private MatterOpediaResult query( ResourceLocation matterType, MatterOpediaQuery.FilterMode filterMode, From 4e40e810884ea3def366f85a1e06548acd6e9eab Mon Sep 17 00:00:00 2001 From: st0rm_kr <963689810@qq.com> Date: Wed, 5 Aug 2026 12:33:27 +0800 Subject: [PATCH 4/7] fix: skip malformed MatterOpedia item ids --- .../client/ClientReplicationCalculation.java | 16 +++++++++---- .../ClientReplicationCalculationTest.java | 24 +++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 src/test/java/com/buuz135/replication/calculation/client/ClientReplicationCalculationTest.java diff --git a/src/main/java/com/buuz135/replication/calculation/client/ClientReplicationCalculation.java b/src/main/java/com/buuz135/replication/calculation/client/ClientReplicationCalculation.java index 7d533cb..30826a1 100644 --- a/src/main/java/com/buuz135/replication/calculation/client/ClientReplicationCalculation.java +++ b/src/main/java/com/buuz135/replication/calculation/client/ClientReplicationCalculation.java @@ -8,6 +8,7 @@ import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import javax.annotation.Nullable; @@ -19,6 +20,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.function.BiConsumer; public class ClientReplicationCalculation { @@ -43,8 +45,7 @@ public static void acceptData(HolderLookup.Provider provider, CompoundTag compou List seeds = new ArrayList<>(DEFAULT_MATTER_COMPOUND.size()); DEFAULT_MATTER_COMPOUND.forEach((itemName, compound) -> { - ResourceLocation itemId = ResourceLocation.parse(itemName); - BuiltInRegistries.ITEM.getOptional(itemId).ifPresent(item -> { + ifRegisteredItem(itemName, (itemId, item) -> { ItemStack stack = item.getDefaultInstance(); if (stack.isEmpty()) { return; @@ -88,8 +89,7 @@ public static void rebuildMatterOpediaNameIndexes() { Map displayNames = new LinkedHashMap<>(); for (String itemName : DEFAULT_MATTER_COMPOUND.keySet()) { - ResourceLocation itemId = ResourceLocation.parse(itemName); - BuiltInRegistries.ITEM.getOptional(itemId).ifPresent(item -> { + ifRegisteredItem(itemName, (itemId, item) -> { ItemStack stack = item.getDefaultInstance(); if (!stack.isEmpty()) { displayNames.put( @@ -120,6 +120,14 @@ public static void clearClientData() { ); } + static void ifRegisteredItem(String itemName, BiConsumer action) { + ResourceLocation itemId = ResourceLocation.tryParse(itemName); + if (itemId == null) { + return; + } + BuiltInRegistries.ITEM.getOptional(itemId).ifPresent(item -> action.accept(itemId, item)); + } + private static Set getMatterTypeKeys() { Set matterTypeKeys = new LinkedHashSet<>(); ReplicationRegistry.MATTER_TYPES_REGISTRY.forEach(matterType -> { diff --git a/src/test/java/com/buuz135/replication/calculation/client/ClientReplicationCalculationTest.java b/src/test/java/com/buuz135/replication/calculation/client/ClientReplicationCalculationTest.java new file mode 100644 index 0000000..75194db --- /dev/null +++ b/src/test/java/com/buuz135/replication/calculation/client/ClientReplicationCalculationTest.java @@ -0,0 +1,24 @@ +package com.buuz135.replication.calculation.client; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class ClientReplicationCalculationTest { + private static final String MALFORMED_ITEM_ID = "Invalid Item ID"; + + @Test + void skipsMalformedItemIdsBeforeRegistryLookup() { + AtomicBoolean actionInvoked = new AtomicBoolean(); + + assertDoesNotThrow(() -> ClientReplicationCalculation.ifRegisteredItem( + MALFORMED_ITEM_ID, + (itemId, item) -> actionInvoked.set(true) + )); + + assertFalse(actionInvoked.get()); + } +} From 11755237df6c2b3eba985e845080cf26b7c2e80b Mon Sep 17 00:00:00 2001 From: st0rm_kr <963689810@qq.com> Date: Wed, 5 Aug 2026 12:42:23 +0800 Subject: [PATCH 5/7] perf: use indexed MatterOpedia queries --- .../client/gui/MatterOpediaTaskWidget.java | 291 +++++++----------- .../client/gui/ReplicationTerminalScreen.java | 3 + 2 files changed, 118 insertions(+), 176 deletions(-) diff --git a/src/main/java/com/buuz135/replication/client/gui/MatterOpediaTaskWidget.java b/src/main/java/com/buuz135/replication/client/gui/MatterOpediaTaskWidget.java index 70a669d..efc5d08 100644 --- a/src/main/java/com/buuz135/replication/client/gui/MatterOpediaTaskWidget.java +++ b/src/main/java/com/buuz135/replication/client/gui/MatterOpediaTaskWidget.java @@ -3,8 +3,10 @@ import com.buuz135.replication.Replication; import com.buuz135.replication.ReplicationRegistry; import com.buuz135.replication.api.IMatterType; -import com.buuz135.replication.calculation.MatterCompound; import com.buuz135.replication.calculation.client.ClientReplicationCalculation; +import com.buuz135.replication.calculation.client.matteropedia.MatterOpediaCatalog; +import com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery; +import com.buuz135.replication.calculation.client.matteropedia.MatterOpediaResult; import com.buuz135.replication.client.gui.button.ReplicationTerminalConfigButton; import com.buuz135.replication.client.gui.button.ReplicationTerminalTexturedButton; import com.buuz135.replication.util.NumberUtils; @@ -20,13 +22,14 @@ import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.Mth; +import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import java.util.ArrayList; -import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; import static com.buuz135.replication.client.gui.ReplicationTerminalScreen.BUTTONS; @@ -45,17 +48,20 @@ public class MatterOpediaTaskWidget extends AbstractWidget implements Renderable private boolean scrolling; private ReplicationTerminalConfigButton sortingType; private ReplicationTerminalConfigButton sortingDirection; - private MatterValuesMenu matterValuesMenu; private EditBox searchBox; private IMatterType searchMatterType; private int searchMatterAmount; - private FilteringMode filteringMode; + private MatterOpediaQuery.FilterMode filteringMode; + private MatterOpediaCatalog catalog = ClientReplicationCalculation.getMatterOpediaCatalog(); + private MatterOpediaResult result = MatterOpediaResult.empty(); + private final Map visibleStacks = new HashMap<>(); + private int firstVisibleEntry; public MatterOpediaTaskWidget(int pX, int pY, int pWidth, int pHeight, Component pMessage, ReplicationTerminalScreen replicationTerminalContainer, String defaultSearch) { super(pX, pY, pWidth, pHeight, pMessage); this.widgets = new ArrayList<>(); this.searchMatterAmount = 0; - this.filteringMode = FilteringMode.NONE; + this.filteringMode = MatterOpediaQuery.FilterMode.NONE; this.replicationTerminalScreen = replicationTerminalContainer; this.closeButton = new ReplicationTerminalTexturedButton(this.getX() + 176, this.getY() + 10, 9, 9, Component.empty(), EXTRAS, Component.translatable("tooltip.replication.close").getString(), 247, 50, 238, 50, button -> this.replicationTerminalScreen.disableMatteropedia()); @@ -84,8 +90,7 @@ public MatterOpediaTaskWidget(int pX, int pY, int pWidth, int pHeight, Component @Override public void onPress() { super.onPress(); - scrollOffs = 0; - matterValuesMenu.scrollTo(0); + refreshQuery(); } @Override @@ -99,8 +104,7 @@ protected void renderWidget(GuiGraphics guiGraphics, int pMouseX, int pMouseY, f @Override public void onPress() { super.onPress(); - scrollOffs = 0; - matterValuesMenu.scrollTo(0); + refreshQuery(); } @Override @@ -109,7 +113,7 @@ protected void renderWidget(GuiGraphics guiGraphics, int pMouseX, int pMouseY, f } }); - this.matterValuesMenu = new MatterValuesMenu(); + refreshQuery(); } @Override @@ -121,14 +125,52 @@ protected void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, flo int k = this.getY() + 28; int i = k + 88; guiGraphics.blit(BUTTONS, j, k + (int) ((float) (i - k - 5) * this.scrollOffs), 245, 0, 11, 5); - this.searchBox.render(guiGraphics, mouseX, mouseY, v); for (AbstractWidget widget : this.widgets) { widget.render(guiGraphics, mouseX, mouseY, v); } - for (int index = 0; index < this.matterValuesMenu.visibleButtons.size(); index++) { - var widget = this.matterValuesMenu.visibleButtons.get(index); - widget.render(guiGraphics, this.getX() + (index % 9) * 18 + 9, this.getY() + (index / 9) * 18 + 26, mouseX, mouseY, v); + + int visibleCount = Math.min(63, result.size() - firstVisibleEntry); + for (int index = 0; index < visibleCount; index++) { + int entryId = result.entryIdAt(firstVisibleEntry + index); + MatterOpediaCatalog.Entry entry = catalog.entry(entryId); + ItemStack stack = visibleStacks.computeIfAbsent(entryId, ignored -> + BuiltInRegistries.ITEM.getOptional(entry.itemId()) + .map(Item::getDefaultInstance) + .orElse(ItemStack.EMPTY)); + renderEntry(guiGraphics, entry, stack, + getX() + (index % 9) * 18 + 9, + getY() + (index / 9) * 18 + 26, + mouseX, mouseY, v); + } + } + + private void renderEntry( + GuiGraphics guiGraphics, + MatterOpediaCatalog.Entry entry, + ItemStack stack, + int x, + int y, + int mouseX, + int mouseY, + float partialTick + ) { + guiGraphics.renderItem(stack, x + 2, y + 2); + guiGraphics.pose().pushPose(); + guiGraphics.pose().translate(0, 0, 200); + var scale = 0.5f; + ResourceLocation matterKey = ReplicationRegistry.MATTER_TYPES_REGISTRY.getKey(searchMatterType); + var amount = entry.amount(matterKey); + if (amount > 0) { + var display = NumberUtils.getFormatedBigNumber(amount); + guiGraphics.pose().scale(scale, scale, scale); + guiGraphics.drawString(Minecraft.getInstance().font, display, + (x + 18) / scale - Minecraft.getInstance().font.width(display), + (y + 14) / scale, 0xFFFFFF, true); + } + guiGraphics.pose().popPose(); + if (mouseX > x + 2 && mouseX < x + 19 && mouseY > y + 2 && mouseY < y + 19) { + guiGraphics.renderTooltip(Minecraft.getInstance().font, stack, mouseX, mouseY); } } @@ -155,7 +197,7 @@ public boolean mouseDragged(double pMouseX, double pMouseY, int pButton, double int j = i + this.scrollBarHeight - 2; this.scrollOffs = ((float) pMouseY - (float) i - 7.5F) / ((float) (j - i) - 15.0F); this.scrollOffs = Mth.clamp(this.scrollOffs, 0.0F, 1.0F); - this.matterValuesMenu.scrollTo(this.scrollOffs); + this.scrollTo(this.scrollOffs); return true; } return false; @@ -166,8 +208,10 @@ public boolean mouseScrolled(double p_98527_, double p_98528_, double p_98529_, if (!this.canScroll()) { return false; } else { - this.scrollOffs = this.matterValuesMenu.subtractInputFromScroll(this.scrollOffs, scrollY); - this.matterValuesMenu.scrollTo(this.scrollOffs); + this.scrollOffs = Mth.clamp( + this.scrollOffs - (float) (scrollY / (double) this.scrollableRows()), + 0.0F, 1.0F); + this.scrollTo(this.scrollOffs); return true; } } @@ -181,7 +225,7 @@ public boolean mouseReleased(double pMouseX, double pMouseY, int pButton) { } private boolean canScroll() { - return this.matterValuesMenu.canScroll(); + return this.result.size() > 63; } @Override @@ -193,7 +237,7 @@ public boolean mouseClicked(double pMouseX, double pMouseY, int pButton) { int j = i + this.scrollBarHeight - 2; this.scrollOffs = ((float) pMouseY - (float) i - 7.5F) / ((float) (j - i) - 15.0F); this.scrollOffs = Mth.clamp(this.scrollOffs, 0.0F, 1.0F); - this.matterValuesMenu.scrollTo(this.scrollOffs); + this.scrollTo(this.scrollOffs); return true; } if (this.searchBox.isHovered()) { @@ -211,10 +255,6 @@ public boolean mouseClicked(double pMouseX, double pMouseY, int pButton) { if (this.sortingType.isHovered()) { return this.sortingType.mouseClicked(pMouseX, pMouseY, pButton); } - if (this.matterValuesMenu.mouseClicked(pMouseX, pMouseY, pButton)) { - return true; - } - } return false; } @@ -224,8 +264,9 @@ public boolean charTyped(char pCodePoint, int pModifiers) { String s = this.searchBox.getValue(); if (this.searchBox.charTyped(pCodePoint, pModifiers)) { if (!Objects.equals(s, this.searchBox.getValue())) { - cacheMatterType(); - this.matterValuesMenu.scrollTo(0f); + if (cacheMatterType()) { + refreshQuery(); + } } return true; } else { @@ -243,8 +284,9 @@ public boolean keyPressed(int p_98547_, int p_98548_, int p_98549_) { String s = this.searchBox.getValue(); if (this.searchBox.keyPressed(p_98547_, p_98548_, p_98549_)) { if (!Objects.equals(s, this.searchBox.getValue())) { - cacheMatterType(); - this.matterValuesMenu.scrollTo(0f); + if (cacheMatterType()) { + refreshQuery(); + } return true; } } else { @@ -253,27 +295,30 @@ public boolean keyPressed(int p_98547_, int p_98548_, int p_98549_) { return false; } - private void cacheMatterType() { + private boolean cacheMatterType() { + IMatterType previousMatterType = this.searchMatterType; + int previousMatterAmount = this.searchMatterAmount; + MatterOpediaQuery.FilterMode previousFilteringMode = this.filteringMode; var s = this.searchBox.getValue(); var matterType = s; this.searchMatterAmount = 0; if (s.contains("=")) { - this.filteringMode = FilteringMode.AMOUNT_EQUAL; + this.filteringMode = MatterOpediaQuery.FilterMode.AMOUNT_EQUAL; matterType = cleanString(s, "="); } else if (s.contains("<")) { - this.filteringMode = FilteringMode.AMOUNT_LESS; + this.filteringMode = MatterOpediaQuery.FilterMode.AMOUNT_LESS; matterType = cleanString(s, "<"); } else if (s.contains(">")) { - this.filteringMode = FilteringMode.AMOUNT_GREATER; + this.filteringMode = MatterOpediaQuery.FilterMode.AMOUNT_GREATER; matterType = cleanString(s, ">"); } else if (s.startsWith("!")) { - this.filteringMode = FilteringMode.DOESNT_HAVE; + this.filteringMode = MatterOpediaQuery.FilterMode.DOESNT_HAVE; matterType = s.replaceFirst("!", ""); } else if (s.startsWith("*")) { - this.filteringMode = FilteringMode.ONLY_HAS; + this.filteringMode = MatterOpediaQuery.FilterMode.ONLY_HAS; matterType = s.replaceFirst("\\*", ""); } else { - this.filteringMode = FilteringMode.NONE; + this.filteringMode = MatterOpediaQuery.FilterMode.NONE; } var found = false; for (IMatterType iMatterType : ReplicationRegistry.MATTER_TYPES_REGISTRY) { @@ -286,6 +331,9 @@ private void cacheMatterType() { if (!found) { this.searchMatterType = ReplicationRegistry.Matter.EARTH.get(); } + return previousMatterType != this.searchMatterType + || previousMatterAmount != this.searchMatterAmount + || previousFilteringMode != this.filteringMode; } private String cleanString(String input, String splitChar) { @@ -301,156 +349,47 @@ private String cleanString(String input, String splitChar) { return input; } - public List getWidgets() { - return widgets; - } + private MatterOpediaQuery currentQuery(MatterOpediaCatalog catalog) { + ResourceLocation matterKey = ReplicationRegistry.MATTER_TYPES_REGISTRY.getKey(searchMatterType); + MatterOpediaQuery.SortType sortType = sortingType.getState() == 0 + ? MatterOpediaQuery.SortType.AMOUNT + : MatterOpediaQuery.SortType.DISPLAY_NAME; - public enum FilteringMode { - NONE, - AMOUNT_EQUAL, - AMOUNT_LESS, - AMOUNT_GREATER, - DOESNT_HAVE, - ONLY_HAS; + // Preserve the existing comparator's direction behavior exactly. + boolean descending = sortType == MatterOpediaQuery.SortType.AMOUNT + ? sortingDirection.getState() == 1 + : sortingDirection.getState() == 0; + return new MatterOpediaQuery( + matterKey, filteringMode, searchMatterAmount, sortType, descending); } - public class MatterDisplay { - - private ItemStack stack; - private MatterCompound matterCompound; - - public MatterDisplay(ItemStack stack, MatterCompound values) { - this.stack = stack; - this.matterCompound = values; - } - - public ItemStack getStack() { - return stack; - } - - public MatterCompound getMatterCompound() { - return matterCompound; - } - - public double getAmountFor(IMatterType type) { - if (matterCompound.getValues().containsKey(type)) { - return matterCompound.getValues().get(type).getAmount(); - } - return 0; - } - - public void render(GuiGraphics guiGraphics, int x, int y, int mouseX, int mouseY, float v) { - guiGraphics.renderItem(stack, x + 2, y + 2); - guiGraphics.pose().pushPose(); - guiGraphics.pose().translate(0, 0, 200); - var scale = 0.5f; - var amount = getAmountFor(MatterOpediaTaskWidget.this.searchMatterType); - if (amount > 0) { - var display = NumberUtils.getFormatedBigNumber(amount); - guiGraphics.pose().scale(scale, scale, scale); - guiGraphics.drawString(Minecraft.getInstance().font, display, (x + 18) / scale - Minecraft.getInstance().font.width(display), (y + 14) / scale, 0xFFFFFF, true); - } - guiGraphics.pose().popPose(); - if (mouseX > x + 2 && mouseX < x + 19 && mouseY > y + 2 && mouseY < y + 19) { - guiGraphics.renderTooltip(Minecraft.getInstance().font, stack, mouseX, mouseY); - } - } + private void refreshQuery() { + result = catalog.query(currentQuery(catalog)); + firstVisibleEntry = 0; + scrollOffs = 0.0F; } - public class MatterValuesMenu { - public List matterPatternButtonList = new ArrayList<>(); - public List visibleButtons = new ArrayList<>(); - - public MatterValuesMenu() { - ClientReplicationCalculation.DEFAULT_MATTER_COMPOUND.forEach((s, matterCompound) -> { - var item = BuiltInRegistries.ITEM.get(ResourceLocation.parse(s)); - var stack = item.getDefaultInstance(); - if (!stack.isEmpty()) { - matterPatternButtonList.add(new MatterDisplay(stack, matterCompound)); - } - }); - this.scrollTo(0.0F); - } - - - protected int calculateRowCount() { - return Mth.positiveCeilDiv(getFilteredPatterns().size(), 9) - 7; - } - - protected int getRowIndexForScroll(float p_259664_) { - return Math.max((int) ((double) (p_259664_ * (float) this.calculateRowCount()) + 0.5), 0); - } - - protected float getScrollForRowIndex(int p_259315_) { - return Mth.clamp((float) p_259315_ / (float) this.calculateRowCount(), 0.0F, 1.0F); - } - - protected float subtractInputFromScroll(float p_259841_, double p_260358_) { - return Mth.clamp(p_259841_ - (float) (p_260358_ / (double) this.calculateRowCount()), 0.0F, 1.0F); - } - - public void scrollTo(float p_98643_) { - int i = this.getRowIndexForScroll(p_98643_); - this.visibleButtons = new ArrayList<>(); - var filtered = getFilteredPatterns(); - Comparator comparator = Comparator.comparingDouble(matterDisplay -> matterDisplay.getAmountFor(MatterOpediaTaskWidget.this.searchMatterType)); - if (MatterOpediaTaskWidget.this.sortingType.getState() == 1) { - comparator = Comparator.comparing(matterPatternButton -> matterPatternButton.getStack().getDisplayName().getString().toLowerCase()); - comparator = comparator.reversed(); - } - if (MatterOpediaTaskWidget.this.sortingDirection.getState() == 1) { - comparator = comparator.reversed(); - } - filtered.sort(comparator); - for (int j = 0; j < 7; ++j) { - for (int k = 0; k < 9; ++k) { - int l = k + (j + i) * 9; - if (l >= 0 && l < filtered.size()) { - this.visibleButtons.add(filtered.get(l)); - } else { - //CreativeModeInventoryScreen.CONTAINER.setItem(k + j * 9, ItemStack.EMPTY); - } - } - } + private int scrollableRows() { + return Math.max(Mth.positiveCeilDiv(result.size(), 9) - 7, 0); + } - } + public void scrollTo(float offset) { + int firstRow = Math.max((int) (offset * scrollableRows() + 0.5F), 0); + firstVisibleEntry = firstRow * 9; + } - public boolean canScroll() { - return getFilteredPatterns().size() > 9 * 7; - } - - private List getFilteredPatterns() { - var list = this.matterPatternButtonList.stream(); - if (MatterOpediaTaskWidget.this.filteringMode == FilteringMode.NONE) { - list = list.filter(matterPatternButton -> matterPatternButton.getAmountFor(MatterOpediaTaskWidget.this.searchMatterType) > 0); - } else if (MatterOpediaTaskWidget.this.filteringMode == FilteringMode.AMOUNT_EQUAL) { - list = list.filter(matterPatternButton -> matterPatternButton.getAmountFor(MatterOpediaTaskWidget.this.searchMatterType) == MatterOpediaTaskWidget.this.searchMatterAmount); - } else if (MatterOpediaTaskWidget.this.filteringMode == FilteringMode.AMOUNT_LESS) { - list = list.filter(matterPatternButton -> matterPatternButton.getAmountFor(MatterOpediaTaskWidget.this.searchMatterType) < MatterOpediaTaskWidget.this.searchMatterAmount); - } else if (MatterOpediaTaskWidget.this.filteringMode == FilteringMode.AMOUNT_GREATER) { - list = list.filter(matterPatternButton -> matterPatternButton.getAmountFor(MatterOpediaTaskWidget.this.searchMatterType) > MatterOpediaTaskWidget.this.searchMatterAmount); - } else if (MatterOpediaTaskWidget.this.filteringMode == FilteringMode.ONLY_HAS) { - list = list.filter(matterPatternButton -> matterPatternButton.getAmountFor(MatterOpediaTaskWidget.this.searchMatterType) > 0 && matterPatternButton.getMatterCompound().getValues().size() == 1); - } else if (MatterOpediaTaskWidget.this.filteringMode == FilteringMode.DOESNT_HAVE) { - list = list.filter(matterPatternButton -> matterPatternButton.getAmountFor(MatterOpediaTaskWidget.this.searchMatterType) == 0); - } - return list.collect(Collectors.toList()); + public void tickCatalog() { + MatterOpediaCatalog current = ClientReplicationCalculation.getMatterOpediaCatalog(); + if (current.generation() != catalog.generation() + || current.languageGeneration() != catalog.languageGeneration()) { + catalog = current; + visibleStacks.clear(); + refreshQuery(); } + } - public boolean mouseClicked(double pMouseX, double pMouseY, int pButton) { - /**for (int matterButtonIndex = 0; matterButtonIndex < this.visibleButtons.size(); matterButtonIndex++) { - if (pMouseX > MatterOpediaTaskWidget.this.getX() + (matterButtonIndex % 9) * 18 + 11 && pMouseX < MatterOpediaTaskWidget.this.getX() + (matterButtonIndex % 9) * 18 + 11 + 18 - && pMouseY > MatterOpediaTaskWidget.this.topPos + (matterButtonIndex / 9) * 18 + 28 && pMouseY < MatterOpediaTaskWidget.this.topPos + (matterButtonIndex / 9) * 18 + 28 + 18) { - var patternButton = this.visibleButtons.get(matterButtonIndex); - if (patternButton.cachedAmount() == 0) return false; - MatterOpediaTaskWidget.this.enableRequest(new ReplicationRequestWidget((MatterOpediaTaskWidget.this.width - 177) / 2, - (MatterOpediaTaskWidget.this.height - 102) / 2, 177, 102, Component.translatable("replication.request_amount"), patternButton, MatterOpediaTaskWidget.this)); - Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(ReplicationRegistry.Sounds.TERMINAL_BUTTON.get(), 1.0F)); - return true; - } - }**/ - return false; - } + public List getWidgets() { + return widgets; } } diff --git a/src/main/java/com/buuz135/replication/client/gui/ReplicationTerminalScreen.java b/src/main/java/com/buuz135/replication/client/gui/ReplicationTerminalScreen.java index 5d03ac4..dc983ac 100644 --- a/src/main/java/com/buuz135/replication/client/gui/ReplicationTerminalScreen.java +++ b/src/main/java/com/buuz135/replication/client/gui/ReplicationTerminalScreen.java @@ -189,6 +189,9 @@ protected void renderBg(GuiGraphics guiGraphics, float v, int mouseX, int mouseY @Override protected void containerTick() { super.containerTick(); + if (this.matterOpediaTaskWidget != null) { + this.matterOpediaTaskWidget.tickCatalog(); + } //this.searchBox.tick(); TODO var shouldSort = false; for (MatterPatternButton matterPatternButton : this.patternMenu.matterPatternButtonList) { From 5f071b337284b21900497763bfb33a3107b0a805 Mon Sep 17 00:00:00 2001 From: st0rm_kr <963689810@qq.com> Date: Wed, 5 Aug 2026 12:50:15 +0800 Subject: [PATCH 6/7] perf: reuse MatterOpedia number formatter --- .../buuz135/replication/util/NumberUtils.java | 23 +++---------------- .../replication/util/NumberUtilsTest.java | 16 +++++++++++++ 2 files changed, 19 insertions(+), 20 deletions(-) create mode 100644 src/test/java/com/buuz135/replication/util/NumberUtilsTest.java diff --git a/src/main/java/com/buuz135/replication/util/NumberUtils.java b/src/main/java/com/buuz135/replication/util/NumberUtils.java index 85dde23..2f41b6d 100644 --- a/src/main/java/com/buuz135/replication/util/NumberUtils.java +++ b/src/main/java/com/buuz135/replication/util/NumberUtils.java @@ -4,25 +4,10 @@ public class NumberUtils { - private static DecimalFormat formatterWithUnits = new DecimalFormat("####0.#"); + private static final ThreadLocal BIG_NUMBER_FORMATTER = + ThreadLocal.withInitial(() -> new DecimalFormat("#.#")); private static final String[] suffixes = {"", "K", "M", "B", "T", "Q", "Qi", "Sx", "Sp", "O"}; - /*public static String getFormatedBigNumber(double number) { - if (number >= 1000000000) { //BILLION - float numb = (float) (number / 1000_000_000F); - return formatterWithUnits.format(numb) + "B"; - } else if (number >= 1000000) { //MILLION - float numb = (float) (number / 1000000F); - if (number > 100000000) numb = Math.round(numb); - return formatterWithUnits.format(numb) + "M"; - } else if (number >= 1000) { //THOUSANDS - float numb = (float) (number / 1000F); - if (number > 100000) numb = Math.round(numb); - return formatterWithUnits.format(numb) + "K"; - } - return String.valueOf(number); - }*/ - public static String getFormatedBigNumber(double value) { if (value < 1000) { return String.valueOf((int) Math.ceil(value)); @@ -33,8 +18,7 @@ public static String getFormatedBigNumber(double value) { return "Err"; } - DecimalFormat decimalFormat = new DecimalFormat("#.#"); - return decimalFormat.format(value / Math.pow(1000, exp)) + suffixes[exp]; + return BIG_NUMBER_FORMATTER.get().format(value / Math.pow(1000, exp)) + suffixes[exp]; } public static double customCeil(double value) { @@ -44,4 +28,3 @@ public static double customCeil(double value) { return (value > 0) ? (long) value + 1 : (long) value; } } - diff --git a/src/test/java/com/buuz135/replication/util/NumberUtilsTest.java b/src/test/java/com/buuz135/replication/util/NumberUtilsTest.java new file mode 100644 index 0000000..9b6f07e --- /dev/null +++ b/src/test/java/com/buuz135/replication/util/NumberUtilsTest.java @@ -0,0 +1,16 @@ +package com.buuz135.replication.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class NumberUtilsTest { + @Test + void preservesMatterDisplayFormatting() { + assertEquals("1", NumberUtils.getFormatedBigNumber(0.1)); + assertEquals("999", NumberUtils.getFormatedBigNumber(999)); + assertEquals("1K", NumberUtils.getFormatedBigNumber(1_000)); + assertEquals("1.5K", NumberUtils.getFormatedBigNumber(1_500)); + assertEquals("1M", NumberUtils.getFormatedBigNumber(1_000_000)); + } +} From faee10bf1afbff25e150644f5df93ae82f38f75c Mon Sep 17 00:00:00 2001 From: st0rm_kr <963689810@qq.com> Date: Wed, 5 Aug 2026 13:31:03 +0800 Subject: [PATCH 7/7] fix: preserve empty MatterOpedia queries --- build.gradle | 2 +- .../client/ClientReplicationCalculation.java | 19 ++-- .../matteropedia/MatterOpediaCatalog.java | 4 + .../ClientReplicationCalculationTest.java | 95 +++++++++++++++++++ .../matteropedia/MatterOpediaCatalogTest.java | 8 ++ 5 files changed, 119 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index f9c0bf2..2f03f0c 100644 --- a/build.gradle +++ b/build.gradle @@ -48,7 +48,7 @@ neoForge { // Include resources generated by data generators. sourceSets.main.resources { srcDir 'src/generated/resources' } -configurations.testImplementation.extendsFrom(configurations.compileOnly) +configurations.testImplementation.extendsFrom(configurations.neoForgeCompileDependencies) repositories { maven { diff --git a/src/main/java/com/buuz135/replication/calculation/client/ClientReplicationCalculation.java b/src/main/java/com/buuz135/replication/calculation/client/ClientReplicationCalculation.java index 30826a1..bdd499f 100644 --- a/src/main/java/com/buuz135/replication/calculation/client/ClientReplicationCalculation.java +++ b/src/main/java/com/buuz135/replication/calculation/client/ClientReplicationCalculation.java @@ -5,6 +5,7 @@ import com.buuz135.replication.calculation.ReplicationCalculation; import com.buuz135.replication.calculation.client.matteropedia.MatterOpediaCatalog; import net.minecraft.core.HolderLookup; +import net.minecraft.core.Registry; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.nbt.CompoundTag; import net.minecraft.resources.ResourceLocation; @@ -83,7 +84,8 @@ public static MatterOpediaCatalog getMatterOpediaCatalog() { } public static void rebuildMatterOpediaNameIndexes() { - if (DEFAULT_MATTER_COMPOUND.isEmpty()) { + MatterOpediaCatalog current = matterOpediaCatalog; + if (current.isEmpty()) { return; } @@ -103,7 +105,6 @@ public static void rebuildMatterOpediaNameIndexes() { return; } - MatterOpediaCatalog current = matterOpediaCatalog; MatterOpediaCatalog replacement = current.withDisplayNames( displayNames, current.languageGeneration() + 1 @@ -129,13 +130,15 @@ static void ifRegisteredItem(String itemName, BiConsumer } private static Set getMatterTypeKeys() { + return getMatterTypeKeys(ReplicationRegistry.MATTER_TYPES_REGISTRY); + } + + static Set getMatterTypeKeys(Registry matterTypes) { Set matterTypeKeys = new LinkedHashSet<>(); - ReplicationRegistry.MATTER_TYPES_REGISTRY.forEach(matterType -> { - if (matterType != ReplicationRegistry.Matter.EMPTY.get()) { - ResourceLocation matterKey = ReplicationRegistry.MATTER_TYPES_REGISTRY.getKey(matterType); - if (matterKey != null) { - matterTypeKeys.add(matterKey); - } + matterTypes.forEach(matterType -> { + ResourceLocation matterKey = matterTypes.getKey(matterType); + if (matterKey != null) { + matterTypeKeys.add(matterKey); } }); return matterTypeKeys; diff --git a/src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalog.java b/src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalog.java index ac25adb..05196a6 100644 --- a/src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalog.java +++ b/src/main/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalog.java @@ -104,6 +104,10 @@ public long languageGeneration() { return languageGeneration; } + public boolean isEmpty() { + return entries.isEmpty(); + } + public MatterOpediaCatalog withDisplayNames( Map displayNames, long nextLanguageGeneration diff --git a/src/test/java/com/buuz135/replication/calculation/client/ClientReplicationCalculationTest.java b/src/test/java/com/buuz135/replication/calculation/client/ClientReplicationCalculationTest.java index 75194db..eef166f 100644 --- a/src/test/java/com/buuz135/replication/calculation/client/ClientReplicationCalculationTest.java +++ b/src/test/java/com/buuz135/replication/calculation/client/ClientReplicationCalculationTest.java @@ -1,10 +1,29 @@ package com.buuz135.replication.calculation.client; +import com.buuz135.replication.api.IMatterType; +import com.buuz135.replication.api.MatterType; +import com.buuz135.replication.calculation.MatterCompound; +import com.buuz135.replication.calculation.client.matteropedia.MatterOpediaCatalog; +import com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery; +import com.buuz135.replication.calculation.client.matteropedia.MatterOpediaResult; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.neoforge.registries.RegistryBuilder; import org.junit.jupiter.api.Test; +import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import static com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery.FilterMode.AMOUNT_EQUAL; +import static com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery.FilterMode.AMOUNT_LESS; +import static com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery.FilterMode.DOESNT_HAVE; +import static com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery.SortType.AMOUNT; +import static com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery.SortType.DISPLAY_NAME; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; class ClientReplicationCalculationTest { @@ -21,4 +40,80 @@ void skipsMalformedItemIdsBeforeRegistryLookup() { assertFalse(actionInvoked.get()); } + + @Test + void registeredEmptyMatterPreservesLegacyMissingAsZeroQueriesInBothDirections() { + ResourceLocation empty = ResourceLocation.parse("replication:empty"); + ResourceKey> registryKey = ResourceKey.createRegistryKey( + ResourceLocation.parse("test:matter_types")); + Registry registry = new RegistryBuilder<>(registryKey) + .disableRegistrationCheck() + .create(); + Registry.register(registry, empty, MatterType.EMPTY); + Registry.register(registry, ResourceLocation.parse("replication:earth"), MatterType.EARTH); + + MatterOpediaCatalog catalog = MatterOpediaCatalog.fromSeeds(List.of( + seed("test:zulu", "Zulu"), + seed("test:alpha", "Alpha"), + seed("test:mike", "Mike") + ), ClientReplicationCalculation.getMatterTypeKeys(registry), 1, 1); + + for (LegacySearch search : List.of( + new LegacySearch("empty=0", AMOUNT_EQUAL, 0), + new LegacySearch("=0", AMOUNT_EQUAL, 0), + new LegacySearch("empty<1", AMOUNT_LESS, 1), + new LegacySearch("<1", AMOUNT_LESS, 1), + new LegacySearch("!empty", DOESNT_HAVE, 0), + new LegacySearch("!", DOESNT_HAVE, 0))) { + assertIds(search.expression(), + catalog.query(query(empty, search.mode(), search.amount(), AMOUNT, false)), + 0, 1, 2); + assertIds(search.expression(), + catalog.query(query(empty, search.mode(), search.amount(), AMOUNT, true)), + 0, 1, 2); + assertIds(search.expression(), + catalog.query(query(empty, search.mode(), search.amount(), DISPLAY_NAME, false)), + 1, 2, 0); + assertIds(search.expression(), + catalog.query(query(empty, search.mode(), search.amount(), DISPLAY_NAME, true)), + 0, 2, 1); + } + } + + private record LegacySearch( + String expression, + MatterOpediaQuery.FilterMode mode, + int amount + ) { + } + + private static MatterOpediaCatalog.Seed seed(String itemId, String displayName) { + return new MatterOpediaCatalog.Seed( + ResourceLocation.parse(itemId), + new MatterCompound(), + Map.of(), + 0, + displayName.toLowerCase(Locale.ROOT) + ); + } + + private static MatterOpediaQuery query( + ResourceLocation matterType, + MatterOpediaQuery.FilterMode filterMode, + int amount, + MatterOpediaQuery.SortType sortType, + boolean descending + ) { + return new MatterOpediaQuery(matterType, filterMode, amount, sortType, descending); + } + + private static void assertIds( + String expression, MatterOpediaResult result, int... expectedIds + ) { + assertEquals(expectedIds.length, result.size(), expression); + for (int index = 0; index < expectedIds.length; index++) { + assertEquals(expectedIds[index], result.entryIdAt(index), + expression + " entry at index " + index); + } + } } diff --git a/src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalogTest.java b/src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalogTest.java index 9eb8093..c9e24d0 100644 --- a/src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalogTest.java +++ b/src/test/java/com/buuz135/replication/calculation/client/matteropedia/MatterOpediaCatalogTest.java @@ -20,9 +20,11 @@ import static com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery.SortType.AMOUNT; import static com.buuz135.replication.calculation.client.matteropedia.MatterOpediaQuery.SortType.DISPLAY_NAME; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class MatterOpediaCatalogTest { private static final ResourceLocation EARTH = ResourceLocation.parse("test:earth"); @@ -88,6 +90,12 @@ void returnsCanonicalEmptyResultForUnknownMatterTypes() { assertSame(MatterOpediaResult.empty(), query(UNKNOWN, NONE, 0, AMOUNT, false)); } + @Test + void reportsWhetherTheCatalogSnapshotHasEntries() { + assertFalse(catalog.isEmpty()); + assertTrue(MatterOpediaCatalog.empty(8, 12).isEmpty()); + } + @Test void keepsEqualAmountTiesInEntryIdOrderForBothDirections() { assertIds(query(EARTH, AMOUNT_EQUAL, 10, AMOUNT, false), ALPHA, GAMMA);