From 0be2082fe7d0830bcec57371decfe56a216e2885 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sat, 20 Feb 2021 00:41:40 +0100 Subject: [PATCH] Split off highly version dependent code from ReplayMod --- .../java/com/replaymod/core/ReplayMod.java | 404 ++---------------- .../com/replaymod/core/ReplayModBackend.java | 21 + .../com/replaymod/core/versions/MCVer.java | 8 + .../core/versions/scheduler/Scheduler.java | 30 ++ .../versions/scheduler/SchedulerImpl.java | 124 ++++++ .../com/replaymod/extras/OpenEyeExtra.java | 4 +- .../handler/ConnectionEventHandler.java | 3 +- .../replaymod/replay/InputReplayTimer.java | 21 +- src/main/resources/fabric.mod.json | 2 +- .../com/replaymod/core/ReplayModBackend.java | 85 ++++ .../versions/scheduler/SchedulerImpl.java | 131 ++++++ .../com/replaymod/core/ReplayModBackend.java | 11 + 12 files changed, 443 insertions(+), 401 deletions(-) create mode 100644 src/main/java/com/replaymod/core/ReplayModBackend.java create mode 100644 src/main/java/com/replaymod/core/versions/scheduler/Scheduler.java create mode 100644 src/main/java/com/replaymod/core/versions/scheduler/SchedulerImpl.java create mode 100644 versions/1.12.2/src/main/java/com/replaymod/core/ReplayModBackend.java create mode 100644 versions/1.12.2/src/main/java/com/replaymod/core/versions/scheduler/SchedulerImpl.java create mode 100644 versions/1.14.4-forge/src/main/java/com/replaymod/core/ReplayModBackend.java diff --git a/src/main/java/com/replaymod/core/ReplayMod.java b/src/main/java/com/replaymod/core/ReplayMod.java index c08416db..797ea79e 100644 --- a/src/main/java/com/replaymod/core/ReplayMod.java +++ b/src/main/java/com/replaymod/core/ReplayMod.java @@ -5,8 +5,9 @@ import com.replaymod.compat.ReplayModCompat; import com.replaymod.core.gui.GuiBackgroundProcesses; import com.replaymod.core.gui.GuiReplaySettings; import com.replaymod.core.gui.RestoreReplayGui; -import com.replaymod.core.mixin.MinecraftAccessor; import com.replaymod.core.versions.MCVer; +import com.replaymod.core.versions.scheduler.Scheduler; +import com.replaymod.core.versions.scheduler.SchedulerImpl; import com.replaymod.editor.ReplayModEditor; import com.replaymod.extras.ReplayModExtras; import com.replaymod.recording.ReplayModRecording; @@ -17,7 +18,6 @@ import com.replaymod.replaystudio.replay.ZipReplayFile; import com.replaymod.replaystudio.studio.ReplayStudio; import com.replaymod.replaystudio.us.myles.ViaVersion.api.protocol.ProtocolVersion; import com.replaymod.replaystudio.util.I18n; -import com.replaymod.replaystudio.viaversion.ViaVersionPacketConverter; import com.replaymod.simplepathing.ReplayModSimplePathing; import de.johni0702.minecraft.gui.container.GuiScreen; import net.minecraft.client.MinecraftClient; @@ -28,63 +28,13 @@ import net.minecraft.text.Style; import net.minecraft.text.LiteralText; import net.minecraft.text.TranslatableText; import net.minecraft.util.Formatting; -import net.minecraft.util.crash.CrashException; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FileUtils; //#if MC>=11400 import net.minecraft.client.options.Option; -import net.minecraft.util.thread.ReentrantThreadExecutor; -//#endif - -//#if FABRIC>=1 -import net.fabricmc.api.ClientModInitializer; -import net.fabricmc.loader.api.FabricLoader; //#else -//$$ import com.google.common.util.concurrent.ListenableFutureTask; -//$$ import net.minecraftforge.eventbus.api.SubscribeEvent; -//$$ import java.util.Queue; -//$$ import java.util.concurrent.FutureTask; -//$$ -//#if MC>=11400 -//$$ import com.replaymod.core.versions.LangResourcePack; -//$$ import net.minecraft.resources.IPackFinder; -//$$ import net.minecraft.resources.ResourcePackInfo; -//$$ import net.minecraftforge.fml.DeferredWorkQueue; -//$$ import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; -//$$ import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; -//$$ import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; -//$$ import net.minecraftforge.versions.mcp.MCPVersion; -//#else -//$$ import static com.replaymod.core.versions.MCVer.FML_BUS; -//$$ import net.minecraft.client.resources.IResourcePack; -//$$ import net.minecraftforge.common.config.Configuration; -//#endif -//$$ -//#if MC>=10800 -//$$ import net.minecraft.client.GameSettings; -//#endif -//$$ -//#if MC>=11400 -//$$ import net.minecraftforge.fml.ModList; -//#else -//$$ import net.minecraftforge.fml.common.Loader; -//$$ import net.minecraftforge.fml.common.Mod.EventHandler; -//$$ import net.minecraftforge.fml.common.Mod.Instance; -//$$ import net.minecraftforge.fml.common.event.FMLInitializationEvent; -//$$ import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; -//#if MC>=10800 -//$$ import net.minecraftforge.fml.client.FMLClientHandler; -//#else -//$$ import com.replaymod.replay.InputReplayTimer; -//$$ -//$$ import java.util.ArrayDeque; -//#endif -//#endif -//$$ import net.minecraftforge.fml.common.Mod; -//#if MC<11400 -//$$ import net.minecraftforge.fml.common.gameevent.TickEvent; -//#endif +//$$ import net.minecraft.client.settings.GameSettings; //#endif import java.io.ByteArrayInputStream; @@ -102,43 +52,9 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -//#if FABRIC<=0 -//#if MC>=11400 -//$$ @Mod(ReplayMod.MOD_ID) -//#else -//$$ @Mod(modid = ReplayMod.MOD_ID, -//$$ useMetadata = true, -//$$ version = "@MOD_VERSION@", -//$$ acceptedMinecraftVersions = "@MC_VERSION@", -//$$ acceptableRemoteVersions = "*", - //#if MC>=10800 - //$$ clientSideOnly = true, - //$$ updateJSON = "https://raw.githubusercontent.com/ReplayMod/ReplayMod/master/versions.json", - //#endif -//$$ guiFactory = "com.replaymod.core.gui.GuiFactory") -//#endif -//#endif -public class ReplayMod implements - //#if FABRIC>=1 - ClientModInitializer, - //#endif - Module -{ - - public static String getMinecraftVersion() { - //#if MC>=11400 - return MinecraftClient.getInstance().getGame().getVersion().getName(); - //#else - //#if MC>=11400 - //$$ return MCPVersion.getMCVersion(); - //#else - //$$ return Loader.MC_VERSION; - //#endif - //#endif - } +public class ReplayMod implements Module, Scheduler { public static final String MOD_ID = "replaymod"; @@ -148,23 +64,15 @@ public class ReplayMod implements private static final MinecraftClient mc = MCVer.getMinecraft(); - //#if MC<11400 - //$$ @Deprecated - //$$ public static Configuration config; - //#endif - + private final ReplayModBackend backend; + private final SchedulerImpl scheduler = new SchedulerImpl(); private final KeyBindingRegistry keyBindingRegistry = new KeyBindingRegistry(); private final SettingsRegistry settingsRegistry = new SettingsRegistry(); { settingsRegistry.register(Setting.class); } - // The instance of your mod that Forge uses. - //#if MC>=11400 { instance = this; } - //#else - //$$ @Instance(MOD_ID) - //#endif public static ReplayMod instance; private final List modules = new ArrayList<>(); @@ -181,15 +89,15 @@ public class ReplayMod implements */ private boolean minimalMode; - public ReplayMod() { + public ReplayMod(ReplayModBackend backend) { + this.backend = backend; + I18n.setI18n(net.minecraft.client.resource.language.I18n::translate); - //#if MC>=11400 // Check Minecraft protocol version for compatibility if (!ProtocolVersion.isRegistered(MCVer.getProtocolVersion()) && !Boolean.parseBoolean(System.getProperty("replaymod.skipversioncheck", "false"))) { minimalMode = true; } - //#endif // Register all RM modules modules.add(this); @@ -205,16 +113,6 @@ public class ReplayMod implements settingsRegistry.register(); } - //#if MC<=11400 - //$$ @EventHandler - //$$ public void init(FMLPreInitializationEvent event) { - //$$ config = new Configuration(event.getSuggestedConfigurationFile()); - //$$ config.load(); - //$$ settingsRegistry.backend.setConfiguration(config); - //$$ settingsRegistry.save(); // Save default values to disk - //$$ } - //#endif - public KeyBindingRegistry getKeyBindingRegistry() { return keyBindingRegistry; } @@ -273,19 +171,14 @@ public class ReplayMod implements return replayFolder.resolve(relative); } - public static final DirectoryResourcePack jGuiResourcePack; + public static final DirectoryResourcePack jGuiResourcePack = createJGuiResourcePack(); public static final String JGUI_RESOURCE_PACK_NAME = "replaymod_jgui"; - static { // Note: even preInit is too late and we'd have to issue another resource reload - jGuiResourcePack = initJGuiResourcePack(); - } - - private static DirectoryResourcePack initJGuiResourcePack() { + private static DirectoryResourcePack createJGuiResourcePack() { File folder = new File("../jGui/src/main/resources"); if (!folder.exists()) { return null; } - //noinspection UnnecessaryLocalVariable - DirectoryResourcePack jGuiResourcePack = new DirectoryResourcePack(folder) { + return new DirectoryResourcePack(folder) { @Override //#if MC>=11400 public String getName() { @@ -313,56 +206,13 @@ public class ReplayMod implements } } }; - - //#if MC<=11202 - //$$ @SuppressWarnings("unchecked") - //$$ List defaultResourcePacks = ((MinecraftAccessor) mc).getDefaultResourcePacks(); - //$$ defaultResourcePacks.add(jGuiResourcePack); - //#endif - - //#if MC<=10710 - //$$ FolderResourcePack mainResourcePack = new FolderResourcePack(new File("../src/main/resources")) { - //$$ @Override - //$$ protected InputStream getInputStreamByName(String resourceName) throws IOException { - //$$ try { - //$$ return super.getInputStreamByName(resourceName); - //$$ } catch (IOException e) { - //$$ if ("pack.mcmeta".equals(resourceName)) { - //$$ return new ByteArrayInputStream(("{\"pack\": {\"description\": \"dummy pack for mod resources in dev-env\", \"pack_format\": 1}}").getBytes(StandardCharsets.UTF_8)); - //$$ } - //$$ throw e; - //$$ } - //$$ } - //$$ }; - //$$ defaultResourcePacks.add(mainResourcePack); - //#endif - - return jGuiResourcePack; } - //#if FABRIC>=1 - @Override - public void onInitializeClient() { + void initModules() { modules.forEach(Module::initCommon); modules.forEach(Module::initClient); modules.forEach(m -> m.registerKeyBindings(keyBindingRegistry)); } - //#else - //#if MC>=11400 - //$$ { - //$$ FMLJavaModLoadingContext.get().getModEventBus().addListener((FMLCommonSetupEvent event) -> modules.forEach(Module::initCommon)); - //$$ FMLJavaModLoadingContext.get().getModEventBus().addListener((FMLClientSetupEvent event) -> modules.forEach(Module::initClient)); - //$$ FMLJavaModLoadingContext.get().getModEventBus().addListener((FMLClientSetupEvent event) -> modules.forEach(m -> m.registerKeyBindings(keyBindingRegistry))); - //$$ } - //#else - //$$ @EventHandler - //$$ public void init(FMLInitializationEvent event) { - //$$ modules.forEach(Module::initCommon); - //$$ modules.forEach(Module::initClient); - //$$ modules.forEach(m -> m.registerKeyBindings(keyBindingRegistry)); - //$$ } - //#endif - //#endif @Override public void registerKeyBindings(KeyBindingRegistry registry) { @@ -373,10 +223,6 @@ public class ReplayMod implements @Override public void initClient() { - //#if MC<=10710 - //$$ FML_BUS.register(this); // For runLater(Runnable) - //#endif - backgroundProcesses.register(); keyBindingRegistry.register(); @@ -391,8 +237,6 @@ public class ReplayMod implements } //#endif - testIfMoeshAndExitMinecraft(); - runPostStartup(() -> { final long DAYS = 24 * 60 * 60 * 1000; @@ -491,231 +335,33 @@ public class ReplayMod implements }); } - /** - * Execute the given runnable on the main client thread, returning only after it has been run (or after 30 seconds). - */ + @Override public void runSync(Runnable runnable) throws InterruptedException, ExecutionException, TimeoutException { - //#if MC>=11400 - if (mc.isOnThread()) { - runnable.run(); - } else { - executor.submit(() -> { - runnable.run(); - return null; - }).get(30, TimeUnit.SECONDS); - } - //#else - //$$ if (mc.isCallingFromMinecraftThread()) { - //$$ runnable.run(); - //$$ } else { - //$$ FutureTask future = new FutureTask<>(runnable, null); - //$$ runLater(future); - //$$ future.get(30, TimeUnit.SECONDS); - //$$ } - //#endif + scheduler.runSync(runnable); } - /** - * Execute the given runnable after game has started (once the overlay has been closed). - * Most importantly, it will run after resources (including language keys!) have been loaded. - * Below 1.14, this is equivalent to {@link #runLater(Runnable)}. - */ + @Override public void runPostStartup(Runnable runnable) { - runLater(new Runnable() { - @Override - public void run() { - //#if MC>=11400 - if (getMinecraft().overlay != null) { - // delay until after resources have been loaded - runLater(this); - return; - } - //#endif - runnable.run(); - } - }); + scheduler.runPostStartup(runnable); } - /** - * Set when the currently running code has been scheduled by runLater. - * If this is the case, subsequent calls to runLater have to be delayed until all scheduled tasks have been - * processed, otherwise a livelock may occur. - */ - private boolean inRunLater = false; - //#if MC>=11400 - private boolean inRenderTaskQueue = false; - // Starting 1.14 MC clears the queue of scheduled tasks on disconnect. - // This works fine for MC since it uses the queue only for packet handling but breaks our assumption that - // stuff submitted via runLater is actually always run (e.g. recording might not be fully stopped because parts - // of that are run via runLater and stopping the recording happens right around the time MC clears the queue). - // Luckily, that's also the version where MC pulled out the executor implementation, so we can just spin up our own. - public static class ReplayModExecutor extends ReentrantThreadExecutor { - private final Thread mcThread = Thread.currentThread(); - // Fail-fast in case we ever switch to async loading and forget to change this - // (except for fabric 1.15+ because it loads the mod before the client thread is set) - //#if FABRIC<1 || MC<11500 - //$$ { if (!MinecraftClient.getInstance().isOnThread()) throw new RuntimeException(); } - //#endif - - private ReplayModExecutor(String string_1) { - super(string_1); - } - - @Override - protected Runnable createTask(Runnable runnable) { - return runnable; - } - - @Override - protected boolean canExecute(Runnable runnable) { - return true; - } - - @Override - protected Thread getThread() { - return mcThread; - } - - @Override - public void runTasks() { - super.runTasks(); - } - } - public final ReplayModExecutor executor = new ReplayModExecutor("Client/ReplayMod"); - //#endif - - // Pre-1.14 MC would hold the lock on the scheduledTasks queue while executing its tasks - // such that no new tasks could be submitted while one of them was running. - // This would cause issues with long-running tasks (e.g. video rendering) as it would - // block all async tasks (e.g. skin loading). + @Override public void runLaterWithoutLock(Runnable runnable) { - //#if MC>=11400 - // MC 1.14+ no longer synchronizes on the queue while running its tasks - runLater(runnable); - //#else - //$$ runLater(() -> runLaterWithoutLock(runnable), runnable); - //#endif + scheduler.runLaterWithoutLock(runnable); } + @Override public void runLater(Runnable runnable) { - runLater(runnable, () -> runLater(runnable)); + scheduler.runLater(runnable); } - private void runLater(Runnable runnable, Runnable defer) { - //#if MC>=11400 - if (mc.isOnThread() && inRunLater && !inRenderTaskQueue) { - ((MinecraftAccessor) mc).getRenderTaskQueue().offer(() -> { - inRenderTaskQueue = true; - try { - defer.run(); - } finally { - inRenderTaskQueue = false; - } - }); - } else { - executor.send(() -> { - inRunLater = true; - try { - runnable.run(); - } catch (CrashException e) { - e.printStackTrace(); - System.err.println(e.getReport().asString()); - mc.setCrashReport(e.getReport()); - } finally { - inRunLater = false; - } - }); - } - //#else - //$$ if (mc.isCallingFromMinecraftThread() && inRunLater) { - //#if MC>=10800 - //$$ FML_BUS.register(new Object() { - //$$ @SubscribeEvent - //$$ public void onRenderTick(TickEvent.RenderTickEvent event) { - //$$ if (event.phase == TickEvent.Phase.START) { - //$$ FML_BUS.unregister(this); - //$$ defer.run(); - //$$ } - //$$ } - //$$ }); - //#else - //$$ FML_BUS.register(new RunLaterHelper(defer)); - //#endif - //$$ return; - //$$ } - //#if MC>=10800 - //$$ Queue> tasks = ((MinecraftAccessor) mc).getScheduledTasks(); - //$$ //noinspection SynchronizationOnLocalVariableOrMethodParameter - //$$ synchronized (tasks) { - //#else - //$$ Queue> tasks = scheduledTasks; - //$$ synchronized (scheduledTasks) { - //#endif - //$$ tasks.add(ListenableFutureTask.create(() -> { - //$$ inRunLater = true; - //$$ try { - //$$ runnable.run(); - //$$ } catch (ReportedException e) { - //$$ e.printStackTrace(); - //$$ System.err.println(e.getCrashReport().getCompleteReport()); - //$$ mc.crashed(e.getCrashReport()); - //$$ } finally { - //$$ inRunLater = false; - //$$ } - //$$ }, null)); - //$$ } - //#endif + @Override + public void runTasks() { + scheduler.runTasks(); } - //#if MC<=10710 - //$$ // 1.7.10: Cannot use MC's because it is processed only during ticks (so not at all when replay is paused) - //$$ private final Queue> scheduledTasks = new ArrayDeque<>(); - //$$ - //$$ // in 1.7.10 apparently events can't be delivered to anonymous classes - //$$ public class RunLaterHelper { - //$$ private final Runnable defer; - //$$ - //$$ private RunLaterHelper(Runnable defer) { - //$$ this.defer = defer; - //$$ } - //$$ - //$$ @SubscribeEvent - //$$ public void onRenderTick(TickEvent.RenderTickEvent event) { - //$$ if (event.phase == TickEvent.Phase.START) { - //$$ FML_BUS.unregister(this); - //$$ defer.run(); - //$$ } - //$$ } - //$$ } - //$$ - //$$ @SubscribeEvent - //$$ public void runScheduledTasks(InputReplayTimer.RunScheduledTasks event) { - //$$ synchronized (scheduledTasks) { - //$$ while (!scheduledTasks.isEmpty()) { - //$$ scheduledTasks.poll().run(); - //$$ } - //$$ } - //$$ } - //#endif - public String getVersion() { - //#if FABRIC>=1 - return FabricLoader.getInstance().getModContainer(MOD_ID) - .orElseThrow(IllegalStateException::new) - .getMetadata().getVersion().toString(); - //#else - //#if MC>=11400 - //$$ return ModList.get().getModContainerById(MOD_ID).get().getModInfo().getVersion().toString(); - //#else - //$$ return Loader.instance().getIndexedModList().get(MOD_ID).getVersion(); - //#endif - //#endif - } - - private void testIfMoeshAndExitMinecraft() { - if("currentPlayer".equals("Moesh")) { - System.exit(-1); - } + return backend.getVersion(); } public MinecraftClient getMinecraft() { diff --git a/src/main/java/com/replaymod/core/ReplayModBackend.java b/src/main/java/com/replaymod/core/ReplayModBackend.java new file mode 100644 index 00000000..826f10f9 --- /dev/null +++ b/src/main/java/com/replaymod/core/ReplayModBackend.java @@ -0,0 +1,21 @@ +package com.replaymod.core; + +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.loader.api.FabricLoader; + +import static com.replaymod.core.ReplayMod.MOD_ID; + +public class ReplayModBackend implements ClientModInitializer { + private final ReplayMod mod = new ReplayMod(this); + + @Override + public void onInitializeClient() { + mod.initModules(); + } + + public String getVersion() { + return FabricLoader.getInstance().getModContainer(MOD_ID) + .orElseThrow(IllegalStateException::new) + .getMetadata().getVersion().toString(); + } +} diff --git a/src/main/java/com/replaymod/core/versions/MCVer.java b/src/main/java/com/replaymod/core/versions/MCVer.java index 75d17f50..83e44b1c 100644 --- a/src/main/java/com/replaymod/core/versions/MCVer.java +++ b/src/main/java/com/replaymod/core/versions/MCVer.java @@ -176,6 +176,14 @@ public class MCVer { ); } + public static String getMinecraftVersion() { + //#if MC>=11400 + return getMinecraft().getGame().getVersion().getName(); + //#else + //$$ return Loader.MC_VERSION; + //#endif + } + public static void addDetail(CrashReportSection category, String name, Callable callable) { //#if MC>=10904 //#if MC>=11200 diff --git a/src/main/java/com/replaymod/core/versions/scheduler/Scheduler.java b/src/main/java/com/replaymod/core/versions/scheduler/Scheduler.java new file mode 100644 index 00000000..df66ddc3 --- /dev/null +++ b/src/main/java/com/replaymod/core/versions/scheduler/Scheduler.java @@ -0,0 +1,30 @@ +package com.replaymod.core.versions.scheduler; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +public interface Scheduler { + /** + * Execute the given runnable on the main client thread, returning only after it has been run (or after 30 seconds). + */ + void runSync(Runnable runnable) throws InterruptedException, ExecutionException, TimeoutException; + + /** + * Execute the given runnable after game has started (once the overlay has been closed). + * Most importantly, it will run after resources (including language keys!) have been loaded. + * Below 1.14, this is equivalent to {@link #runLater(Runnable)}. + */ + void runPostStartup(Runnable runnable); + + /** + * Pre-1.14 MC would hold the lock on the scheduledTasks queue while executing its tasks + * such that no new tasks could be submitted while one of them was running. + * This would cause issues with long-running tasks (e.g. video rendering) as it would + * block all async tasks (e.g. skin loading). + */ + void runLaterWithoutLock(Runnable runnable); + + void runLater(Runnable runnable); + + void runTasks(); +} diff --git a/src/main/java/com/replaymod/core/versions/scheduler/SchedulerImpl.java b/src/main/java/com/replaymod/core/versions/scheduler/SchedulerImpl.java new file mode 100644 index 00000000..2da1760a --- /dev/null +++ b/src/main/java/com/replaymod/core/versions/scheduler/SchedulerImpl.java @@ -0,0 +1,124 @@ +package com.replaymod.core.versions.scheduler; + +import com.replaymod.core.mixin.MinecraftAccessor; +import net.minecraft.client.MinecraftClient; +import net.minecraft.util.crash.CrashException; +import net.minecraft.util.thread.ReentrantThreadExecutor; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class SchedulerImpl implements Scheduler { + private static final MinecraftClient mc = MinecraftClient.getInstance(); + + @Override + public void runSync(Runnable runnable) throws InterruptedException, ExecutionException, TimeoutException { + if (mc.isOnThread()) { + runnable.run(); + } else { + executor.submit(() -> { + runnable.run(); + return null; + }).get(30, TimeUnit.SECONDS); + } + } + + @Override + public void runPostStartup(Runnable runnable) { + runLater(new Runnable() { + @Override + public void run() { + if (mc.overlay != null) { + // delay until after resources have been loaded + runLater(this); + return; + } + runnable.run(); + } + }); + } + + /** + * Set when the currently running code has been scheduled by runLater. + * If this is the case, subsequent calls to runLater have to be delayed until all scheduled tasks have been + * processed, otherwise a livelock may occur. + */ + private boolean inRunLater = false; + private boolean inRenderTaskQueue = false; + // Starting 1.14 MC clears the queue of scheduled tasks on disconnect. + // This works fine for MC since it uses the queue only for packet handling but breaks our assumption that + // stuff submitted via runLater is actually always run (e.g. recording might not be fully stopped because parts + // of that are run via runLater and stopping the recording happens right around the time MC clears the queue). + // Luckily, that's also the version where MC pulled out the executor implementation, so we can just spin up our own. + public static class ReplayModExecutor extends ReentrantThreadExecutor { + private final Thread mcThread = Thread.currentThread(); + + private ReplayModExecutor(String string_1) { + super(string_1); + } + + @Override + protected Runnable createTask(Runnable runnable) { + return runnable; + } + + @Override + protected boolean canExecute(Runnable runnable) { + return true; + } + + @Override + protected Thread getThread() { + return mcThread; + } + + @Override + public void runTasks() { + super.runTasks(); + } + } + public final ReplayModExecutor executor = new ReplayModExecutor("Client/ReplayMod"); + + @Override + public void runTasks() { + executor.runTasks(); + } + + @Override + public void runLaterWithoutLock(Runnable runnable) { + // MC 1.14+ no longer synchronizes on the queue while running its tasks + runLater(runnable); + } + + @Override + public void runLater(Runnable runnable) { + runLater(runnable, () -> runLater(runnable)); + } + + private void runLater(Runnable runnable, Runnable defer) { + if (mc.isOnThread() && inRunLater && !inRenderTaskQueue) { + ((MinecraftAccessor) mc).getRenderTaskQueue().offer(() -> { + inRenderTaskQueue = true; + try { + defer.run(); + } finally { + inRenderTaskQueue = false; + } + }); + } else { + executor.send(() -> { + inRunLater = true; + try { + runnable.run(); + } catch (CrashException e) { + e.printStackTrace(); + System.err.println(e.getReport().asString()); + mc.setCrashReport(e.getReport()); + } finally { + inRunLater = false; + } + }); + } + } +} diff --git a/src/main/java/com/replaymod/extras/OpenEyeExtra.java b/src/main/java/com/replaymod/extras/OpenEyeExtra.java index 49675f01..9c7b6941 100644 --- a/src/main/java/com/replaymod/extras/OpenEyeExtra.java +++ b/src/main/java/com/replaymod/extras/OpenEyeExtra.java @@ -28,7 +28,7 @@ import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY; import static com.replaymod.extras.ReplayModExtras.LOGGER; public class OpenEyeExtra implements Extra { - private static final String DOWNLOAD_URL = "https://www.replaymod.com/dl/openeye/" + ReplayMod.getMinecraftVersion(); + private static final String DOWNLOAD_URL = "https://www.replaymod.com/dl/openeye/" + MCVer.getMinecraftVersion(); private ReplayMod mod; @@ -86,7 +86,7 @@ public class OpenEyeExtra implements Extra { GuiPopup popup = new GuiPopup(OfferGui.this); new Thread(() -> { try { - File targetFile = new File(mod.getMinecraft().runDirectory, "mods/" + ReplayMod.getMinecraftVersion() + "/OpenEye.jar"); + File targetFile = new File(mod.getMinecraft().runDirectory, "mods/" + MCVer.getMinecraftVersion() + "/OpenEye.jar"); FileUtils.forceMkdir(targetFile.getParentFile()); HttpsURLConnection connection = (HttpsURLConnection) new URL(DOWNLOAD_URL).openConnection(); diff --git a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java index 9707020a..4c638f50 100644 --- a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java @@ -3,6 +3,7 @@ package com.replaymod.recording.handler; import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.ModCompat; import com.replaymod.core.utils.Utils; +import com.replaymod.core.versions.MCVer; import com.replaymod.editor.gui.MarkerProcessor; import com.replaymod.recording.ServerInfoExt; import com.replaymod.recording.Setting; @@ -137,7 +138,7 @@ public class ConnectionEventHandler { metaData.setCustomServerName(serverName); metaData.setGenerator("ReplayMod v" + ReplayMod.instance.getVersion()); metaData.setDate(System.currentTimeMillis()); - metaData.setMcVersion(ReplayMod.getMinecraftVersion()); + metaData.setMcVersion(MCVer.getMinecraftVersion()); packetListener = new PacketListener(core, outputPath, replayFile, metaData); Channel channel = ((NetworkManagerAccessor) networkManager).getChannel(); channel.pipeline().addBefore(packetHandlerKey, "replay_recorder", packetListener); diff --git a/src/main/java/com/replaymod/replay/InputReplayTimer.java b/src/main/java/com/replaymod/replay/InputReplayTimer.java index 3f5c8ec5..394fdd50 100644 --- a/src/main/java/com/replaymod/replay/InputReplayTimer.java +++ b/src/main/java/com/replaymod/replay/InputReplayTimer.java @@ -1,5 +1,6 @@ package com.replaymod.replay; +import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.WrappedTimer; import com.replaymod.core.versions.MCVer; import com.replaymod.replay.camera.CameraController; @@ -7,10 +8,6 @@ import com.replaymod.replay.camera.CameraEntity; import net.minecraft.client.MinecraftClient; import net.minecraft.client.render.RenderTickCounter; -//#if MC>=11400 -import com.replaymod.core.ReplayMod; -//#endif - //#if MC>=11400 import org.lwjgl.glfw.GLFW; //#else @@ -22,10 +19,7 @@ import org.lwjgl.glfw.GLFW; //$$ import java.io.IOException; //#else //$$ import com.replaymod.replay.gui.screen.GuiOpeningReplay; -//$$ import cpw.mods.fml.common.eventhandler.Event; //$$ import net.minecraft.client.renderer.entity.RenderManager; -//$$ -//$$ import static com.replaymod.core.versions.MCVer.FML_BUS; //#endif //#endif @@ -65,10 +59,9 @@ public class InputReplayTimer extends WrappedTimer { //#endif ); - // 1.7.10: We have to run the scheduled executables (ours only) because MC would only run them every tick + ReplayMod.instance.runTasks(); + //#if MC<=10710 - //$$ FML_BUS.post(new RunScheduledTasks()); - //$$ //$$ // Code below only updates the current screen when a world and player is loaded. This may not be the case for //$$ // the GuiOpeningReplay screen resulting in a livelock. //$$ // To counteract that, we always update that screen (doesn't matter if we do it twice). @@ -77,10 +70,6 @@ public class InputReplayTimer extends WrappedTimer { //$$ } //#endif - //#if MC>=11400 - ReplayMod.instance.executor.runTasks(); - //#endif - // If we are in a replay, we have to manually process key and mouse events as the // tick speed may vary or there may not be any ticks at all (when the replay is paused) if (mod.getReplayHandler() != null && mc.world != null && mc.player != null) { @@ -147,8 +136,4 @@ public class InputReplayTimer extends WrappedTimer { } } } - - //#if MC<=10710 - //$$ public static class RunScheduledTasks extends Event {} - //#endif } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 6bc530eb..8dc61a19 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -20,7 +20,7 @@ "environment": "client", "entrypoints": { "client": [ - "com.replaymod.core.ReplayMod" + "com.replaymod.core.ReplayModBackend" ], "modmenu": [ "com.replaymod.core.gui.ModMenuApiImpl" diff --git a/versions/1.12.2/src/main/java/com/replaymod/core/ReplayModBackend.java b/versions/1.12.2/src/main/java/com/replaymod/core/ReplayModBackend.java new file mode 100644 index 00000000..75fb9e91 --- /dev/null +++ b/versions/1.12.2/src/main/java/com/replaymod/core/ReplayModBackend.java @@ -0,0 +1,85 @@ +package com.replaymod.core; + +import com.replaymod.core.mixin.MinecraftAccessor; +import net.minecraft.client.resources.IResourcePack; +import net.minecraftforge.common.config.Configuration; +import net.minecraftforge.fml.common.Loader; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.common.Mod.EventHandler; +import net.minecraftforge.fml.common.event.FMLInitializationEvent; +import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; + +import java.util.List; + +import static com.replaymod.core.ReplayMod.MOD_ID; +import static com.replaymod.core.ReplayMod.jGuiResourcePack; +import static com.replaymod.core.versions.MCVer.getMinecraft; + +//#if MC<=10710 +//$$ import net.minecraft.client.resources.FolderResourcePack; +//$$ import java.io.ByteArrayInputStream; +//$$ import java.io.File; +//$$ import java.io.IOException; +//$$ import java.io.InputStream; +//$$ import java.nio.charset.StandardCharsets; +//#endif + +@Mod(modid = ReplayMod.MOD_ID, + useMetadata = true, + version = "@MOD_VERSION@", + acceptedMinecraftVersions = "@MC_VERSION@", + acceptableRemoteVersions = "*", + //#if MC>=10800 + clientSideOnly = true, + updateJSON = "https://raw.githubusercontent.com/ReplayMod/ReplayMod/master/versions.json", + //#endif + guiFactory = "com.replaymod.core.gui.GuiFactory") +public class ReplayModBackend { + private final ReplayMod mod = new ReplayMod(this); + + @Deprecated + public static Configuration config; + + @EventHandler + public void init(FMLPreInitializationEvent event) { + config = new Configuration(event.getSuggestedConfigurationFile()); + config.load(); + SettingsRegistry settingsRegistry = mod.getSettingsRegistry(); + settingsRegistry.backend.setConfiguration(config); + settingsRegistry.save(); // Save default values to disk + } + + @EventHandler + public void init(FMLInitializationEvent event) { + mod.initModules(); + } + + public String getVersion() { + return Loader.instance().getIndexedModList().get(MOD_ID).getVersion(); + } + + static { // Note: even preInit is too late and we'd have to issue another resource reload + List defaultResourcePacks = ((MinecraftAccessor) getMinecraft()).getDefaultResourcePacks(); + + if (jGuiResourcePack != null) { + defaultResourcePacks.add(jGuiResourcePack); + } + + //#if MC<=10710 + //$$ FolderResourcePack mainResourcePack = new FolderResourcePack(new File("../src/main/resources")) { + //$$ @Override + //$$ protected InputStream getInputStreamByName(String resourceName) throws IOException { + //$$ try { + //$$ return super.getInputStreamByName(resourceName); + //$$ } catch (IOException e) { + //$$ if ("pack.mcmeta".equals(resourceName)) { + //$$ return new ByteArrayInputStream(("{\"pack\": {\"description\": \"dummy pack for mod resources in dev-env\", \"pack_format\": 1}}").getBytes(StandardCharsets.UTF_8)); + //$$ } + //$$ throw e; + //$$ } + //$$ } + //$$ }; + //$$ defaultResourcePacks.add(mainResourcePack); + //#endif + } +} diff --git a/versions/1.12.2/src/main/java/com/replaymod/core/versions/scheduler/SchedulerImpl.java b/versions/1.12.2/src/main/java/com/replaymod/core/versions/scheduler/SchedulerImpl.java new file mode 100644 index 00000000..9938ce24 --- /dev/null +++ b/versions/1.12.2/src/main/java/com/replaymod/core/versions/scheduler/SchedulerImpl.java @@ -0,0 +1,131 @@ +package com.replaymod.core.versions.scheduler; + +import com.google.common.util.concurrent.ListenableFutureTask; +import com.replaymod.core.mixin.MinecraftAccessor; +import net.minecraft.client.Minecraft; +import net.minecraft.util.ReportedException; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; + +import java.util.Queue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +//#if MC<10800 +//$$ import java.util.ArrayDeque; +//#endif + +import static com.replaymod.core.versions.MCVer.FML_BUS; + +public class SchedulerImpl implements Scheduler { + private static final Minecraft mc = Minecraft.getMinecraft(); + + @Override + public void runSync(Runnable runnable) throws InterruptedException, ExecutionException, TimeoutException { + if (mc.isCallingFromMinecraftThread()) { + runnable.run(); + } else { + FutureTask future = new FutureTask<>(runnable, null); + runLater(future); + future.get(30, TimeUnit.SECONDS); + } + } + + @Override + public void runPostStartup(Runnable runnable) { + runLater(runnable); + } + + /** + * Set when the currently running code has been scheduled by runLater. + * If this is the case, subsequent calls to runLater have to be delayed until all scheduled tasks have been + * processed, otherwise a livelock may occur. + */ + private boolean inRunLater = false; + + @Override + public void runLaterWithoutLock(Runnable runnable) { + runLater(() -> runLaterWithoutLock(runnable), runnable); + } + + public void runLater(Runnable runnable) { + runLater(runnable, () -> runLater(runnable)); + } + + private void runLater(Runnable runnable, Runnable defer) { + if (mc.isCallingFromMinecraftThread() && inRunLater) { + //#if MC>=10800 + FML_BUS.register(new Object() { + @SubscribeEvent + public void onRenderTick(TickEvent.RenderTickEvent event) { + if (event.phase == TickEvent.Phase.START) { + FML_BUS.unregister(this); + defer.run(); + } + } + }); + //#else + //$$ FML_BUS.register(new RunLaterHelper(defer)); + //#endif + return; + } + //#if MC>=10800 + Queue> tasks = ((MinecraftAccessor) mc).getScheduledTasks(); + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (tasks) { + //#else + //$$ Queue> tasks = scheduledTasks; + //$$ synchronized (scheduledTasks) { + //#endif + tasks.add(ListenableFutureTask.create(() -> { + inRunLater = true; + try { + runnable.run(); + } catch (ReportedException e) { + e.printStackTrace(); + System.err.println(e.getCrashReport().getCompleteReport()); + mc.crashed(e.getCrashReport()); + } finally { + inRunLater = false; + } + }, null)); + } + } + + //#if MC>=10800 + @Override + public void runTasks() { + } + //#else + //$$ // 1.7.10: Cannot use MC's because it is processed only during ticks (so not at all when replay is paused) + //$$ private final Queue> scheduledTasks = new ArrayDeque<>(); + //$$ + //$$ // in 1.7.10 apparently events can't be delivered to anonymous classes + //$$ public class RunLaterHelper { + //$$ private final Runnable defer; + //$$ + //$$ private RunLaterHelper(Runnable defer) { + //$$ this.defer = defer; + //$$ } + //$$ + //$$ @SubscribeEvent + //$$ public void onRenderTick(TickEvent.RenderTickEvent event) { + //$$ if (event.phase == TickEvent.Phase.START) { + //$$ FML_BUS.unregister(this); + //$$ defer.run(); + //$$ } + //$$ } + //$$ } + //$$ + //$$ @Override + //$$ public void runTasks() { + //$$ synchronized (scheduledTasks) { + //$$ while (!scheduledTasks.isEmpty()) { + //$$ scheduledTasks.poll().run(); + //$$ } + //$$ } + //$$ } + //#endif +} diff --git a/versions/1.14.4-forge/src/main/java/com/replaymod/core/ReplayModBackend.java b/versions/1.14.4-forge/src/main/java/com/replaymod/core/ReplayModBackend.java new file mode 100644 index 00000000..3527ca6d --- /dev/null +++ b/versions/1.14.4-forge/src/main/java/com/replaymod/core/ReplayModBackend.java @@ -0,0 +1,11 @@ +package com.replaymod.core; + +import net.minecraftforge.fml.ModList; + +import static com.replaymod.core.ReplayMod.MOD_ID; + +public class ReplayModBackend { + public String getVersion() { + return ModList.get().getModContainerById(MOD_ID).get().getModInfo().getVersion().toString(); + } +}