diff --git a/xplat/src/main/java/dev/emi/emi/api/EmiApi.java b/xplat/src/main/java/dev/emi/emi/api/EmiApi.java index cd88b30f..e2d8a97a 100644 --- a/xplat/src/main/java/dev/emi/emi/api/EmiApi.java +++ b/xplat/src/main/java/dev/emi/emi/api/EmiApi.java @@ -5,6 +5,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import dev.emi.emi.api.search.EmiSearchManager; +import dev.emi.emi.search.EmiSearchManagerImpl; import org.jetbrains.annotations.Nullable; import com.google.common.collect.Lists; @@ -65,6 +67,10 @@ public static void setSearchText(String text) { EmiScreenManager.search.setText(text); } + public static EmiSearchManager createSearchManager() { + return new EmiSearchManagerImpl(); + } + public static boolean isSearchFocused() { return EmiScreenManager.search.isFocused(); } diff --git a/xplat/src/main/java/dev/emi/emi/api/search/EmiSearchManager.java b/xplat/src/main/java/dev/emi/emi/api/search/EmiSearchManager.java new file mode 100644 index 00000000..0e61cb16 --- /dev/null +++ b/xplat/src/main/java/dev/emi/emi/api/search/EmiSearchManager.java @@ -0,0 +1,82 @@ +package dev.emi.emi.api.search; + +import dev.emi.emi.api.stack.EmiIngredient; +import dev.emi.emi.registry.EmiStackList; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * A search manager controls searching for stacks using EMI infrastructure. + */ +public interface EmiSearchManager { + /** + * Search for ingredients matching the given query string. + * + * @param query the query string to use when searching + * @param ingredients the list of ingredients to search in + * @return a future that completes with the updated list + */ + SearchFuture search(String query, List ingredients); + + /** + * Search for ingredients matching the given query string in the list of all known ingredients. + * @param query the query string to use when searching + * @return a future that completes with the updated list + */ + default SearchFuture search(String query) { + return search(query, EmiStackList.stacks); + } + + interface SearchFuture extends Future> { + /** + * {@return the search results if the search has completed, or the original input list} + */ + List getNow(); + + SearchFuture whenCompleted(Consumer> consumer); + + static SearchFuture completedFuture(List list) { + return new SearchFuture() { + @Override + public List getNow() { + return list; + } + + @Override + public SearchFuture whenCompleted(Consumer> consumer) { + consumer.accept(list); + return this; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public List get() { + return list; + } + + @Override + public List get(long timeout, @NotNull TimeUnit unit) { + return list; + } + }; + } + } +} diff --git a/xplat/src/main/java/dev/emi/emi/mixin/ItemStackMixin.java b/xplat/src/main/java/dev/emi/emi/mixin/ItemStackMixin.java index 79c3a229..dd08e418 100644 --- a/xplat/src/main/java/dev/emi/emi/mixin/ItemStackMixin.java +++ b/xplat/src/main/java/dev/emi/emi/mixin/ItemStackMixin.java @@ -22,7 +22,7 @@ public class ItemStackMixin { @Inject(at = @At("RETURN"), method = "getTooltip") private void getTooltip(PlayerEntity player, TooltipContext context, CallbackInfoReturnable> info) { - if (EmiConfig.appendItemModId && EmiConfig.appendModId && Thread.currentThread() != EmiSearch.searchThread) { + if (EmiConfig.appendItemModId && EmiConfig.appendModId && !EmiSearch.isSearchThread()) { List text = info.getReturnValue(); String namespace = EmiPort.getItemRegistry().getId(((ItemStack) (Object) this).getItem()).getNamespace(); String mod = EmiUtil.getModName(namespace); diff --git a/xplat/src/main/java/dev/emi/emi/screen/ConfigScreen.java b/xplat/src/main/java/dev/emi/emi/screen/ConfigScreen.java index de776c98..922d07c9 100644 --- a/xplat/src/main/java/dev/emi/emi/screen/ConfigScreen.java +++ b/xplat/src/main/java/dev/emi/emi/screen/ConfigScreen.java @@ -87,7 +87,7 @@ public void setActiveBind(EmiBind bind, int offset) { @Override public void close() { EmiConfig.writeConfig(); - EmiSearch.update(); + EmiScreenManager.updateSearch(); MinecraftClient.getInstance().setScreen(last); } diff --git a/xplat/src/main/java/dev/emi/emi/screen/EmiScreenManager.java b/xplat/src/main/java/dev/emi/emi/screen/EmiScreenManager.java index 31ca30ab..dcd26936 100644 --- a/xplat/src/main/java/dev/emi/emi/screen/EmiScreenManager.java +++ b/xplat/src/main/java/dev/emi/emi/screen/EmiScreenManager.java @@ -2,11 +2,15 @@ import java.util.List; import java.util.Set; +import java.util.concurrent.Future; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; +import dev.emi.emi.api.search.EmiSearchManager; +import dev.emi.emi.registry.EmiStackList; +import dev.emi.emi.search.EmiSearchManagerImpl; import org.jetbrains.annotations.Nullable; import org.lwjgl.glfw.GLFW; @@ -65,7 +69,6 @@ import dev.emi.emi.screen.widget.EmiSearchWidget; import dev.emi.emi.screen.widget.SidebarButtonWidget; import dev.emi.emi.screen.widget.SizedButtonWidget; -import dev.emi.emi.search.EmiSearch; import dev.emi.emi.search.EmiSearch.CompiledQuery; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.Element; @@ -125,17 +128,30 @@ public class EmiScreenManager { () -> true, (w) -> EmiApi.viewRecipeTree(), List.of(EmiPort.translatable("tooltip.emi.recipe_tree"))); + public static EmiSearchManagerImpl searchManager = new EmiSearchManagerImpl(); + public static EmiSearchManager.SearchFuture lastSearch = EmiSearchManager.SearchFuture.completedFuture(EmiScreenManager.getSearchSource()); + public static boolean searchChanged = true; + + public static boolean isDisabled() { return !EmiReloadManager.isLoaded() || !EmiConfig.enabled; } + public static void updateSearch() { + if(lastSearch != null) { + lastSearch.cancel(true); + } + lastSearch = searchManager.search(search.getText(), EmiScreenManager.getSearchSource()).whenCompleted(l -> searchChanged = true); + } + public static void recalculate() { updateCraftables(); SidebarPanel searchPanel = getSearchPanel(); if (searchPanel != null && searchPanel.space != null) { - if (searchedStacks != EmiSearch.stacks) { + if (searchChanged) { searchPanel.space.batcher.repopulate(); - searchedStacks = EmiSearch.stacks; + searchedStacks = lastSearch.getNow(); + searchChanged = false; } } @@ -238,7 +254,7 @@ private static void updateCraftables() { if (searchPanel != null && searchPanel.space != null) { searchPanel.space.batcher.repopulate(); if (searchPanel.getType() == SidebarType.CRAFTABLES) { - EmiSearch.update(); + EmiScreenManager.updateSearch(); } } EmiFavorites.updateSynthetic(inv); @@ -807,8 +823,8 @@ private static void renderExclusionAreas(EmiDrawContext context, int mouseX, int @SuppressWarnings({"rawtypes", "unchecked"}) private static void renderSlotOverlays(EmiDrawContext context, int mouseX, int mouseY, float delta, EmiScreenBase base) { CompiledQuery query = null; - if (EmiScreenManager.search.highlight) { - query = EmiSearch.compiledQuery; + if (EmiScreenManager.search.highlight && lastSearch instanceof EmiSearchManagerImpl.SearchWorker worker) { + query = worker.getCompiledQuery(); } Set ignoredSlots = Sets.newHashSet(); Set synfavs = Sets.newHashSet(); @@ -1380,7 +1396,7 @@ public void setSidebarPage(int page) { } } if (isSearch()) { - EmiSearch.search(search.getText()); + searchManager.search(search.getText(), EmiScreenManager.getSearchSource()); } if (space != null) { space.batcher.repopulate(); diff --git a/xplat/src/main/java/dev/emi/emi/screen/widget/EmiSearchWidget.java b/xplat/src/main/java/dev/emi/emi/screen/widget/EmiSearchWidget.java index 2e06a29c..624cb409 100644 --- a/xplat/src/main/java/dev/emi/emi/screen/widget/EmiSearchWidget.java +++ b/xplat/src/main/java/dev/emi/emi/screen/widget/EmiSearchWidget.java @@ -131,7 +131,7 @@ public EmiSearchWidget(TextRenderer textRenderer, int x, int y, int width, int h styles.add(new Pair(string.length(), Style.EMPTY.withFormatting(Formatting.WHITE))); } this.styles = styles; - EmiSearch.search(string); + EmiScreenManager.updateSearch(); }); } diff --git a/xplat/src/main/java/dev/emi/emi/search/EmiSearch.java b/xplat/src/main/java/dev/emi/emi/search/EmiSearch.java index 46f9b0b6..02c18113 100644 --- a/xplat/src/main/java/dev/emi/emi/search/EmiSearch.java +++ b/xplat/src/main/java/dev/emi/emi/search/EmiSearch.java @@ -2,6 +2,8 @@ import java.util.List; import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Matcher; @@ -32,14 +34,21 @@ public class EmiSearch { public static final Pattern TOKENS = Pattern.compile("(-?[@#]?\\/(\\\\.|[^\\\\\\/])+\\/|[^\\s]+)"); - private static volatile SearchWorker currentWorker = null; - public static volatile Thread searchThread = null; - public static volatile List stacks = EmiStackList.stacks; - public static volatile CompiledQuery compiledQuery; + private static Thread searchThread; + public static final Executor executor = Executors.newSingleThreadExecutor(task -> { + Thread t = new Thread(task, "EMI Search Thread"); + t.setDaemon(true); + searchThread = t; + return t; + }); public static Set bakedStacks; public static SuffixArray names, tooltips, mods; public static SuffixArray aliases; + public static boolean isSearchThread() { + return searchThread == Thread.currentThread(); + } + public static void bake() { SuffixArray names = new SuffixArray<>(); SuffixArray tooltips = new SuffixArray<>(); @@ -110,31 +119,6 @@ public static void bake() { EmiSearch.bakedStacks = bakedStacks; } - public static void update() { - search(EmiScreenManager.search.getText()); - } - - public static void search(String query) { - synchronized (EmiSearch.class) { - SearchWorker worker = new SearchWorker(query, EmiScreenManager.getSearchSource()); - currentWorker = worker; - - searchThread = new Thread(worker); - searchThread.setDaemon(true); - searchThread.start(); - } - } - - public static void apply(SearchWorker worker, List stacks) { - synchronized (EmiSearch.class) { - if (worker == currentWorker) { - EmiSearch.stacks = stacks; - currentWorker = null; - searchThread = null; - } - } - } - public static class CompiledQuery { public final Query fullQuery; @@ -223,48 +207,4 @@ private static void addQuery(String s, boolean negated, List queries, Fun queries.add(q); } } - - private static class SearchWorker implements Runnable { - private final String query; - private final List source; - - public SearchWorker(String query, List source) { - this.query = query; - this.source = source; - } - - @Override - public void run() { - try { - CompiledQuery compiled = new CompiledQuery(query); - compiledQuery = compiled; - if (compiled.isEmpty()) { - apply(this, source); - return; - } - List stacks = Lists.newArrayList(); - int processed = 0; - for (EmiIngredient stack : source) { - if (processed++ >= 1024) { - processed = 0; - if (this != currentWorker) { - return; - } - } - List ess = stack.getEmiStacks(); - // TODO properly support ingredients? - if (ess.size() == 1) { - EmiStack es = ess.get(0); - if (compiled.test(es)) { - stacks.add(stack); - } - } - } - apply(this, List.copyOf(stacks)); - } catch (Exception e) { - EmiLog.error("Error when attempting to search:"); - e.printStackTrace(); - } - } - } } diff --git a/xplat/src/main/java/dev/emi/emi/search/EmiSearchManagerImpl.java b/xplat/src/main/java/dev/emi/emi/search/EmiSearchManagerImpl.java new file mode 100644 index 00000000..65299aae --- /dev/null +++ b/xplat/src/main/java/dev/emi/emi/search/EmiSearchManagerImpl.java @@ -0,0 +1,126 @@ +package dev.emi.emi.search; + +import com.google.common.collect.Lists; +import dev.emi.emi.api.search.EmiSearchManager; +import dev.emi.emi.api.stack.EmiIngredient; +import dev.emi.emi.api.stack.EmiStack; +import dev.emi.emi.runtime.EmiLog; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; + +public class EmiSearchManagerImpl implements EmiSearchManager { + public SearchWorker search(String query, List ingredients) { + synchronized (this) { + SearchWorker worker = new SearchWorker(query, ingredients); + + EmiSearch.executor.execute(worker); + return worker; + } + } + + public class SearchWorker implements Runnable, SearchFuture { + private final String query; + private EmiSearch.CompiledQuery compiledQuery; + private final List source; + private final CompletableFuture> completion; + private boolean interrupted; + + SearchWorker(String query, List source) { + this.query = query; + this.source = source; + this.completion = new CompletableFuture<>(); + } + + private void apply(List stacks) { + completion.complete(stacks); + } + + public EmiSearch.CompiledQuery getCompiledQuery() { + return this.compiledQuery; + } + + @Override + public void run() { + try { + EmiSearch.CompiledQuery compiled = new EmiSearch.CompiledQuery(query); + compiledQuery = compiled; + if (compiled.isEmpty()) { + apply(source); + return; + } + List stacks = Lists.newArrayList(); + int processed = 0; + for (EmiIngredient stack : source) { + if (processed++ >= 1024) { + processed = 0; + if (interrupted) { + apply(source); + return; + } + } + List ess = stack.getEmiStacks(); + // TODO properly support ingredients? + if (ess.size() == 1) { + EmiStack es = ess.get(0); + if (compiled.test(es)) { + stacks.add(stack); + } + } + } + apply(List.copyOf(stacks)); + } catch (Exception e) { + EmiLog.error("Error when attempting to search:"); + e.printStackTrace(); + apply(source); + } + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + this.interrupted = true; + return true; + } + + @Override + public boolean isCancelled() { + return this.interrupted; + } + + @Override + public boolean isDone() { + return this.completion.isDone(); + } + + @Override + public List get() throws InterruptedException, ExecutionException { + return this.completion.get(); + } + + @Override + public List get(long timeout, @NotNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return this.completion.get(timeout, unit); + } + + @Override + public List getNow() { + return this.completion.getNow(this.source); + } + + @Override + public SearchWorker whenCompleted(Consumer> consumer) { + this.completion.whenComplete((c, ex) -> { + if(c != null) { + consumer.accept(c); + } + }); + return this; + } + } +}