Release 2.6.4

This commit is contained in:
Jonas Herzig
2022-03-09 07:09:00 +01:00
66 changed files with 711 additions and 339 deletions

View File

@@ -245,6 +245,7 @@ dependencies {
11701: '1.17.1',
11800: '1.18',
11801: '1.18.1',
11802: '1.18.2',
][mcVersion]
mappings 'net.fabricmc:yarn:' + [
11404: '1.14.4+build.16',
@@ -256,6 +257,7 @@ dependencies {
11701: '1.17.1+build.29:v2',
11800: '1.18+build.1:v2',
11801: '1.18.1+build.1:v2',
11802: '1.18.2+build.1:v2',
][mcVersion]
modImplementation 'net.fabricmc:fabric-loader:0.12.5'
def fabricApiVersion = [
@@ -268,6 +270,7 @@ dependencies {
11701: '0.37.1+1.17',
11800: '0.43.1+1.18',
11801: '0.43.1+1.18',
11802: '0.47.9+1.18.2',
][mcVersion]
def fabricApiModules = [
"api-base",
@@ -338,7 +341,7 @@ dependencies {
shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8'
shadow "com.github.ReplayMod:ReplayStudio:70f59ef", shadeExclusions
shadow "com.github.ReplayMod:ReplayStudio:b5539d1", shadeExclusions
implementation(FABRIC ? dependencies.project(path: jGui.path, configuration: "namedElements") : jGui) {
transitive = false // FG 1.2 puts all MC deps into the compile configuration and we don't want to shade those
@@ -346,21 +349,27 @@ dependencies {
shadow 'com.github.ReplayMod:lwjgl-utils:27dcd66'
if (FABRIC) {
if (mcVersion >= 11800) {
if (mcVersion >= 11802) {
modImplementation 'com.terraformersmc:modmenu:3.1.0'
} else if (mcVersion >= 11800) {
modImplementation 'com.terraformersmc:modmenu:3.0.0'
} else if (mcVersion >= 11700) {
modImplementation 'com.terraformersmc:modmenu:2.0.0-beta.7'
} else if (mcVersion >= 11602) {
modImplementation 'com.terraformersmc:modmenu:1.16.8'
} else if (mcVersion >= 11600) {
modImplementation 'io.github.prospector:modmenu:1.14.0+build.24'
modImplementation('com.terraformersmc:modmenu:1.14.15') {
exclude module: 'fabric-resource-loader-v0' // inappropriate version for 1.16.1
}
} else {
modImplementation 'io.github.prospector.modmenu:ModMenu:1.6.2-92'
modImplementation 'com.terraformersmc:modmenu:1.10.6'
}
}
if (mcVersion >= 11600) {
modCompileOnly 'com.github.IrisShaders:Iris:1.0.0'
modCompileOnly("com.github.IrisShaders:Iris:1.18.x~v1.2.0") {
transitive = false // we do not want to upgrade our libs, we only need this to compile our mixins
}
}
testImplementation 'junit:junit:4.11'

View File

@@ -465,7 +465,7 @@ If you have a Replay in a dark setting (for example at nighttime, or in a cave)
This works as a replacement for the **Night Vision Potion Effect**, without the side effect of a weird sky color.
## Quick Mode [quickmode] (Minecraft 1.9 and up)
## Quick Mode [quickmode]
![](img/quickmode-icon.jpg)
In **Quick Mode**, this clock symbol is displayed in the lower right corner of the screen.
@@ -473,8 +473,6 @@ When you first enable **Quick Mode** in a replay, an internal reference of certa
As a side effect, certain features like particles and second skin layers will not be rendered in the preview.
By default, **Quick Mode** is toggled with `Q`.
**Quick Mode** is available in ReplayMod for Minecraft 1.9.4 and up.
## Player Overview [overview]
![](img/player-overview.jpg)
The **Player Overview** Screen

2
jGui

Submodule jGui updated: c79b62a73e...5e41452b0e

View File

@@ -189,6 +189,7 @@ val doRelease by tasks.registering {
defaultTasks("bundleJar")
preprocess {
val mc11802 = createNode("1.18.2", 11802, "yarn")
val mc11801 = createNode("1.18.1", 11801, "yarn")
val mc11701 = createNode("1.17.1", 11701, "yarn")
val mc11700 = createNode("1.17", 11700, "yarn")
@@ -208,6 +209,7 @@ preprocess {
val mc10800 = createNode("1.8", 10800, "srg")
val mc10710 = createNode("1.7.10", 10710, "srg")
mc11802.link(mc11801)
mc11801.link(mc11701, file("versions/mapping-fabric-1.18.1-1.17.1.txt"))
mc11701.link(mc11700)
mc11700.link(mc11604, file("versions/mapping-fabric-1.17-1.16.4.txt"))

View File

@@ -32,6 +32,7 @@ val jGuiVersions = listOf(
"1.17",
"1.17.1",
"1.18.1",
"1.18.2",
)
val replayModVersions = listOf(
// "1.7.10",
@@ -52,6 +53,7 @@ val replayModVersions = listOf(
"1.17",
"1.17.1",
"1.18.1",
"1.18.2",
)
rootProject.buildFileName = "root.gradle.kts"

View File

@@ -9,6 +9,10 @@ import java.io.IOException;
import java.util.List;
import java.util.Set;
//#if FABRIC
import net.fabricmc.loader.api.FabricLoader;
//#endif
//#if MC>=11400
import java.io.InputStream;
//#else
@@ -34,6 +38,9 @@ public class ReplayModMixinConfigPlugin implements IMixinConfigPlugin {
private final Logger logger = LogManager.getLogger("replaymod/mixin");
private final boolean hasOF = hasClass("optifine.OptiFineForgeTweaker") || hasClass("me.modmuss50.optifabric.mod.Optifabric");
//#if FABRIC
private final boolean hasIris = FabricLoader.getInstance().isModLoaded("iris");
//#endif
{
logger.debug("hasOF: " + hasOF);
@@ -49,6 +56,9 @@ public class ReplayModMixinConfigPlugin implements IMixinConfigPlugin {
}
if (mixinClassName.endsWith("_OF")) return hasOF;
if (mixinClassName.endsWith("_NoOF")) return !hasOF;
//#if FABRIC
if (mixinClassName.endsWith("_Iris")) return hasIris;
//#endif
return true;
}

View File

@@ -22,6 +22,7 @@ import java.nio.file.WatchService;
import java.util.List;
import java.util.Map;
import static com.replaymod.core.utils.Utils.ensureDirectoryExists;
import static com.replaymod.core.versions.MCVer.getMinecraft;
class SettingsRegistryBackend {
@@ -179,7 +180,7 @@ class SettingsRegistryBackend {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
String config = gson.toJson(root);
try {
Files.createDirectories(configFile.getParent());
ensureDirectoryExists(configFile.getParent());
Files.write(configFile, config.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();

View File

@@ -10,6 +10,8 @@ import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import static com.replaymod.core.utils.Utils.ensureDirectoryExists;
public class ReplayFoldersService {
private final Path mcDir = MinecraftClient.getInstance().runDirectory.toPath();
private final SettingsRegistry settings;
@@ -19,14 +21,14 @@ public class ReplayFoldersService {
}
public Path getReplayFolder() throws IOException {
return Files.createDirectories(mcDir.resolve(settings.get(Setting.RECORDING_PATH)));
return ensureDirectoryExists(mcDir.resolve(settings.get(Setting.RECORDING_PATH)));
}
/**
* Folder into which replay backups are saved before the MarkerProcessor is unleashed.
*/
public Path getRawReplayFolder() throws IOException {
return Files.createDirectories(getReplayFolder().resolve("raw"));
return ensureDirectoryExists(getReplayFolder().resolve("raw"));
}
/**
@@ -34,7 +36,7 @@ public class ReplayFoldersService {
* Distinct from the main folder, so they cannot be opened while they are still saving.
*/
public Path getRecordingFolder() throws IOException {
return Files.createDirectories(getReplayFolder().resolve("recording"));
return ensureDirectoryExists(getReplayFolder().resolve("recording"));
}
/**
@@ -42,7 +44,7 @@ public class ReplayFoldersService {
* Distinct from the recording folder cause people kept confusing them with recordings.
*/
public Path getCacheFolder() throws IOException {
Path path = Files.createDirectories(mcDir.resolve(settings.get(Setting.CACHE_PATH)));
Path path = ensureDirectoryExists(mcDir.resolve(settings.get(Setting.CACHE_PATH)));
try {
Files.setAttribute(path, "dos:hidden", true);
} catch (UnsupportedOperationException ignored) {

View File

@@ -56,11 +56,16 @@ public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> {
new GuiLabel().setI18nText("replaymod.gui.restorereplay1"),
new GuiLabel().setI18nText("replaymod.gui.restorereplay2", Files.getNameWithoutExtension(file.getName())),
new GuiLabel().setI18nText("replaymod.gui.restorereplay3"));
LOGGER.info("Found partially saved replay, offering recovery: " + file);
yesButton.onClick(() -> {
LOGGER.info("Attempting recovery: " + file);
recoverInBackground();
parent.display();
});
noButton.onClick(() -> {
LOGGER.info("Recovery rejected, marking for deletion: " + file);
try {
File tmp = new File(file.getParentFile(), file.getName() + ".tmp");
File deleted = new File(file.getParentFile(), file.getName() + ".del");

View File

@@ -63,6 +63,7 @@ public abstract class MixinMinecraft
PostRenderCallback.EVENT.invoker().postRender();
}
//#else
//$$ @Shadow long systemTime;
//#if MC>=10904
//$$ @Shadow protected abstract void runTickKeyboard() throws IOException;
//$$ @Shadow protected abstract void runTickMouse() throws IOException;
@@ -80,6 +81,8 @@ public abstract class MixinMinecraft
//$$ public void replayModRunTickMouse() {
//$$ try {
//$$ runTickMouse();
//$$ // Update last tick time (MC ignores inputs when there hasn't been a tick in 200ms)
//$$ systemTime = Minecraft.getSystemTime();
//$$ } catch (IOException e) {
//$$ e.printStackTrace();
//$$ }
@@ -94,7 +97,12 @@ public abstract class MixinMinecraft
//$$
//$$ @Inject(method = "runTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;sendClickBlockToController(Z)V"), cancellable = true)
//$$ private void doEarlyReturnFromRunTick(CallbackInfo ci) {
//$$ if (earlyReturn) ci.cancel();
//$$ if (earlyReturn) {
//$$ ci.cancel();
//$$
//$$ // Update last tick time (MC ignores inputs when there hasn't been a tick in 200ms)
//$$ systemTime = Minecraft.getSystemTime();
//$$ }
//$$ }
//#endif
//$$ @Redirect(

View File

@@ -55,8 +55,13 @@ import javax.net.ssl.TrustManagerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
@@ -166,8 +171,45 @@ public class Utils {
private static final PercentEscaper REPLAY_NAME_ENCODER = new PercentEscaper(".-_ ", false);
public static String replayNameToFileName(String replayName) {
return REPLAY_NAME_ENCODER.escape(replayName) + ".mcpr";
public static Path replayNameToPath(Path folder, String replayName) {
// If we can, prefer directly using the replay name as the file name
if (isUsable(folder, replayName + ".mcpr")) {
return folder.resolve(replayName + ".mcpr");
} else {
// otherwise, fall back to percent encoding
return folder.resolve(REPLAY_NAME_ENCODER.escape(replayName) + ".mcpr");
}
}
/**
* Checks whether a given file name is actually usable with the file system / operating system at the given folder.
*/
private static boolean isUsable(Path folder, String fileName) {
Path path = folder.resolve(fileName);
if (Files.exists(path)) {
return true; // if it already exits, it's definitely usable
}
// Otherwise, there's no sure way to know, so we just gotta try
try (OutputStream outputStream = Files.newOutputStream(path, StandardOpenOption.CREATE_NEW)) {
outputStream.flush();
} catch (IOException e) {
return false;
}
// Looking good, but now we gotta clean up that mess (and Anti-Virus / Cloud Sync are know to lock them)
int attempts = 0;
while (true) {
try {
Files.delete(path);
return true;
} catch (IOException e) {
if (attempts++ > 100) {
LOGGER.warn("Repeatedly failed to clean up temporary test file at " + path + ": ", e);
return false; // while we were able to use it, it's taken now and we can't get it back
}
}
}
}
public static String fileNameToReplayName(String fileName) {
@@ -356,4 +398,14 @@ public class Utils {
configure.accept(instance);
return instance;
}
/**
* Like {@link Files#createDirectories(Path, FileAttribute[])} but doesn't explode if it's a symlink.
*/
public static Path ensureDirectoryExists(Path path) throws IOException {
// Who in their right mind thought the default behavior of throwing when the target is a link to a directory
// was the preferred behavior?! Everyone has to fall for this at least once to learn it...
// https://bugs.openjdk.java.net/browse/JDK-8130464
return Files.createDirectories(Files.exists(path) ? path.toRealPath() : path);
}
}

View File

@@ -304,7 +304,7 @@ public class MCVer {
//#if MC<=11601
//$$ public static Vec3d getTrackedPosition(Entity entity) {
//$$ return new Vec3d(entity.trackedX, entity.trackedY, entity.trackedZ);
//$$ return new Vec3d(entity.trackedX / 4096.0, entity.trackedY / 4096.0, entity.trackedZ / 4096.0);
//$$ }
//#endif

View File

@@ -21,6 +21,8 @@ import de.johni0702.minecraft.gui.popup.AbstractGuiPopup;
import de.johni0702.minecraft.gui.utils.lwjgl.Color;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import net.minecraft.util.crash.CrashReport;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.file.Path;
@@ -31,6 +33,8 @@ import java.util.function.Consumer;
import java.util.stream.Collectors;
public class GuiEditReplay extends AbstractGuiPopup<GuiEditReplay> {
private static final Logger LOGGER = LogManager.getLogger();
private final Path inputPath;
private final EditTimeline timeline;
@@ -60,6 +64,8 @@ public class GuiEditReplay extends AbstractGuiPopup<GuiEditReplay> {
super(container);
this.inputPath = inputPath;
LOGGER.info("Opening replay in editor: " + inputPath);
try (ReplayFile replayFile = ReplayMod.instance.files.open(inputPath)) {
markers = replayFile.getMarkers().or(HashSet::new);
timeline = new EditTimeline(new HashSet<>(markers), markers -> this.markers = markers);

View File

@@ -133,7 +133,6 @@ public class MarkerProcessor {
for (int i = 1; Files.exists(inputPath); i++) {
inputPath = inputPath.resolveSibling(replayName + "." + i + ".mcpr");
}
Files.createDirectories(inputPath.getParent());
Files.move(path, inputPath);
try (ReplayFile inputReplayFile = mod.files.open(inputPath)) {

View File

@@ -25,7 +25,7 @@ public class QuickMode extends EventRegistrations implements Extra {
return;
}
replayHandler.getReplaySender().setSyncModeAndWait();
mod.runLater(() -> {
mod.runLaterWithoutLock(() -> {
replayHandler.ensureQuickModeInitialized(() -> {
boolean enabled = !replayHandler.isQuickMode();
updateIndicator(replayHandler.getOverlay(), enabled);

View File

@@ -40,7 +40,7 @@ public class GuiCreateScreenshot extends GuiRenderSettings implements Loadable {
new GuiLabel().setI18nText("replaymod.gui.advancedscreenshots.resolution"), videoResolutionPanel,
new GuiLabel().setI18nText("replaymod.gui.rendersettings.outputfile"), outputFileButton);
resetChildren(advancedPanel).addElements(null, nametagCheckbox, new GuiPanel().setLayout(
resetChildren(advancedPanel).addElements(null, nametagCheckbox, alphaCheckbox , new GuiPanel().setLayout(
new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(5).setSpacingY(15))
.addElements(new GridLayout.Data(0, 0.5),
new GuiLabel().setI18nText("replaymod.gui.rendersettings.stabilizecamera"), stabilizePanel,

View File

@@ -155,9 +155,9 @@ public class GuiSavingReplay {
try {
Path replaysFolder = core.folders.getReplayFolder();
Path newPath = replaysFolder.resolve(Utils.replayNameToFileName(newName));
Path newPath = Utils.replayNameToPath(replaysFolder, newName);
for (int i = 1; Files.exists(newPath); i++) {
newPath = replaysFolder.resolve(Utils.replayNameToFileName(newName + " (" + i + ")"));
newPath = Utils.replayNameToPath(replaysFolder, newName + " (" + i + ")");
}
Files.move(path, newPath);
} catch (IOException e) {

View File

@@ -127,7 +127,7 @@ public class ConnectionEventHandler {
}
String name = sdf.format(Calendar.getInstance().getTime());
Path outputPath = core.folders.getRecordingFolder().resolve(Utils.replayNameToFileName(name));
Path outputPath = Utils.replayNameToPath(core.folders.getRecordingFolder(), name);
ReplayFile replayFile = core.files.open(outputPath);
replayFile.writeModInfo(ModCompat.getInstalledNetworkMods());

View File

@@ -33,7 +33,9 @@ public abstract class MixinMouseHelper {
long _p0, double _p1, double _p2,
CallbackInfo ci,
double _l1,
//#if MC>=11400
//#if MC>=11802
//$$ int yOffsetAccumulated
//#elseif MC>=11400
float yOffsetAccumulated
//#else
//$$ double yOffsetAccumulated

View File

@@ -16,6 +16,10 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
//#if MC>=11802
//$$ import net.minecraft.util.registry.RegistryEntry;
//#endif
//#if MC>=11600
import net.minecraft.util.registry.RegistryKey;
import net.minecraft.world.MutableWorldProperties;
@@ -52,7 +56,12 @@ public abstract class MixinWorldClient extends World implements RecordingEventHa
//#if MC<11602
//$$ RegistryKey<DimensionType> registryKey2,
//#endif
DimensionType dimensionType, Supplier<Profiler> profiler, boolean bl, boolean bl2, long l) {
//#if MC>=11802
//$$ RegistryEntry<DimensionType> dimensionType,
//#else
DimensionType dimensionType,
//#endif
Supplier<Profiler> profiler, boolean bl, boolean bl2, long l) {
super(mutableWorldProperties, registryKey,
//#if MC<11602
//$$ registryKey2,

View File

@@ -285,7 +285,6 @@ public class PacketListener extends ChannelInboundHandlerAdapter {
for (int i = 1; Files.exists(rawPath); i++) {
rawPath = rawPath.resolveSibling(replayName + "." + i + ".mcpr");
}
Files.createDirectories(rawPath.getParent());
replayFile.saveTo(rawPath.toFile());
replayFile.close();

View File

@@ -28,9 +28,11 @@ import static org.lwjgl.util.tinyexr.TinyEXR.*;
public class EXRWriter implements FrameConsumer<BitmapFrame> {
private final Path outputFolder;
private final boolean keepAlpha;
public EXRWriter(Path outputFolder) throws IOException {
public EXRWriter(Path outputFolder, boolean keepAlpha) throws IOException {
this.outputFolder = outputFolder;
this.keepAlpha = keepAlpha;
Files.createDirectories(outputFolder);
}
@@ -92,11 +94,15 @@ public class EXRWriter implements FrameConsumer<BitmapFrame> {
bgrChannels[(i + 3) % 4] = channel;
}
}
int alphaMask = keepAlpha ? 0 : 0xff;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
for (FloatBuffer channel : bgrChannels) {
channel.put(((int) bgra.get() & 0xff) / 255f);
}
bgrChannels[0].put(((int) bgra.get() & 0xff) / 255f); // b
bgrChannels[1].put(((int) bgra.get() & 0xff) / 255f); // g
bgrChannels[2].put(((int) bgra.get() & 0xff) / 255f); // r
bgrChannels[3].put(((int) bgra.get() & 0xff | alphaMask) / 255f); // a
}
}
if (depthFrame != null && depthChannel != null) {

View File

@@ -19,9 +19,11 @@ import java.util.Map;
public class PNGWriter implements FrameConsumer<BitmapFrame> {
private final Path outputFolder;
private final boolean keepAlpha;
public PNGWriter(Path outputFolder) throws IOException {
public PNGWriter(Path outputFolder, boolean keepAlpha) throws IOException {
this.outputFolder = outputFolder;
this.keepAlpha = keepAlpha;
Files.createDirectories(outputFolder);
}
@@ -47,6 +49,7 @@ public class PNGWriter implements FrameConsumer<BitmapFrame> {
}
private void withImage(BitmapFrame frame, IOConsumer<Image> consumer) throws IOException {
byte alphaMask = (byte) (keepAlpha ? 0 : 0xff);
ByteBuffer buffer = frame.getByteBuffer();
ReadableDimension size = frame.getSize();
int width = size.getWidth();
@@ -58,7 +61,7 @@ public class PNGWriter implements FrameConsumer<BitmapFrame> {
byte g = buffer.get();
byte r = buffer.get();
byte a = buffer.get();
image.setRGBA(x, y, r, g, b, a);
image.setRGBA(x, y, r, g, b, a | alphaMask);
}
}
consumer.accept(image);

View File

@@ -151,6 +151,7 @@ public class RenderSettings {
private final File outputFile;
private final boolean renderNameTags;
private final boolean includeAlphaChannel;
private final boolean stabilizeYaw;
private final boolean stabilizePitch;
private final boolean stabilizeRoll;
@@ -187,6 +188,7 @@ public class RenderSettings {
false,
false,
false,
false,
null,
360,
180,
@@ -209,6 +211,7 @@ public class RenderSettings {
int bitRate,
File outputFile,
boolean renderNameTags,
boolean includeAlphaChannel,
boolean stabilizeYaw,
boolean stabilizePitch,
boolean stabilizeRoll,
@@ -231,6 +234,7 @@ public class RenderSettings {
this.bitRate = bitRate;
this.outputFile = outputFile;
this.renderNameTags = renderNameTags;
this.includeAlphaChannel = includeAlphaChannel;
this.stabilizeYaw = stabilizeYaw;
this.stabilizePitch = stabilizePitch;
this.stabilizeRoll = stabilizeRoll;
@@ -256,6 +260,7 @@ public class RenderSettings {
bitRate,
outputFile,
renderNameTags,
includeAlphaChannel,
stabilizeYaw,
stabilizePitch,
stabilizeRoll,
@@ -415,6 +420,10 @@ public class RenderSettings {
return renderNameTags;
}
public boolean isIncludeAlphaChannel() {
return includeAlphaChannel;
}
public boolean isStabilizeYaw() {
return stabilizeYaw;
}
@@ -478,6 +487,7 @@ public class RenderSettings {
", bitRate=" + bitRate +
", outputFile=" + outputFile +
", renderNameTags=" + renderNameTags +
", includeAlphaChannel=" + includeAlphaChannel +
", stabilizeYaw=" + stabilizeYaw +
", stabilizePitch=" + stabilizePitch +
", stabilizeRoll=" + stabilizeRoll +

View File

@@ -1,7 +1,6 @@
//#if MC>=11600
package com.replaymod.render.capturer;
import com.mojang.blaze3d.platform.GlStateManager;
import com.replaymod.render.RenderSettings;
import com.replaymod.render.frame.CubicOpenGlFrame;
import com.replaymod.render.frame.ODSOpenGlFrame;
@@ -16,18 +15,13 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import static com.replaymod.core.versions.MCVer.popMatrix;
import static com.replaymod.core.versions.MCVer.pushMatrix;
import static com.replaymod.core.versions.MCVer.resizeMainWindow;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
public class IrisODSFrameCapturer implements FrameCapturer<ODSOpenGlFrame> {
public static final String SHADER_PACK_NAME = "assets/replaymod/iris/ods";
public static IrisODSFrameCapturer INSTANCE;
private final CubicPboOpenGlFrameCapturer left, right;
private final String prevShaderPack;
private final boolean prevShadersEnabled;
private int direction;
private boolean isLeftEye;
@@ -67,13 +61,16 @@ public class IrisODSFrameCapturer implements FrameCapturer<ODSOpenGlFrame> {
right = new CubicStereoFrameCapturer(worldRenderer, fakeInfo, frameSize);
INSTANCE = this;
prevShaderPack = Iris.getIrisConfig().getShaderPackName().orElse(null);
setShaderPack(SHADER_PACK_NAME);
IrisConfig irisConfig = Iris.getIrisConfig();
prevShaderPack = irisConfig.getShaderPackName().orElse(null);
prevShadersEnabled = irisConfig.areShadersEnabled();
setShaderPack(SHADER_PACK_NAME, true);
}
private static void setShaderPack(String name) {
private static void setShaderPack(String name, boolean enabled) {
IrisConfig irisConfig = Iris.getIrisConfig();
irisConfig.setShaderPackName(name);
irisConfig.setShadersEnabled(enabled);
try {
irisConfig.save();
Iris.reload();
@@ -121,7 +118,7 @@ public class IrisODSFrameCapturer implements FrameCapturer<ODSOpenGlFrame> {
left.close();
right.close();
INSTANCE = null;
setShaderPack(prevShaderPack);
setShaderPack(prevShaderPack, prevShadersEnabled);
}
private class CubicStereoFrameCapturer extends CubicPboOpenGlFrameCapturer {
@@ -131,25 +128,8 @@ public class IrisODSFrameCapturer implements FrameCapturer<ODSOpenGlFrame> {
@Override
protected OpenGlFrame renderFrame(int frameId, float partialTicks, CubicOpenGlFrameCapturer.Data captureData) {
resizeMainWindow(mc, getFrameWidth(), getFrameHeight());
pushMatrix();
frameBuffer().beginWrite(true);
GlStateManager.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT
//#if MC>=11400
, false
//#endif
);
GlStateManager.enableTexture();
direction = captureData.ordinal();
worldRenderer.renderWorld(partialTicks, null);
frameBuffer().endWrite();
popMatrix();
return captureFrame(frameId, captureData);
return super.renderFrame(frameId, partialTicks, captureData);
}
}
}

View File

@@ -1,6 +1,5 @@
package com.replaymod.render.capturer;
import com.mojang.blaze3d.platform.GlStateManager;
import com.replaymod.render.rendering.Channel;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import com.replaymod.render.RenderSettings;
@@ -20,12 +19,6 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import static com.replaymod.core.versions.MCVer.popMatrix;
import static com.replaymod.core.versions.MCVer.pushMatrix;
import static com.replaymod.core.versions.MCVer.resizeMainWindow;
import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT;
public class ODSFrameCapturer implements FrameCapturer<ODSOpenGlFrame> {
private static final Identifier vertexResource = new Identifier("replaymod", "shader/ods.vert");
private static final Identifier fragmentResource = new Identifier("replaymod", "shader/ods.frag");
@@ -166,25 +159,8 @@ public class ODSFrameCapturer implements FrameCapturer<ODSOpenGlFrame> {
@Override
protected OpenGlFrame renderFrame(int frameId, float partialTicks, CubicOpenGlFrameCapturer.Data captureData) {
resizeMainWindow(mc, getFrameWidth(), getFrameHeight());
pushMatrix();
frameBuffer().beginWrite(true);
GlStateManager.clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT
//#if MC>=11400
, false
//#endif
);
GlStateManager.enableTexture();
directionVariable.set(captureData.ordinal());
worldRenderer.renderWorld(partialTicks, null);
frameBuffer().endWrite();
popMatrix();
return captureFrame(frameId, captureData);
return super.renderFrame(frameId, partialTicks, captureData);
}
}
}

View File

@@ -90,6 +90,7 @@ public class GuiExportFailed extends GuiScreen {
oldSettings.getBitRate(),
oldSettings.getOutputFile(),
oldSettings.isRenderNameTags(),
oldSettings.isIncludeAlphaChannel(),
oldSettings.isStabilizeYaw(),
oldSettings.isStabilizePitch(),
oldSettings.isStabilizeRoll(),

View File

@@ -0,0 +1,48 @@
package com.replaymod.render.gui;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.layout.VerticalLayout;
import java.net.URI;
import static com.replaymod.core.versions.MCVer.openURL;
import static de.johni0702.minecraft.gui.versions.MCVer.setClipboardString;
public class GuiNoFfmpeg extends GuiScreen {
private static final String LINK = "https://www.replaymod.com/docs/#installing-ffmpeg";
private final GuiLabel message = new GuiLabel()
.setI18nText("replaymod.gui.rendering.error.message");
private final GuiLabel link = new GuiLabel()
.setText(LINK);
private final GuiButton openLinkButton = new GuiButton()
.setI18nLabel("chat.link.open")
.setSize(100, 20)
.onClick(() -> openURL(URI.create(LINK)));
private final GuiButton copyToClipboardButton = new GuiButton()
.setI18nLabel("chat.copy")
.setSize(100, 20)
.onClick(() -> setClipboardString(LINK));
private final GuiButton backButton = new GuiButton()
.setI18nLabel("gui.back")
.setSize(100, 20);
private final GuiPanel buttons = new GuiPanel()
.setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(4))
.addElements(null, openLinkButton, copyToClipboardButton, backButton);
{
setBackground(Background.DIRT);
setTitle(new GuiLabel().setI18nText("replaymod.gui.rendering.error.title"));
setLayout(new VerticalLayout(VerticalLayout.Alignment.CENTER).setSpacing(30));
addElements(new VerticalLayout.Data(0.5), message, link, buttons);
}
public GuiNoFfmpeg(Runnable goBack) {
backButton.onClick(goBack);
}
}

View File

@@ -37,7 +37,6 @@ import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.NoticeScreen;
import net.minecraft.util.crash.CrashReport;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.Pair;
@@ -181,17 +180,7 @@ public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> implements
videoRenderer.renderVideo();
} catch (FFmpegWriter.NoFFmpegException e) {
LOGGER.error("Rendering video:", e);
NoticeScreen errorScreen = new NoticeScreen(
//#if MC>=11400
container::display,
new TranslatableText("replaymod.gui.rendering.error.title"),
new TranslatableText("replaymod.gui.rendering.error.message")
//#else
//$$ I18n.format("replaymod.gui.rendering.error.title"),
//$$ I18n.format("replaymod.gui.rendering.error.message")
//#endif
);
mc.openScreen(errorScreen);
mc.openScreen(new GuiNoFfmpeg(container::display).toMinecraft());
return;
} catch (FFmpegWriter.FFmpegStartupException e) {
int jobsToSkip = jobsDone;
@@ -229,7 +218,7 @@ public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> implements
ReplayFile replayFile = null;
try {
replayFile = mod.getCore().files.open(next.getKey().toPath());
replayHandler = mod.startReplay(replayFile, true, false);
replayHandler = mod.startReplay(replayFile, false, false);
} catch (IOException e) {
Utils.error(LOGGER, container, CrashReport.create(e, "Opening replay"), () -> {});
container.display(); // Re-show the queue popup and the new error popup

View File

@@ -30,7 +30,6 @@ import de.johni0702.minecraft.gui.utils.Utils;
import de.johni0702.minecraft.gui.utils.lwjgl.Color;
import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import net.minecraft.client.gui.screen.NoticeScreen;
import net.minecraft.client.resource.language.I18n;
import net.minecraft.util.crash.CrashReport;
@@ -149,6 +148,9 @@ public class GuiRenderSettings extends AbstractGuiPopup<GuiRenderSettings> {
public final GuiCheckbox nametagCheckbox = new GuiCheckbox()
.setI18nLabel("replaymod.gui.rendersettings.nametags");
public final GuiCheckbox alphaCheckbox = new GuiCheckbox()
.setI18nLabel("replaymod.gui.rendersettings.includealpha");
public final GuiPanel stabilizePanel = new GuiPanel().setLayout(new HorizontalLayout().setSpacing(10));
public final GuiCheckbox stabilizeYaw = new GuiCheckbox(stabilizePanel)
.setI18nLabel("replaymod.gui.yaw");
@@ -188,7 +190,7 @@ public class GuiRenderSettings extends AbstractGuiPopup<GuiRenderSettings> {
.setSize(200, 20).setValues(RenderSettings.AntiAliasing.values()).setSelected(RenderSettings.AntiAliasing.NONE);
public final GuiPanel advancedPanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(15))
.addElements(null, nametagCheckbox, new GuiPanel().setLayout(
.addElements(null, nametagCheckbox, alphaCheckbox, new GuiPanel().setLayout(
new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(5).setSpacingY(15))
.addElements(new GridLayout.Data(0, 0.5),
new GuiLabel().setI18nText("replaymod.gui.rendersettings.stabilizecamera"), stabilizePanel,
@@ -240,17 +242,7 @@ public class GuiRenderSettings extends AbstractGuiPopup<GuiRenderSettings> {
videoRenderer.renderVideo();
} catch (FFmpegWriter.NoFFmpegException e) {
LOGGER.error("Rendering video:", e);
NoticeScreen errorScreen = new NoticeScreen(
//#if MC>=11400
getScreen()::display,
new TranslatableText("replaymod.gui.rendering.error.title"),
new TranslatableText("replaymod.gui.rendering.error.message")
//#else
//$$ I18n.format("replaymod.gui.rendering.error.title"),
//$$ I18n.format("replaymod.gui.rendering.error.message")
//#endif
);
getMinecraft().openScreen(errorScreen);
getMinecraft().openScreen(new GuiNoFfmpeg(getScreen()::display).toMinecraft());
} catch (FFmpegWriter.FFmpegStartupException e) {
GuiExportFailed.tryToRecover(e, newSettings -> {
// Update settings with fixed ffmpeg arguments
@@ -533,6 +525,7 @@ public class GuiRenderSettings extends AbstractGuiPopup<GuiRenderSettings> {
}
outputFileButton.setLabel(this.outputFile.getName());
nametagCheckbox.setChecked(settings.isRenderNameTags());
alphaCheckbox.setChecked(settings.isIncludeAlphaChannel());
stabilizeYaw.setChecked(settings.isStabilizeYaw());
stabilizePitch.setChecked(settings.isStabilizePitch());
stabilizeRoll.setChecked(settings.isStabilizeRoll());
@@ -571,6 +564,7 @@ public class GuiRenderSettings extends AbstractGuiPopup<GuiRenderSettings> {
bitRateField.getInteger() << (10 * bitRateUnit.getSelected()),
serialize && !userDefinedOutputFileName ? getParentFile(outputFile) : outputFile,
nametagCheckbox.isChecked(),
alphaCheckbox.isChecked(),
stabilizeYaw.isChecked() && (serialize || stabilizeYaw.isEnabled()),
stabilizePitch.isChecked() && (serialize || stabilizePitch.isEnabled()),
stabilizeRoll.isChecked() && (serialize || stabilizeRoll.isEnabled()),

View File

@@ -141,7 +141,7 @@ public class GuiVideoRenderer extends GuiScreen implements Tickable {
@Override
public void tick() {
long current = System.currentTimeMillis();
long current = System.nanoTime() / 1_000_000;
//first, update the total render time (only if rendering is not paused and has already started)
if(!renderer.isPaused() && renderer.getFramesDone() > 0 && prevTime > -1) {

View File

@@ -8,6 +8,7 @@ import com.replaymod.render.Setting;
import com.replaymod.render.capturer.CaptureData;
import com.replaymod.render.capturer.RenderInfo;
import com.replaymod.render.capturer.WorldRenderer;
import com.replaymod.render.mixin.GameRendererAccessor;
import com.replaymod.replay.ReplayModReplay;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import net.minecraft.client.MinecraftClient;
@@ -81,11 +82,16 @@ public class EntityRendererHandler extends EventRegistrations implements WorldRe
//#endif
if (mc.world != null && mc.player != null) {
GameRendererAccessor gameRenderer = (GameRendererAccessor) mc.gameRenderer;
Screen orgScreen = mc.currentScreen;
boolean orgPauseOnLostFocus = mc.options.pauseOnLostFocus;
boolean orgRenderHand = gameRenderer.getRenderHand();
try {
mc.currentScreen = null; // do not want to render the current screen (that'd just be the progress gui)
mc.options.pauseOnLostFocus = false; // do not want the pause menu to open if the window is unfocused
if (omnidirectional) {
gameRenderer.setRenderHand(false); // makes no sense, we wouldn't even know where to put it
}
//#if MC>=11400
mc.gameRenderer.render(partialTicks, finishTimeNano, true);
@@ -100,6 +106,7 @@ public class EntityRendererHandler extends EventRegistrations implements WorldRe
} finally {
mc.currentScreen = orgScreen;
mc.options.pauseOnLostFocus = orgPauseOnLostFocus;
gameRenderer.setRenderHand(orgRenderHand);
}
}

View File

@@ -0,0 +1,13 @@
package com.replaymod.render.mixin;
import net.minecraft.client.render.GameRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(GameRenderer.class)
public interface GameRendererAccessor {
@Accessor
boolean getRenderHand();
@Accessor
void setRenderHand(boolean value);
}

View File

@@ -3,6 +3,7 @@ package com.replaymod.render.mixin;
import net.minecraft.client.util.Window;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(Window.class)
public interface MainWindowAccessor {
@@ -14,4 +15,12 @@ public interface MainWindowAccessor {
int getFramebufferHeight();
@Accessor
void setFramebufferHeight(int value);
// FIXME preprocessor should be able to infer this mapping
// FIXME preprocessor should be able to remap this one when the mapping is given manually
//#if MC>=11500
@Invoker
//#else
//$$ @Invoker("method_4483")
//#endif
void invokeUpdateFramebufferSize();
}

View File

@@ -21,7 +21,12 @@ public abstract class Mixin_ChromaKeyColorSky {
@Shadow @Final private MinecraftClient client;
//#if MC>=11800
//$$ @Inject(method = "renderSky(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/util/math/Matrix4f;FLjava/lang/Runnable;)V",
//$$ @Inject(
//#if MC>=11802
//$$ method = "renderSky(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/util/math/Matrix4f;FLnet/minecraft/client/render/Camera;ZLjava/lang/Runnable;)V",
//#else
//$$ method = "renderSky(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/util/math/Matrix4f;FLjava/lang/Runnable;)V",
//#endif
//$$ at = @At(value = "INVOKE", target = "Ljava/lang/Runnable;run()V", remap = false, shift = At.Shift.AFTER),
//$$ cancellable = true)
//#elseif MC>=11400 || 10710>=MC

View File

@@ -4,7 +4,6 @@ package com.replaymod.render.mixin;
import com.replaymod.render.capturer.IrisODSFrameCapturer;
import net.coderbot.iris.Iris;
import net.fabricmc.loader.api.FabricLoader;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.injection.At;
@@ -15,14 +14,14 @@ import java.nio.file.Path;
@Pseudo
@Mixin(value = Iris.class, remap = false)
public class Mixin_LoadIrisOdsShaderPack {
@Redirect(method = "loadExternalShaderpack", at = @At(value = "FIELD", opcode = Opcodes.GETSTATIC, target = "Lnet/coderbot/iris/Iris;SHADERPACKS_DIRECTORY:Ljava/nio/file/Path;"))
@Redirect(method = "loadExternalShaderpack", at = @At(value = "INVOKE", target = "Lnet/coderbot/iris/Iris;getShaderpacksDirectory()Ljava/nio/file/Path;"))
private static Path loadReplayModOdsPack(String name) {
if (IrisODSFrameCapturer.INSTANCE != null && IrisODSFrameCapturer.SHADER_PACK_NAME.equals(name)) {
return FabricLoader.getInstance().getModContainer("replaymod")
.orElseThrow(() -> new RuntimeException("Failed to get mod container for ReplayMod"))
.getRootPath();
} else {
return Iris.SHADERPACKS_DIRECTORY;
return Iris.getShaderpacksDirectory();
}
}
}

View File

@@ -9,7 +9,7 @@ import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(GameRenderer.class)
public abstract class Mixin_Omnidirectional_Camera implements EntityRendererHandler.IEntityRenderer {
@Redirect(method = "method_22973", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/math/Matrix4f;viewboxMatrix(DFFF)Lnet/minecraft/util/math/Matrix4f;"))
@Redirect(method = "getBasicProjectionMatrix", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/math/Matrix4f;viewboxMatrix(DFFF)Lnet/minecraft/util/math/Matrix4f;"))
private Matrix4f replayModRender_perspective$0(double fovY, float aspect, float zNear, float zFar) {
return replayModRender_perspective((float) fovY, aspect, zNear, zFar);
}

View File

@@ -1,37 +0,0 @@
package com.replaymod.render.mixin;
import com.replaymod.core.versions.MCVer;
import com.replaymod.render.hooks.EntityRendererHandler;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
//#if MC>=10800
import net.minecraft.client.render.Frustum;
//#else
//$$ import net.minecraft.client.renderer.culling.Frustrum;
//#endif
//#if MC>=10800
@Mixin(Frustum.class)
//#else
//$$ @Mixin(Frustrum.class)
//#endif
public abstract class Mixin_Omnidirectional_DisableFrustumCulling {
//#if MC>=11500
@Inject(method = "isAnyCornerVisible", at = @At("HEAD"), cancellable = true)
//#else
//$$ @Inject(method = "intersects", at = @At("HEAD"), cancellable = true)
//#endif
public void intersects(CallbackInfoReturnable<Boolean> ci) {
EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) MCVer.getMinecraft().gameRenderer).replayModRender_getHandler();
if (handler != null && handler.omnidirectional) {
// Note the following used to be true but for simplicity non-ODS omnidirectional is the same now too.
// Normally the camera is always facing the direction of the omnidirectional image face that is currently
// getting rendered. With ODS however, the camera is always facing forwards and the turning happens in the
// vertex shader (non-trivial due to stereo). As such, all chunks need to be rendered all the time for ODS.
ci.setReturnValue(true);
}
}
}

View File

@@ -1,34 +0,0 @@
package com.replaymod.render.mixin;
import com.replaymod.render.hooks.EntityRendererHandler;
import net.minecraft.client.render.Camera;
import net.minecraft.client.render.GameRenderer;
import net.minecraft.client.util.math.MatrixStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(GameRenderer.class)
public abstract class Mixin_Omnidirectional_SkipHand implements EntityRendererHandler.IEntityRenderer {
@Inject(method = "renderHand", at = @At("HEAD"), cancellable = true)
private void replayModRender_renderSpectatorHand(
//#if MC>=11500
MatrixStack matrixStack,
//#endif
//#if MC>=11400
Camera camera,
//#endif
float partialTicks,
//#if MC<11400
//$$ int renderPass,
//#endif
CallbackInfo ci
) {
EntityRendererHandler handler = replayModRender_getHandler();
if (handler != null && handler.omnidirectional) {
// No spectator hands during 360° view, we wouldn't even know where to put it
ci.cancel();
}
}
}

View File

@@ -13,7 +13,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(GameRenderer.class)
public abstract class Mixin_Stereoscopic_Camera implements EntityRendererHandler.IEntityRenderer {
@Inject(method = "method_22973", at = @At("RETURN"), cancellable = true)
@Inject(method = "getBasicProjectionMatrix", at = @At("RETURN"), cancellable = true)
private void replayModRender_setupStereoscopicProjection(CallbackInfoReturnable<Matrix4f> ci) {
if (replayModRender_getHandler() != null) {
Matrix4f offset;

View File

@@ -120,6 +120,7 @@ public class VideoRenderer implements RenderInfo {
private Framebuffer guiFramebuffer;
private int displayWidth, displayHeight;
private int framebufferWidth, framebufferHeight;
public VideoRenderer(RenderSettings settings, ReplayHandler replayHandler, Timeline timeline) throws IOException {
this.settings = settings;
@@ -135,12 +136,12 @@ public class VideoRenderer implements RenderInfo {
FrameConsumer<BitmapFrame> frameConsumer;
if (settings.getEncodingPreset() == RenderSettings.EncodingPreset.EXR) {
//#if MC>=11400
frameConsumer = new EXRWriter(settings.getOutputFile().toPath());
frameConsumer = new EXRWriter(settings.getOutputFile().toPath(), settings.isIncludeAlphaChannel());
//#else
//$$ throw new UnsupportedOperationException("EXR requires LWJGL3");
//#endif
} else if (settings.getEncodingPreset() == RenderSettings.EncodingPreset.PNG) {
frameConsumer = new PNGWriter(settings.getOutputFile().toPath());
frameConsumer = new PNGWriter(settings.getOutputFile().toPath(), settings.isIncludeAlphaChannel());
} else {
frameConsumer = new FFmpegWriter(this);
}
@@ -245,10 +246,10 @@ public class VideoRenderer implements RenderInfo {
public float updateForNextFrame() {
// because the jGui lib uses Minecraft's displayWidth and displayHeight values, update these temporarily
MainWindowAccessor acc = (MainWindowAccessor) (Object) mc.getWindow();
int displayWidthBefore = acc.getFramebufferWidth();
int displayHeightBefore = acc.getFramebufferHeight();
acc.setFramebufferWidth(displayWidth);
acc.setFramebufferHeight(displayHeight);
int framebufferWidthBefore = acc.getFramebufferWidth();
int framebufferHeightBefore = acc.getFramebufferHeight();
acc.setFramebufferWidth(framebufferWidth);
acc.setFramebufferHeight(framebufferHeight);
if (!settings.isHighPerformance() || framesDone % fps == 0) {
while (drawGui() && paused) {
@@ -289,8 +290,8 @@ public class VideoRenderer implements RenderInfo {
}
// change Minecraft's display size back
acc.setFramebufferWidth(displayWidthBefore);
acc.setFramebufferHeight(displayHeightBefore);
acc.setFramebufferWidth(framebufferWidthBefore);
acc.setFramebufferHeight(framebufferHeightBefore);
if (cameraPathExporter != null) {
cameraPathExporter.recordFrame(timer.tickDelta);
@@ -362,6 +363,7 @@ public class VideoRenderer implements RenderInfo {
}
updateDisplaySize();
updateFramebufferSize();
gui.toMinecraft().init(mc, mc.getWindow().getScaledWidth(), mc.getWindow().getScaledHeight());
@@ -369,9 +371,9 @@ public class VideoRenderer implements RenderInfo {
// Set up our own framebuffer to render the GUI to
//#if MC>=11700
//$$ guiFramebuffer = new WindowFramebuffer(displayWidth, displayHeight);
//$$ guiFramebuffer = new WindowFramebuffer(framebufferWidth, framebufferHeight);
//#else
guiFramebuffer = new Framebuffer(displayWidth, displayHeight, true
guiFramebuffer = new Framebuffer(framebufferWidth, framebufferHeight, true
//#if MC>=11400
, false
//#endif
@@ -425,7 +427,7 @@ public class VideoRenderer implements RenderInfo {
}
// Finally, resize the Minecraft framebuffer to the actual width/height of the window
resizeMainWindow(mc, displayWidth, displayHeight);
resizeMainWindow(mc, framebufferWidth, framebufferHeight);
}
private void executeTaskQueue() {
@@ -483,17 +485,23 @@ public class VideoRenderer implements RenderInfo {
return false;
}
// Resize the GUI framebuffer if the display size changed
// Check if display size has changes and force recalculate GUI framebuffer size.
if (displaySizeChanged()) {
updateDisplaySize();
((MainWindowAccessor) (Object) window).invokeUpdateFramebufferSize();
}
// Resize the GUI framebuffer if the display size changed
if (framebufferSizeChanged()) {
updateFramebufferSize();
//#if MC>=11400
guiFramebuffer.resize(displayWidth, displayHeight
guiFramebuffer.resize(framebufferWidth, framebufferHeight
//#if MC>=11400
, false
//#endif
);
//#else
//$$ guiFramebuffer.createBindFramebuffer(mc.displayWidth, mc.displayHeight);
//$$ guiFramebuffer.createBindFramebuffer(framebufferWidth, framebufferHeight);
//#endif
}
@@ -594,7 +602,7 @@ public class VideoRenderer implements RenderInfo {
guiFramebuffer.endWrite();
popMatrix();
pushMatrix();
guiFramebuffer.draw(displayWidth, displayHeight);
guiFramebuffer.draw(framebufferWidth, framebufferHeight);
popMatrix();
//#if MC>=11500
@@ -633,12 +641,18 @@ public class VideoRenderer implements RenderInfo {
private boolean displaySizeChanged() {
int realWidth = mc.getWindow().getWidth();
int realHeight = mc.getWindow().getHeight();
return displayWidth != realWidth || displayHeight != realHeight;
}
private boolean framebufferSizeChanged() {
int realWidth = mc.getWindow().getFramebufferWidth();
int realHeight = mc.getWindow().getFramebufferHeight();
if (realWidth == 0 || realHeight == 0) {
// These can be zero on Windows if minimized.
// Creating zero-sized framebuffers however will throw an error, so we never want to switch to zero values.
return false;
}
return displayWidth != realWidth || displayHeight != realHeight;
return framebufferWidth != realWidth || framebufferHeight != realHeight;
}
private void updateDisplaySize() {
@@ -646,6 +660,11 @@ public class VideoRenderer implements RenderInfo {
displayHeight = mc.getWindow().getHeight();
}
private void updateFramebufferSize() {
framebufferWidth = mc.getWindow().getFramebufferWidth();
framebufferHeight = mc.getWindow().getFramebufferHeight();
}
public int getFramesDone() {
return framesDone;
}

View File

@@ -125,6 +125,9 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.replaymod.core.versions.MCVer.*;
import static com.replaymod.replaystudio.util.Utils.readInt;
@@ -349,6 +352,7 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
return;
}
terminate = true;
syncSender.shutdown();
events.unregister();
try {
channelInactive(ctx);
@@ -403,53 +407,7 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
super.channelRead(ctx, p);
}
// If we do not give minecraft time to tick, there will be dead entity artifacts left in the world
// Therefore we have to remove all loaded, dead entities manually if we are in sync mode.
// We do this after every SpawnX packet and after the destroy entities packet.
if (!asyncMode && mc.world != null) {
if (p instanceof PlayerSpawnS2CPacket
|| p instanceof EntitySpawnS2CPacket
|| p instanceof MobSpawnS2CPacket
//#if MC<11600
//$$ || p instanceof EntitySpawnGlobalS2CPacket
//#endif
|| p instanceof PaintingSpawnS2CPacket
|| p instanceof ExperienceOrbSpawnS2CPacket
|| p instanceof EntitiesDestroyS2CPacket) {
ClientWorld world = mc.world;
//#if MC>=11700
//$$ // From the looks of it, this has now been resolved (thanks to EntityChangeListener)
//#elseif MC>=11400
// Note: Not sure if it's still required but there's this really handy method anyway
world.finishRemovingEntities();
//#else
//$$ Iterator<Entity> iter = world.loadedEntityList.iterator();
//$$ while (iter.hasNext()) {
//$$ Entity entity = iter.next();
//$$ if (entity.isDead) {
//$$ int chunkX = entity.chunkCoordX;
//$$ int chunkY = entity.chunkCoordZ;
//$$
//#if MC>=11400
//$$ if (entity.addedToChunk && world.getChunkProvider().provideChunk(chunkX, chunkY, false, false) != null) {
//#else
//#if MC>=10904
//$$ if (entity.addedToChunk && world.getChunkProvider().getLoadedChunk(chunkX, chunkY) != null) {
//#else
//$$ if (entity.addedToChunk && world.getChunkProvider().chunkExists(chunkX, chunkY)) {
//#endif
//#endif
//$$ world.getChunkFromChunkCoords(chunkX, chunkY).removeEntity(entity);
//$$ }
//$$
//$$ iter.remove();
//$$ world.onEntityRemoved(entity);
//$$ }
//$$
//$$ }
//#endif
}
}
maybeRemoveDeadEntities(p);
//#if MC>=11400
if (p instanceof ChunkDataS2CPacket) {
@@ -504,6 +462,69 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
return p;
}
// If we do not give minecraft time to tick, there will be dead entity artifacts left in the world
// Therefore we have to remove all loaded, dead entities manually if we are in sync mode.
// We do this after every SpawnX packet and after the destroy entities packet.
private void maybeRemoveDeadEntities(Packet packet) {
if (asyncMode) {
return; // MC should have enough time to tick
}
boolean relevantPacket = packet instanceof PlayerSpawnS2CPacket
|| packet instanceof EntitySpawnS2CPacket
|| packet instanceof MobSpawnS2CPacket
//#if MC<11600
//$$ || packet instanceof EntitySpawnGlobalS2CPacket
//#endif
|| packet instanceof PaintingSpawnS2CPacket
|| packet instanceof ExperienceOrbSpawnS2CPacket
|| packet instanceof EntitiesDestroyS2CPacket;
if (!relevantPacket) {
return; // don't want to do it too often, only when there's likely to be a dead entity
}
mc.send(() -> {
ClientWorld world = mc.world;
if (world != null) {
removeDeadEntities(world);
}
});
}
private void removeDeadEntities(ClientWorld world) {
//#if MC>=11700
//$$ // From the looks of it, this has now been resolved (thanks to EntityChangeListener)
//#elseif MC>=11400
// Note: Not sure if it's still required but there's this really handy method anyway
world.finishRemovingEntities();
//#else
//$$ Iterator<Entity> iter = world.loadedEntityList.iterator();
//$$ while (iter.hasNext()) {
//$$ Entity entity = iter.next();
//$$ if (entity.isDead) {
//$$ int chunkX = entity.chunkCoordX;
//$$ int chunkY = entity.chunkCoordZ;
//$$
//#if MC>=11400
//$$ if (entity.addedToChunk && world.getChunkProvider().provideChunk(chunkX, chunkY, false, false) != null) {
//#else
//#if MC>=10904
//$$ if (entity.addedToChunk && world.getChunkProvider().getLoadedChunk(chunkX, chunkY) != null) {
//#else
//$$ if (entity.addedToChunk && world.getChunkProvider().chunkExists(chunkX, chunkY)) {
//#endif
//#endif
//$$ world.getChunkFromChunkCoords(chunkX, chunkY).removeEntity(entity);
//$$ }
//$$
//$$ iter.remove();
//$$ world.onEntityRemoved(entity);
//$$ }
//$$
//$$ }
//#endif
}
/**
* Process a packet and return the result.
* @param p The packet to process
@@ -627,7 +648,11 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
//#if MC>=11600
//#if MC>=11603
packet.getDimensionIds(),
//#if MC>=11800
//$$ packet.registryManager(),
//#else
(net.minecraft.util.registry.DynamicRegistryManager.Impl) packet.getRegistryManager(),
//#endif
packet.getDimensionType(),
//#else
//$$ packet.method_29443(),
@@ -824,7 +849,13 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
}
}
return asyncMode ? processPacketAsync(p) : processPacketSync(p);
if (asyncMode) {
return processPacketAsync(p);
} else {
Packet fp = p;
mc.send(() -> processPacketSync(fp));
return p;
}
}
@Override
@@ -1085,6 +1116,10 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
// Synchronous packet processing //
/////////////////////////////////////////////////////////
// Even in sync mode, we send from another thread because mods may rely on that
private final ExecutorService syncSender = Executors.newSingleThreadExecutor(runnable ->
new Thread(runnable, "replaymod-sync-sender"));
/**
* Sends all packets until the specified timestamp is reached (inclusive).
* If the timestamp is smaller than the last packet sent, the replay is restarted from the beginning.
@@ -1093,6 +1128,36 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
@Override
public void sendPacketsTill(int timestamp) {
Preconditions.checkState(!asyncMode, "This method cannot be used in async mode. Use jumpToTime(int) instead.");
// Submit our target to the sender thread and track its progress
AtomicBoolean doneSending = new AtomicBoolean();
syncSender.submit(() -> {
try {
doSendPacketsTill(timestamp);
} finally {
doneSending.set(true);
}
});
// Drain the task queue while we are sending (in case a mod blocks the io thread waiting for the main thread)
while (!doneSending.get()) {
executeTaskQueue();
// Wait until the sender thread has made progress
try {
//noinspection BusyWait
Thread.sleep(0, 100_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
// Everything has been sent, drain the queue one last time
executeTaskQueue();
}
private void doSendPacketsTill(int timestamp) {
try {
while (ctx == null && !terminate) { // Make sure channel is ready
Thread.sleep(10);
@@ -1112,7 +1177,7 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
loginPhase = true;
startFromBeginning = false;
nextPacket = null;
replayHandler.restartedReplay();
ReplayMod.instance.runSync(replayHandler::restartedReplay);
}
if (replayIn == null) {
@@ -1159,7 +1224,30 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
}
}
protected Packet processPacketSync(Packet p) {
private void executeTaskQueue() {
//#if MC>=11400
((MCVer.MinecraftMethodAccessor) mc).replayModExecuteTaskQueue();
//#else
//$$ java.util.Queue<java.util.concurrent.FutureTask<?>> scheduledTasks = ((MinecraftAccessor) mc).getScheduledTasks();
//$$
//$$ // Live-lock detection: if we already hold the lock, then the sender thread will never be able to queue its
//$$ // tasks
//$$ if (Thread.holdsLock(scheduledTasks)) {
//$$ throw new IllegalStateException("Task queue already locked. " +
//$$ "You may want to use `Scheduler.runLaterWithoutLock` to run while the lock is not taken.");
//$$ }
//$$
//$$ //noinspection SynchronizationOnLocalVariableOrMethodParameter
//$$ synchronized (scheduledTasks) {
//$$ while (!scheduledTasks.isEmpty()) {
//$$ scheduledTasks.poll().run();
//$$ }
//$$ }
//#endif
ReplayMod.instance.runTasks();
}
protected void processPacketSync(Packet p) {
//#if MC>=10904
if (p instanceof UnloadChunkS2CPacket) {
UnloadChunkS2CPacket packet = (UnloadChunkS2CPacket) p;
@@ -1280,7 +1368,6 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
}
}
}
return p; // During synchronous playback everything is sent normally
}
private void forcePositionForVehicleAndSelf(Entity entity) {

View File

@@ -8,6 +8,10 @@ import com.replaymod.replay.camera.CameraEntity;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.RenderTickCounter;
//#if MC>=11802
//$$ import net.minecraft.client.gui.screen.DownloadingTerrainScreen;
//#endif
//#if MC>=11400
import org.lwjgl.glfw.GLFW;
//#else
@@ -111,6 +115,15 @@ public class InputReplayTimer extends WrappedTimer {
//#endif
//$$ }
//#endif
//#if MC>=11802
//$$ // As of 1.18.2, this screen always stays open for at least two seconds, and requires ticking to close.
//$$ // Thanks, but we'll have none of that (at least while in a replay).
//$$ if (mc.currentScreen instanceof DownloadingTerrainScreen) {
//$$ mc.currentScreen.close();
//$$ }
//#endif
}
//#if MC>=11600
return ticksThisFrame;

View File

@@ -1,4 +1,4 @@
//#if MC>=10904
//#if MC>=10800
package com.replaymod.replay;
import com.google.common.util.concurrent.FutureCallback;
@@ -78,7 +78,11 @@ public class QuickReplaySender extends ChannelHandlerAdapter implements ReplaySe
wrappedBuf.writerIndex(size);
PacketByteBuf packetByteBuf = new PacketByteBuf(wrappedBuf);
//#if MC>=10809
Packet<?> mcPacket;
//#else
//$$ Packet mcPacket;
//#endif
//#if MC>=11700
//$$ mcPacket = NetworkState.PLAY.getPacketHandler(NetworkSide.CLIENTBOUND, packet.getId(), packetByteBuf);
//#elseif MC>=11500
@@ -137,13 +141,13 @@ public class QuickReplaySender extends ChannelHandlerAdapter implements ReplaySe
LOGGER.info("Initialized quick replay sender in " + (System.currentTimeMillis() - start) + "ms");
} catch (Throwable e) {
LOGGER.error("Initializing quick replay sender:", e);
mod.getCore().runLater(() -> {
mod.getCore().runLaterWithoutLock(() -> {
mod.getCore().printWarningToChat("Error initializing quick replay sender: %s", e.getLocalizedMessage());
promise.setException(e);
});
return;
}
mod.getCore().runLater(() -> promise.set(null));
mod.getCore().runLaterWithoutLock(() -> promise.set(null));
}).start();
return promise;
}

View File

@@ -69,7 +69,7 @@ import net.minecraft.entity.LivingEntity;
//$$ import io.netty.channel.ChannelOutboundHandlerAdapter;
//#endif
//#if MC<10904
//#if MC<10800
//$$ import de.johni0702.minecraft.gui.element.GuiLabel;
//$$ import de.johni0702.minecraft.gui.popup.GuiInfoPopup;
//$$ import de.johni0702.minecraft.gui.utils.Colors;
@@ -119,11 +119,11 @@ public class ReplayHandler {
* Decodes and sends packets into channel.
*/
private final FullReplaySender fullReplaySender;
//#if MC>=10904
//#if MC>=10800
private final QuickReplaySender quickReplaySender;
private boolean quickMode = false;
//#else
//$$ private static final String QUICK_MODE_MIN_MC = "1.9.4";
//$$ private static final String QUICK_MODE_MIN_MC = "1.8";
//#endif
/**
@@ -160,7 +160,7 @@ public class ReplayHandler {
markers = replayFile.getMarkers().or(Collections.emptySet());
fullReplaySender = new FullReplaySender(this, replayFile, false);
//#if MC>=10904
//#if MC>=10800
quickReplaySender = new QuickReplaySender(ReplayModReplay.instance, replayFile);
//#endif
@@ -206,7 +206,7 @@ public class ReplayHandler {
ReplayClosingCallback.EVENT.invoker().replayClosing(this);
fullReplaySender.terminateReplay();
//#if MC>=10904
//#if MC>=10800
if (quickMode) {
quickReplaySender.unregister();
}
@@ -308,7 +308,7 @@ public class ReplayHandler {
//$$ channel = new EmbeddedChannel(dummyHandler);
//$$ channel.pipeline().remove(dummyHandler);
//#endif
//#if MC>=10904
//#if MC>=10800
channel.pipeline().addLast("ReplayModReplay_quickReplaySender", quickReplaySender);
//#endif
channel.pipeline().addLast("ReplayModReplay_replaySender", fullReplaySender);
@@ -329,7 +329,7 @@ public class ReplayHandler {
}
public ReplaySender getReplaySender() {
//#if MC>=10904
//#if MC>=10800
return quickMode ? quickReplaySender : fullReplaySender;
//#else
//$$ return fullReplaySender;
@@ -340,7 +340,7 @@ public class ReplayHandler {
return overlay;
}
//#if MC>=10904
//#if MC>=10800
public void ensureQuickModeInitialized(Runnable andThen) {
if (Utils.ifMinimalModeDoPopup(overlay, () -> {})) return;
ListenableFuture<Void> future = quickReplaySender.getInitializationPromise();
@@ -534,7 +534,11 @@ public class ReplayHandler {
}
public void doJump(int targetTime, boolean retainCameraPosition) {
//#if MC>=10904
if (!getReplaySender().isAsyncMode()) {
return; // path playback, rendering, etc. -> no jumping allowed
}
//#if MC>=10800
if (getReplaySender() == quickReplaySender) {
// Always round to full tick
targetTime = targetTime + targetTime % 50;
@@ -603,6 +607,7 @@ public class ReplayHandler {
// Render our please-wait-screen
GuiScreen guiScreen = new GuiScreen();
guiScreen.setBackground(AbstractGuiScreen.Background.DIRT);
guiScreen.setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER));
guiScreen.addElements(new HorizontalLayout.Data(0.5),
new GuiLabel().setI18nText("replaymod.gui.pleasewait"));

View File

@@ -31,6 +31,10 @@ import net.minecraft.stat.StatHandler;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Box;
//#if MC>=11802
//$$ import net.minecraft.tag.TagKey;
//#endif
//#if FABRIC>=1
//#else
//$$ import net.minecraftforge.client.event.EntityViewRenderEvent;
@@ -354,7 +358,13 @@ public class CameraEntity
//#if MC>=11400
@Override
public boolean isSubmergedIn(Tag<Fluid> fluid) {
public boolean isSubmergedIn(
//#if MC>=11802
//$$ TagKey<Fluid> fluid
//#else
Tag<Fluid> fluid
//#endif
) {
return falseUnlessSpectating(entity -> entity.isSubmergedIn(fluid));
}

View File

@@ -49,6 +49,8 @@ import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
@@ -95,8 +97,10 @@ public class GuiReplayViewer extends GuiScreen {
List<GuiReplayEntry> selected = list.getSelected();
if (selected.size() == 1) {
File file = selected.get(0).file;
LOGGER.info("Opening replay in viewer: " + file);
try {
mod.startReplay(selected.get(0).file);
mod.startReplay(file);
} catch (IOException e) {
e.printStackTrace();
}
@@ -130,8 +134,8 @@ public class GuiReplayViewer extends GuiScreen {
public final GuiButton renameButton = new GuiButton().onClick(new Runnable() {
@Override
public void run() {
final File file = list.getSelected().get(0).file;
String name = Utils.fileNameToReplayName(file.getName());
final Path path = list.getSelected().get(0).file.toPath();
String name = Utils.fileNameToReplayName(path.getFileName().toString());
final GuiTextField nameField = new GuiTextField().setSize(200, 20).setFocused(true).setText(name);
final GuiYesNoPopup popup = GuiYesNoPopup.open(GuiReplayViewer.this,
new GuiLabel().setI18nText("replaymod.gui.viewer.rename.name").setColor(Colors.BLACK),
@@ -147,16 +151,16 @@ public class GuiReplayViewer extends GuiScreen {
}
}).onTextChanged(obj -> {
popup.getYesButton().setEnabled(!nameField.getText().isEmpty()
&& !new File(file.getParentFile(), Utils.replayNameToFileName(nameField.getText())).exists());
&& Files.notExists(Utils.replayNameToPath(path.getParent(), nameField.getText())));
});
popup.onAccept(() -> {
// Sanitize their input
String newName = nameField.getText().trim();
// This file is what they want
File targetFile = new File(file.getParentFile(), Utils.replayNameToFileName(newName));
Path targetPath = Utils.replayNameToPath(path.getParent(), newName);
try {
// Finally, try to move it
FileUtils.moveFile(file, targetFile);
Files.move(path, targetPath);
} catch (IOException e) {
// We failed (might also be their OS)
e.printStackTrace();

View File

@@ -18,7 +18,7 @@ public abstract class MixinGuiSpectator {
//$$ @Inject(method = "func_175260_a", at = @At("HEAD"), cancellable = true)
//#endif
public void isInReplay(
//#if MC>=11400
//#if MC>=11400 && MC<11802
double i,
//#else
//$$ int i,

View File

@@ -0,0 +1,35 @@
//#if MC>=11400
package com.replaymod.replay.mixin;
import com.replaymod.replay.camera.CameraEntity;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.network.ClientPlayerInteractionManager;
import net.minecraft.world.GameMode;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import static com.replaymod.core.versions.MCVer.getMinecraft;
@Pseudo
@Mixin(targets = "net.coderbot.iris.pipeline.HandRenderer", remap = false)
public abstract class Mixin_ShowSpectatedHand_Iris {
@Redirect(
method = "*",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;getCurrentGameMode()Lnet/minecraft/world/GameMode;",
remap = true
)
)
private GameMode getGameMode(ClientPlayerInteractionManager interactionManager) {
ClientPlayerEntity camera = getMinecraft().player;
if (camera instanceof CameraEntity) {
// alternative doesn't really matter, the caller only checks for equality to SPECTATOR
return camera.isSpectator() ? GameMode.SPECTATOR : GameMode.SURVIVAL;
}
return interactionManager.getCurrentGameMode();
}
}
//#endif

View File

@@ -0,0 +1,38 @@
package com.replaymod.replay.mixin.world_border;
import com.replaymod.core.versions.MCVer;
import com.replaymod.replay.ReplayHandler;
import com.replaymod.replay.ReplayModReplay;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Normally Minecraft's world border movement is based off real time;
* this redirect ensures that it is synced with the time in the Replay instead.
*/
//#if MC>=11400
// FIXME: preprocessor should be able to remap between fabric and forge
//#if FABRIC
@Mixin(targets = "net.minecraft.world.border.WorldBorder.MovingArea")
//#else
//$$ @Mixin(targets = "net.minecraft.world.border.WorldBorder.MovingBorderInfo")
//#endif
//#else
//$$ @Mixin(net.minecraft.world.border.WorldBorder.class)
//#endif
public class Mixin_UseReplayTime_ForMovement {
//#if MC>=11400
@Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Util;getMeasuringTimeMs()J"))
//#else
//$$ @Redirect(method = "*", at = @At(value = "INVOKE", target = "Ljava/lang/System;currentTimeMillis()J"))
//#endif
private long getWorldBorderTime() {
ReplayHandler replayHandler = ReplayModReplay.instance.getReplayHandler();
if (replayHandler != null) {
return replayHandler.getReplaySender().currentTimeStamp();
}
return MCVer.milliTime();
}
}

View File

@@ -0,0 +1,29 @@
package com.replaymod.replay.mixin.world_border;
import com.replaymod.core.versions.MCVer;
import com.replaymod.replay.ReplayHandler;
import com.replaymod.replay.ReplayModReplay;
import net.minecraft.client.render.WorldRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Normally Minecraft's world border texture animation is based off real time;
* this redirect ensures that it is synced with the time in the Replay instead.
*/
@Mixin(WorldRenderer.class)
public class Mixin_UseReplayTime_ForTexture {
//#if MC>=11400
@Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Util;getMeasuringTimeMs()J"))
//#else
//$$ @Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getSystemTime()J"))
//#endif
private long getWorldBorderTime() {
ReplayHandler replayHandler = ReplayModReplay.instance.getReplayHandler();
if (replayHandler != null) {
return replayHandler.getReplaySender().currentTimeStamp();
}
return MCVer.milliTime();
}
}

View File

@@ -9,10 +9,49 @@ uniform int direction;
const float eyeDistance = 0.14;
void orient(vec4 position, int orientation) {
float z;
if (orientation == 0) { // LEFT
z = position.z;
position.z = position.x;
position.x = -z;
} else if (orientation == 1) { // RIGHT
z = position.z;
position.z = -position.x;
position.x = z;
} else if (orientation == 2) { // FRONT
// No changes required
} else if (orientation == 3) { // BACK
position.x = -position.x;
position.z = -position.z;
} else if (orientation == 4) { // TOP
z = position.z;
position.z = -position.y;
position.y = z;
} else if (orientation == 5) { // BOTTOM
z = position.z;
position.z = position.y;
position.y = -z;
}
}
void orientInverse(vec4 position, int orientation) {
if (orientation < 2) {
orient(position, 1 - orientation); // LEFT and RIGHT flip
} else if (orientation < 4) {
orient(position, orientation); // FRONT and BACK are their own inverses
} else {
orient(position, (1 - (orientation - 4)) + 4); // TOP and BOTTOM flip
}
}
void main() {
// Transform to view space
vec4 position = gl_ModelViewMatrix * gl_Vertex;
// Undo the camera rotation, so we always apply our stereo effect looking in the same direction
orientInverse(position, direction);
// Distort for ODS
// O := The origin
// P := The current vertex/point
@@ -34,30 +73,8 @@ void main() {
// Calculate the vector between O and T and finally move the vertex by that vector
position -= vec4(distTO * sin(angOT), 0, distTO * cos(angOT), 0);
// Rotate for different cubic views
float z;
if (direction == 0) { // LEFT
z = position.z;
position.z = position.x;
position.x = -z;
} else if (direction == 1) { // RIGHT
z = position.z;
position.z = -position.x;
position.x = z;
} else if (direction == 2) { // FRONT
// No changes required
} else if (direction == 3) { // BACK
position.x = -position.x;
position.z = -position.z;
} else if (direction == 4) { // TOP
z = position.z;
position.z = -position.y;
position.y = z;
} else if (direction == 5) { // BOTTOM
z = position.z;
position.z = position.y;
position.y = -z;
}
// Rotate back into the correct cubic view
orient(position, direction);
// Transform to screen space
gl_Position = gl_ProjectionMatrix * position;

View File

@@ -14,10 +14,49 @@ uniform int direction;
const float eyeDistance = 0.14;
void orient(vec4 position, int orientation) {
float z;
if (orientation == 0) { // LEFT
z = position.z;
position.z = position.x;
position.x = -z;
} else if (orientation == 1) { // RIGHT
z = position.z;
position.z = -position.x;
position.x = z;
} else if (orientation == 2) { // FRONT
// No changes required
} else if (orientation == 3) { // BACK
position.x = -position.x;
position.z = -position.z;
} else if (orientation == 4) { // TOP
z = position.z;
position.z = -position.y;
position.y = z;
} else if (orientation == 5) { // BOTTOM
z = position.z;
position.z = position.y;
position.y = -z;
}
}
void orientInverse(vec4 position, int orientation) {
if (orientation < 2) {
orient(position, 1 - orientation); // LEFT and RIGHT flip
} else if (orientation < 4) {
orient(position, orientation); // FRONT and BACK are their own inverses
} else {
orient(position, (1 - (orientation - 4)) + 4); // TOP and BOTTOM flip
}
}
void main() {
// Transform to view space
vec4 position = gl_ModelViewMatrix * gl_Vertex;
// Undo the camera rotation, so we always apply our stereo effect looking in the same direction
orientInverse(position, direction);
// Distort for ODS
// O := The origin
// P := The current vertex/point
@@ -39,30 +78,8 @@ void main() {
// Calculate the vector between O and T and finally move the vertex by that vector
position -= vec4(distTO * sin(angOT), 0, distTO * cos(angOT), 0);
// Rotate for different cubic views
float z;
if (direction == 0) { // LEFT
z = position.z;
position.z = position.x;
position.x = -z;
} else if (direction == 1) { // RIGHT
z = position.z;
position.z = -position.x;
position.x = z;
} else if (direction == 2) { // FRONT
// No changes required
} else if (direction == 3) { // BACK
position.x = -position.x;
position.z = -position.z;
} else if (direction == 4) { // TOP
z = position.z;
position.z = -position.y;
position.y = z;
} else if (direction == 5) { // BOTTOM
z = position.z;
position.z = position.y;
position.y = -z;
}
// Rotate back into the correct cubic view
orient(position, direction);
// Transform to screen space
gl_Position = gl_ProjectionMatrix * position;

View File

@@ -47,6 +47,10 @@
"fabric-resource-loader-v0": "*"
},
"conflicts": {
"iris": "<1.1.3"
},
"custom": {
"mm:early_risers": [
"com.replaymod.core.ReplayModMMLauncher"

View File

@@ -11,8 +11,10 @@
"SPacketSpawnMobAccessor",
"SPacketSpawnPlayerAccessor",
"MixinServerInfo",
//#if MC>=11400
//#if MC>=10800
"MixinDownloadingPackFinder",
//#endif
//#if MC>=11400
"MixinMouseHelper",
//#endif
//#if MC>=10904

View File

@@ -14,9 +14,7 @@
"Mixin_HideNameTags",
"Mixin_HideNameTags_LivingEntity",
"Mixin_Omnidirectional_Camera",
"Mixin_Omnidirectional_DisableFrustumCulling",
"Mixin_Omnidirectional_Rotation",
"Mixin_Omnidirectional_SkipHand",
"Mixin_PreserveDepthDuringGuiRendering",
"Mixin_SkipBlockOutlinesDuringRender",
"Mixin_SkipHudDuringRender",
@@ -39,6 +37,7 @@
"Mixin_PreserveDepthDuringHandRendering",
"Mixin_WindowsWorkaroundForTinyEXRNatives",
//#endif
"GameRendererAccessor",
"MainWindowAccessor",
"WorldRendererAccessor",
//#if MC>=10904

View File

@@ -7,6 +7,8 @@
"client": [
"entity_tracking.Mixin_EntityExt",
"entity_tracking.Mixin_FixPartialUpdates",
"world_border.Mixin_UseReplayTime_ForMovement",
"world_border.Mixin_UseReplayTime_ForTexture",
"Mixin_FixNPCSkinCaching",
//#if MC>=11800
//$$ "Mixin_FixEntityNotTracking",
@@ -23,6 +25,7 @@
"ClientWorldAccessor",
"EntityLivingBaseAccessor",
//#if MC>=11400
"Mixin_ShowSpectatedHand_Iris",
"Mixin_ShowSpectatedHand_NoOF",
"Mixin_ShowSpectatedHand_OF",
//#else

View File

@@ -1,14 +1,6 @@
{
"pack": {
"description": "ReplayMod resources",
//#if MC>=11400
"pack_format": 4
//#else
//#if MC>=11002
//$$ "pack_format": 2
//#else
//$$ "pack_format": 1
//#endif
//#endif
}
}

View File

@@ -1 +1 @@
2.6.3
2.6.4

View File

@@ -34,6 +34,11 @@ public class Window implements MainWindowAccessor {
mc.displayHeight = value;
}
@Override
public void invokeUpdateFramebufferSize() {
// no-op, pre-LWJGL3 MC doesn't differentiate between window and framebuffer size
}
public long getHandle() {
return 0;
}

View File

@@ -3,6 +3,7 @@ package com.replaymod.render.mixin;
import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(Minecraft.class)
public interface MainWindowAccessor {
@@ -14,4 +15,6 @@ public interface MainWindowAccessor {
int getFramebufferHeight();
@Accessor("displayHeight")
void setFramebufferHeight(int value);
@Invoker
void invokeUpdateFramebufferSize();
}

View File

@@ -0,0 +1,6 @@
{
"pack": {
"description": "ReplayMod resources",
"pack_format": 2
}
}

0
versions/1.18.2/.gitkeep Normal file
View File

View File

@@ -0,0 +1,6 @@
{
"pack": {
"description": "ReplayMod resources",
"pack_format": 1
}
}

View File

@@ -1 +1,2 @@
net.minecraft.client.network.ClientPlayerEntity getUnderwaterVisibility() method_3140()
net.minecraft.client.render.GameRenderer getBasicProjectionMatrix() method_22973()