Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,16 @@ default boolean isLeftHandSidePanel() {
@ApiStatus.Experimental
boolean doesCacheDisplayLookup();

/**
* Returns whether REI should force loading recipes from local client data
* instead of waiting for server synchronization.
* Useful for multiplayer servers that do not send recipe data.
*
* @return whether force local recipes is enabled
*/
@ApiStatus.Experimental
boolean isForceLocalRecipes();

boolean doDebugRenderTimeRequired();

boolean doMergeDisplayUnderOne();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
import me.shedaniel.rei.impl.common.networking.DisplaySyncPacket;
import me.shedaniel.rei.impl.common.plugins.PluginManagerImpl;
import me.shedaniel.rei.impl.common.plugins.ReloadManagerImpl;
import me.shedaniel.rei.impl.common.registry.displays.ServerDisplayRegistryImpl;
import me.shedaniel.rei.impl.common.util.InstanceHelper;
import me.shedaniel.rei.impl.common.util.IssuesDetector;
import me.shedaniel.rei.plugin.test.REITestCommonPlugin;
Expand All @@ -117,14 +118,22 @@
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.core.HolderLookup;
import net.minecraft.server.packs.PackResources;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.resources.MultiPackResourceManager;
import net.minecraft.tags.TagLoader;
import net.minecraft.world.item.crafting.RecipeAccess;
import net.minecraft.world.item.crafting.RecipeHolder;
import net.minecraft.world.item.crafting.RecipeManager;
import net.minecraft.world.item.crafting.display.RecipeDisplayEntry;
import net.minecraft.world.item.crafting.display.RecipeDisplayId;
import org.apache.commons.lang3.mutable.MutableLong;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;

import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.*;
import java.util.stream.Stream;

Expand Down Expand Up @@ -311,13 +320,22 @@ private void registerEvents() {
Minecraft client = Minecraft.getInstance();
final ResourceLocation recipeButtonTex = ResourceLocation.withDefaultNamespace("textures/gui/recipe_button.png");
MutableLong endReload = new MutableLong(-1);
// Set up client-side recipe loading for forceLocalRecipes mode.
// When on a remote server with no integrated server, ServerDisplayRegistryImpl
// cannot access GameInstance.getServer().getRecipeManager(). This supplier
// provides recipes loaded from the client's vanilla data pack instead.
ServerDisplayRegistryImpl.setClientRecipeSupplier(RoughlyEnoughItemsCoreClient::loadRecipesFromClientDataPacks);
PRE_UPDATE_RECIPES.register((recipeAccess, registryAccess) -> {
reloadPlugins(null, ReloadStage.START, registryAccess);
});
ClientRecipeUpdateEvent.EVENT.register(recipeManager -> {
reloadPlugins(endReload, ReloadStage.END);
});
ClientRecipeUpdateEvent.ADD.register((recipeAccess, entries) -> {
if (ConfigObject.getInstance().isForceLocalRecipes() && !Minecraft.getInstance().isLocalServer()) {
InternalLogger.getInstance().debug("Ignoring server recipe ADD (%d entries) because forceLocalRecipes is enabled.", entries.size());
return;
}
if (ClientHelperImpl.getInstance().canUsePackets()) {
return;
}
Expand All @@ -328,6 +346,10 @@ private void registerEvents() {
registry.addJob(() -> registry.addRecipes(mapped));
});
ClientRecipeUpdateEvent.REMOVE.register((recipeAccess, entries) -> {
if (ConfigObject.getInstance().isForceLocalRecipes() && !Minecraft.getInstance().isLocalServer()) {
InternalLogger.getInstance().debug("Ignoring server recipe REMOVE (%d entries) because forceLocalRecipes is enabled.", entries.size());
return;
}
if (ClientHelperImpl.getInstance().canUsePackets()) {
return;
}
Expand Down Expand Up @@ -500,4 +522,70 @@ public static void reloadPlugins(MutableLong lastReload, @Nullable ReloadStage s
}
ReloadManagerImpl.reloadPlugins(start, () -> InstanceHelper.connectionFromClient() == null);
}

private static List<RecipeHolder<?>> loadRecipesFromClientDataPacks() {
try {
Minecraft mc = Minecraft.getInstance();
if (mc.getConnection() == null) return Collections.emptyList();
if (mc.isLocalServer()) return Collections.emptyList();
if (!ConfigObject.getInstance().isForceLocalRecipes()) return Collections.emptyList();

InternalLogger.getInstance().info("[Force Local Recipes] Loading recipes from client data packs...");

RegistryAccess registryAccess = mc.getConnection().registryAccess();

// Create a data resource manager from the vanilla pack.
// The vanilla JAR contains all recipe JSONs under data/<namespace>/recipe/.
List<PackResources> packResourcesList = new ArrayList<>();
packResourcesList.add(mc.getVanillaPackResources());

List<RecipeHolder<?>> result;
try (MultiPackResourceManager dataManager = new MultiPackResourceManager(PackType.SERVER_DATA, packResourcesList)) {
// Load tags for ALL builtin registries (items, blocks, fluids, etc.)
// from the vanilla data pack. The connection's registryAccess only contains
// dynamic registries, but recipe ingredients reference tags from builtin
// registries like minecraft:item (e.g. #minecraft:bundles, #minecraft:planks).
int tagCount = 0;
for (net.minecraft.core.Registry<?> registry : net.minecraft.core.registries.BuiltInRegistries.REGISTRY) {
if (registry instanceof net.minecraft.core.WritableRegistry) {
try {
@SuppressWarnings({"unchecked", "rawtypes"})
net.minecraft.core.WritableRegistry writable = (net.minecraft.core.WritableRegistry) registry;
TagLoader.loadTagsForRegistry(dataManager, writable);
tagCount++;
} catch (Exception e) {
// Some registries might not have tag directories, skip silently
}
}
}
InternalLogger.getInstance().info("[Force Local Recipes] Loaded tags for %d builtin registries from client data packs", tagCount);

// Also load tags for dynamic registries from the connection's registryAccess
List<net.minecraft.core.Registry.PendingTags<?>> pendingTags =
TagLoader.loadTagsForExistingRegistries(dataManager, registryAccess);
for (net.minecraft.core.Registry.PendingTags<?> pt : pendingTags) {
pt.apply();
}
if (!pendingTags.isEmpty()) {
InternalLogger.getInstance().info("[Force Local Recipes] Loaded and applied %d dynamic registry tag sets", pendingTags.size());
}

// Now load recipes — tag references will resolve correctly.
RecipeManager recipeManager = new RecipeManager(registryAccess);
recipeManager.reload(
CompletableFuture::completedFuture,
dataManager,
Runnable::run,
Runnable::run
).join();
result = new ArrayList<>(recipeManager.getRecipes());
}

InternalLogger.getInstance().info("[Force Local Recipes] Loaded %d recipes from client data packs", result.size());
return result;
} catch (Exception e) {
InternalLogger.getInstance().error("[Force Local Recipes] Failed to load recipes from client data packs", e);
return Collections.emptyList();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,11 @@ public boolean doesCacheDisplayLookup() {
return advanced.miscellaneous.cachingDisplayLookup;
}

@Override
public boolean isForceLocalRecipes() {
return advanced.miscellaneous.forceLocalRecipes;
}

@Override
public boolean doDebugRenderTimeRequired() {
return advanced.layout.debugRenderTimeRequired;
Expand Down Expand Up @@ -739,6 +744,8 @@ public static class Miscellaneous {
public boolean newFastEntryRendering = true;
public boolean cachingFastEntryRendering = false;
public boolean cachingDisplayLookup = true;
@Comment("Forces REI to load recipes from local client data instead of waiting for server synchronization. Useful for servers that do not send recipe data.")
public boolean forceLocalRecipes = false;
public CategorySettings categorySettings = new CategorySettings();

public static class CategorySettings {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ static <T> OptionGroup make(String id) {
.add(CUSTOMIZED_FILTERING);
OptionGroup FILTERING_ADVANCED = make("filtering.advanced")
.add(FILTER_DISPLAYS)
.add(MERGE_DISPLAYS);
.add(MERGE_DISPLAYS)
.add(FORCE_LOCAL_RECIPES);
OptionGroup LIST_ENTRIES = make("list.entries")
.add(DISPLAY_MODE)
.add(ORDERING)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ static ComparableValue<Double>[] doubleRange(double start, double end, double st
.ofBoolean(translatable("config.rei.values.performance.reload_thread.main_thread"), translatable("config.rei.values.performance.reload_thread.rei_thread"));
CompositeOption<Boolean> CACHED_DISPLAY_LOOKUP = make("performance.cached_display_lookup", i -> i.advanced.miscellaneous.cachingDisplayLookup, (i, v) -> i.advanced.miscellaneous.cachingDisplayLookup = v)
.enabledDisabled();
CompositeOption<Boolean> FORCE_LOCAL_RECIPES = make("filtering.force_local_recipes", i -> i.advanced.miscellaneous.forceLocalRecipes, (i, v) -> i.advanced.miscellaneous.forceLocalRecipes = v)
.enabledDisabled();
CompositeOption<Object> PLUGINS_PERFORMANCE = make("debug.plugins_performance", i -> null, (i, v) -> new Object())
.details((access, option, onClose) -> Minecraft.getInstance().setScreen(new PerformanceScreen(onClose)))
.requiresLevel();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import me.shedaniel.math.Rectangle;
import me.shedaniel.rei.RoughlyEnoughItemsCoreClient;
import me.shedaniel.rei.api.client.ClientHelper;
import me.shedaniel.rei.api.client.config.ConfigObject;
import me.shedaniel.rei.api.client.gui.config.DisplayPanelLocation;
import me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds;
import me.shedaniel.rei.api.client.gui.widgets.Widgets;
Expand Down Expand Up @@ -63,7 +64,7 @@ public ImportantWarningsWidget() {
prevId = newId;
dirty = true;
}
dirty = dirty && !ClientHelper.getInstance().canUseMovePackets();
dirty = dirty && !ClientHelper.getInstance().canUseMovePackets() && !ConfigObject.getInstance().isForceLocalRecipes();
}

this.visible = dirty;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,15 @@ public void startReload() {

@Override
public void endReload() {
// Inject force-local recipe displays from ServerDisplayRegistryImpl
List<Display> forceLocalDisplays = me.shedaniel.rei.impl.common.registry.displays.ServerDisplayRegistryImpl.consumePendingForceLocalDisplays();
if (forceLocalDisplays != null && !forceLocalDisplays.isEmpty()) {
InternalLogger.getInstance().info("[Force Local Recipes] Injecting %d displays into client registry", forceLocalDisplays.size());
for (Display display : forceLocalDisplays) {
super.add(display, SYNCED);
}
}

InternalLogger.getInstance().debug("Found %d displays", size());

for (CategoryIdentifier<?> identifier : getAll().keySet()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import me.shedaniel.rei.RoughlyEnoughItemsNetwork;
import me.shedaniel.rei.api.client.config.ConfigObject;
import me.shedaniel.rei.api.client.registry.display.DisplayRegistry;
import me.shedaniel.rei.api.common.display.Display;
import me.shedaniel.rei.impl.client.registry.display.DisplayRegistryImpl;
Expand Down Expand Up @@ -102,6 +103,10 @@ public Type<? extends CustomPacketPayload> type() {

@Environment(EnvType.CLIENT)
public void handle(NetworkManager.PacketContext context) {
if (ConfigObject.getInstance().isForceLocalRecipes() && !net.minecraft.client.Minecraft.getInstance().isLocalServer()) {
InternalLogger.getInstance().info("[REI Server Display Sync] Ignoring server display sync because forceLocalRecipes is enabled.");
return;
}
DisplayRegistryImpl registry = (DisplayRegistryImpl) DisplayRegistry.getInstance();
if (syncType() == SyncType.SET) {
InternalLogger.getInstance().info("[REI Server Display Sync] Received server's request to set %d recipes.", displays().size());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,21 @@ public class ServerDisplayRegistryImpl extends AbstractDisplayRegistry<REICommon
private static final Comparator<RecipeHolder<?>> RECIPE_COMPARATOR = Comparator.comparing((RecipeHolder<?> o) -> o.id().location().getNamespace()).thenComparing(o -> o.id().location().getPath());
private final Object2LongMap<UUID> playerVersionMap = new Object2LongOpenHashMap<>();
private int reloadVersionHash = UUID.randomUUID().hashCode();
@Nullable
private static java.util.function.Supplier<List<RecipeHolder<?>>> clientRecipeSupplier;
@Nullable
private static List<Display> pendingForceLocalDisplays;

public static void setClientRecipeSupplier(@Nullable java.util.function.Supplier<List<RecipeHolder<?>>> supplier) {
clientRecipeSupplier = supplier;
}

@Nullable
public static List<Display> consumePendingForceLocalDisplays() {
List<Display> displays = pendingForceLocalDisplays;
pendingForceLocalDisplays = null;
return displays;
}

public ServerDisplayRegistryImpl() {
super(ServerDisplaysHolder::new);
Expand Down Expand Up @@ -207,6 +222,24 @@ public boolean add(Display display, @Nullable Object origin) {
public void endReload() {
InternalLogger.getInstance().debug("Found preliminary %d displays", size());
fillRecipes();

// If force-local recipes were loaded, store displays for client registry injection
// On a remote server with forceLocalRecipes, the normal DisplaySyncPacket path won't work,
// so we need to pass displays directly to the client DisplayRegistryImpl.
try {
if (me.shedaniel.rei.api.client.config.ConfigObject.getInstance().isForceLocalRecipes()
&& !net.minecraft.client.Minecraft.getInstance().isLocalServer()
&& size() > 0) {
List<Display> allDisplays = new ArrayList<>();
for (List<Display> displays : getAll().values()) {
allDisplays.addAll(displays);
}
pendingForceLocalDisplays = allDisplays;
InternalLogger.getInstance().info("[Force Local Recipes] Stored %d displays for client registry injection", allDisplays.size());
}
} catch (Exception e) {
InternalLogger.getInstance().debug("[Force Local Recipes] Could not check force-local state: %s", e.getMessage());
}
}

private void fillRecipes() {
Expand All @@ -227,7 +260,27 @@ private void fillRecipes() {
}

private List<RecipeHolder<?>> getAllSortedRecipes() {
return GameInstance.getServer().getRecipeManager().getRecipes().parallelStream().sorted(RECIPE_COMPARATOR).toList();
try {
var server = GameInstance.getServer();
if (server != null) {
return server.getRecipeManager().getRecipes()
.parallelStream().sorted(RECIPE_COMPARATOR).toList();
}
} catch (Exception e) {
InternalLogger.getInstance().error("Failed to get recipes from server: %s", e.getMessage());
}
if (clientRecipeSupplier != null) {
try {
List<RecipeHolder<?>> recipes = clientRecipeSupplier.get();
if (!recipes.isEmpty()) {
InternalLogger.getInstance().info("[Force Local Recipes] Loaded %d recipes from client-side provider", recipes.size());
}
return recipes;
} catch (Exception e) {
InternalLogger.getInstance().error("[Force Local Recipes] Failed to get client-side recipes", e);
}
}
return Collections.emptyList();
}

public static class ServerDisplaysHolder extends DisplaysHolderImpl {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,8 @@
"config.rei.options.filtering.filter_displays.desc": "Apply filtering rules to determine the visibility of displays. Displays with all entries filtered will be hidden.",
"config.rei.options.filtering.merge_displays": "Merge Displays",
"config.rei.options.filtering.merge_displays.desc": "Merge displays with the same recipe. This is useful for unifying recipes.",
"config.rei.options.filtering.force_local_recipes": "Force Local Recipes (Ignore Server Sync)",
"config.rei.options.filtering.force_local_recipes.desc": "Loads recipes from client resources instead of waiting for server synchronization. Useful for multiplayer servers (e.g. 2b2t) that do not send recipe data. §eWarning: Some recipes may not exist on the server. §eRequires reconnecting to the server after toggling.",
"config.rei.options.groups.list.entries": "Entries",
"config.rei.options.list.display_mode": "Display Mode",
"config.rei.options.list.display_mode.desc": "The way entries are laid out. Paginated mode displays entries in pages, where there are buttons to traverse the different pages. Scrolled mode displays entries in a vertical list.",
Expand Down