Release 2.6.2

This commit is contained in:
Jonas Herzig
2021-12-10 11:52:58 +01:00
55 changed files with 1206 additions and 282 deletions

BIN
.gitignore vendored

Binary file not shown.

View File

@@ -34,7 +34,7 @@ buildscript {
dependencies {
classpath 'gradle.plugin.com.github.jengelman.gradle.plugins:shadow:7.0.0'
if (fabric) {
classpath 'fabric-loom:fabric-loom.gradle.plugin:0.8-SNAPSHOT'
classpath 'fabric-loom:fabric-loom.gradle.plugin:0.10-SNAPSHOT'
} else if (mcVersion >= 11400) {
classpath('net.minecraftforge.gradle:ForgeGradle:5.0.5') { // the FG people still haven't learned to not do breaking changes
exclude group: 'trove', module: 'trove' // preprocessor/idea requires more recent one
@@ -43,7 +43,7 @@ buildscript {
classpath('com.github.ReplayMod:ForgeGradle:' + (
mcVersion >= 11200 ? '34ab703' : // FG 2.3
mcVersion >= 10904 ? '5d1e8d8' : // FG 2.2
'd1a7165' // FG 2.1
'ceb83c0' // FG 2.1
) + ':all')
} else {
classpath 'com.github.ReplayMod:ForgeGradle:a8a9e0ca:all' // FG 1.2
@@ -108,9 +108,9 @@ preprocess {
def mcVersionStr = "${(int)(mcVersion/10000)}.${(int)(mcVersion/100)%100}" + (mcVersion%100==0 ? '' : ".${mcVersion%100}")
sourceCompatibility = targetCompatibility = mcVersion >= 11700 ? 16 : 1.8
sourceCompatibility = targetCompatibility = mcVersion >= 11800 ? 17 : mcVersion >= 11700 ? 16 : 1.8
tasks.withType(JavaCompile).configureEach {
options.release = mcVersion >= 11700 ? 16 : 8
options.release = mcVersion >= 11800 ? 17 : mcVersion >= 11700 ? 16 : 8
}
if (mcVersion >= 11400) {
@@ -124,8 +124,8 @@ group= "com.replaymod"
archivesBaseName = "replaymod"
if (FABRIC) {
minecraft {
refmapName = 'mixins.replaymod.refmap.json'
loom {
mixin.defaultRefmapName.set('mixins.replaymod.refmap.json')
runConfigs.all {
ideConfigGenerated = true
}
@@ -243,6 +243,8 @@ dependencies {
11604: '1.16.4',
11700: '1.17',
11701: '1.17.1',
11800: '1.18',
11801: '1.18.1',
][mcVersion]
mappings 'net.fabricmc:yarn:' + [
11404: '1.14.4+build.16',
@@ -252,8 +254,10 @@ dependencies {
11604: '1.16.4+build.6:v2',
11700: '1.17+build.13:v2',
11701: '1.17.1+build.29:v2',
11800: '1.18+build.1:v2',
11801: '1.18.1+build.1:v2',
][mcVersion]
modImplementation 'net.fabricmc:fabric-loader:0.11.6'
modImplementation 'net.fabricmc:fabric-loader:0.12.5'
def fabricApiVersion = [
11404: '0.4.3+build.247-1.14',
11502: '0.5.1+build.294-1.15',
@@ -262,6 +266,8 @@ dependencies {
11604: '0.25.1+build.416-1.16',
11700: '0.36.0+1.17',
11701: '0.37.1+1.17',
11800: '0.43.1+1.18',
11801: '0.43.1+1.18',
][mcVersion]
def fabricApiModules = [
"api-base",
@@ -332,15 +338,17 @@ dependencies {
shadow 'com.github.ReplayMod.JavaBlend:2.79.0:a0696f8'
shadow "com.github.ReplayMod:ReplayStudio:c9de2f5", shadeExclusions
shadow "com.github.ReplayMod:ReplayStudio:70f59ef", shadeExclusions
implementation(jGui){
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
}
shadow 'com.github.ReplayMod:lwjgl-utils:27dcd66'
if (FABRIC) {
if (mcVersion >= 11700) {
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'

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

2
jGui

Submodule jGui updated: 31bcfabe67...c79b62a73e

View File

@@ -2,8 +2,8 @@ import groovy.json.JsonOutput
import java.io.ByteArrayOutputStream
plugins {
id("fabric-loom") version "0.8-SNAPSHOT" apply false
id("com.replaymod.preprocess") version "123fb7a"
id("fabric-loom") version "0.10-SNAPSHOT" apply false
id("com.replaymod.preprocess") version "7746c47"
id("com.github.hierynomus.license") version "0.15.0"
}
@@ -189,6 +189,7 @@ val doRelease by tasks.registering {
defaultTasks("bundleJar")
preprocess {
val mc11801 = createNode("1.18.1", 11801, "yarn")
val mc11701 = createNode("1.17.1", 11701, "yarn")
val mc11700 = createNode("1.17", 11700, "yarn")
val mc11604 = createNode("1.16.4", 11604, "yarn")
@@ -207,10 +208,11 @@ preprocess {
val mc10800 = createNode("1.8", 10800, "srg")
val mc10710 = createNode("1.7.10", 10710, "srg")
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"))
mc11604.link(mc11601)
mc11601.link(mc11502)
mc11601.link(mc11502, file("versions/mapping-fabric-1.16.1-1.15.2.txt"))
mc11502.link(mc11404, file("versions/mapping-fabric-1.15.2-1.14.4.txt"))
mc11404.link(mc11404Forge, file("versions/mapping-1.14.4-fabric-forge.txt"))
mc11404Forge.link(mc11202, file("versions/1.14.4-forge/mapping.txt"))

View File

@@ -31,6 +31,7 @@ val jGuiVersions = listOf(
"1.16.4",
"1.17",
"1.17.1",
"1.18.1",
)
val replayModVersions = listOf(
// "1.7.10",
@@ -50,6 +51,7 @@ val replayModVersions = listOf(
"1.16.4",
"1.17",
"1.17.1",
"1.18.1",
)
rootProject.buildFileName = "root.gradle.kts"

View File

@@ -1,10 +1,10 @@
package com.replaymod.core;
import com.google.common.net.PercentEscaper;
import com.replaymod.compat.ReplayModCompat;
import com.replaymod.core.files.ReplayFilesService;
import com.replaymod.core.files.ReplayFoldersService;
import com.replaymod.core.gui.GuiBackgroundProcesses;
import com.replaymod.core.gui.GuiReplaySettings;
import com.replaymod.core.gui.RestoreReplayGui;
import com.replaymod.core.versions.MCVer;
import com.replaymod.core.versions.scheduler.Scheduler;
import com.replaymod.core.versions.scheduler.SchedulerImpl;
@@ -14,12 +14,9 @@ import com.replaymod.recording.ReplayModRecording;
import com.replaymod.render.ReplayModRender;
import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replaystudio.lib.viaversion.api.protocol.version.ProtocolVersion;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ZipReplayFile;
import com.replaymod.replaystudio.studio.ReplayStudio;
import com.replaymod.replaystudio.util.I18n;
import com.replaymod.simplepathing.ReplayModSimplePathing;
import de.johni0702.minecraft.gui.container.GuiScreen;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.options.Option;
import net.minecraft.resource.DirectoryResourcePack;
@@ -29,21 +26,12 @@ import net.minecraft.text.Text;
import net.minecraft.text.TranslatableText;
import net.minecraft.util.Formatting;
import net.minecraft.util.Identifier;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
@@ -73,6 +61,8 @@ public class ReplayMod implements Module, Scheduler {
private final List<Module> modules = new ArrayList<>();
private final GuiBackgroundProcesses backgroundProcesses = new GuiBackgroundProcesses();
public final ReplayFoldersService folders = new ReplayFoldersService(settingsRegistry);
public final ReplayFilesService files = new ReplayFilesService(folders);
/**
* Whether the current MC version is supported by the embedded ReplayStudio version.
@@ -116,58 +106,6 @@ public class ReplayMod implements Module, Scheduler {
return settingsRegistry;
}
public Path getReplayFolder() throws IOException {
String str = getSettingsRegistry().get(Setting.RECORDING_PATH);
return Files.createDirectories(getMinecraft().runDirectory.toPath().resolve(str));
}
/**
* Folder into which replay backups are saved before the MarkerProcessor is unleashed.
*/
public Path getRawReplayFolder() throws IOException {
return Files.createDirectories(getReplayFolder().resolve("raw"));
}
/**
* Folder into which replays are recorded.
* 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"));
}
/**
* Folder in which replay cache files are stored.
* Distinct from the recording folder cause people kept confusing them with recordings.
*/
public Path getCacheFolder() throws IOException {
String str = getSettingsRegistry().get(Setting.CACHE_PATH);
Path path = getMinecraft().runDirectory.toPath().resolve(str);
Files.createDirectories(path);
try {
Files.setAttribute(path, "dos:hidden", true);
} catch (UnsupportedOperationException ignored) {
} catch (Exception e) {
e.printStackTrace();
}
return path;
}
private static final PercentEscaper CACHE_FILE_NAME_ENCODER = new PercentEscaper("-_ ", false);
public Path getCachePathForReplay(Path replay) throws IOException {
Path replayFolder = getReplayFolder();
Path cacheFolder = getCacheFolder();
Path relative = replayFolder.toAbsolutePath().relativize(replay.toAbsolutePath());
return cacheFolder.resolve(CACHE_FILE_NAME_ENCODER.escape(relative.toString()));
}
public Path getReplayPathForCache(Path cache) throws IOException {
String relative = URLDecoder.decode(cache.getFileName().toString(), "UTF-8");
Path replayFolder = getReplayFolder();
return replayFolder.resolve(relative);
}
public static final DirectoryResourcePack jGuiResourcePack = createJGuiResourcePack();
public static final String JGUI_RESOURCE_PACK_NAME = "replaymod_jgui";
private static DirectoryResourcePack createJGuiResourcePack() {
@@ -230,114 +168,7 @@ public class ReplayMod implements Module, Scheduler {
}
//#endif
runPostStartup(() -> {
final long DAYS = 24 * 60 * 60 * 1000;
// Cleanup any cache folders still remaining in the recording folder (we once used to put them there)
try {
Files.walkFileTree(getReplayFolder(), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
String name = dir.getFileName().toString();
if (name.endsWith(".mcpr.cache")) {
FileUtils.deleteDirectory(dir.toFile());
return FileVisitResult.SKIP_SUBTREE;
}
return super.preVisitDirectory(dir, attrs);
}
});
} catch (IOException e) {
e.printStackTrace();
}
// Cleanup raw folder content three weeks after creation (these are pretty valuable for debugging)
try (DirectoryStream<Path> paths = Files.newDirectoryStream(getRawReplayFolder())) {
for (Path path : paths) {
if (Files.getLastModifiedTime(path).toMillis() + 21 * DAYS < System.currentTimeMillis()) {
Files.delete(path);
}
}
} catch (IOException e) {
e.printStackTrace();
}
// Move anything which is still in the recording folder into the regular replay folder
// so it can be opened and/or recovered
try (DirectoryStream<Path> paths = Files.newDirectoryStream(getRecordingFolder())) {
for (Path path : paths) {
Path destination = getReplayFolder().resolve(path.getFileName());
if (Files.exists(destination)) {
continue; // better play it save
}
Files.move(path, destination);
}
} catch (IOException e) {
e.printStackTrace();
}
// Cleanup cache folders 7 days after last modification or when its replay is gone
try (DirectoryStream<Path> paths = Files.newDirectoryStream(getCacheFolder())) {
for (Path path : paths) {
if (Files.isDirectory(path)) {
Path replay = getReplayPathForCache(path);
long lastModified = Files.getLastModifiedTime(path).toMillis();
if (lastModified + 7 * DAYS < System.currentTimeMillis() || !Files.exists(replay)) {
FileUtils.deleteDirectory(path.toFile());
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
// Cleanup deleted corrupted replays
try (DirectoryStream<Path> paths = Files.newDirectoryStream(getReplayFolder())) {
for (Path path : paths) {
String name = path.getFileName().toString();
if (name.endsWith(".mcpr.del") && Files.isDirectory(path)) {
long lastModified = Files.getLastModifiedTime(path).toMillis();
if (lastModified + 2 * DAYS < System.currentTimeMillis()) {
FileUtils.deleteDirectory(path.toFile());
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
// Restore corrupted replays
try (DirectoryStream<Path> paths = Files.newDirectoryStream(getReplayFolder())) {
for (Path path : paths) {
String name = path.getFileName().toString();
if (name.endsWith(".mcpr.tmp") && Files.isDirectory(path)) {
Path original = path.resolveSibling(FilenameUtils.getBaseName(name));
Path noRecoverMarker = original.resolveSibling(original.getFileName() + ".no_recover");
if (Files.exists(noRecoverMarker)) {
// This file, when its markers are processed, doesn't actually result in any replays.
// So we don't really need to recover it either, let's just get rid of it.
FileUtils.deleteDirectory(path.toFile());
Files.delete(noRecoverMarker);
continue;
}
new RestoreReplayGui(this, GuiScreen.wrap(mc.currentScreen), original.toFile()).display();
}
}
} catch (IOException e) {
e.printStackTrace();
}
// Cleanup leftover no_recover files
try (DirectoryStream<Path> paths = Files.newDirectoryStream(getReplayFolder())) {
for (Path path : paths) {
String name = path.getFileName().toString();
if (name.endsWith(".no_recover")) {
Files.delete(path);
}
}
} catch (IOException e) {
e.printStackTrace();
}
});
runPostStartup(() -> files.initialScan(this));
}
@Override
@@ -437,17 +268,4 @@ public class ReplayMod implements Module, Scheduler {
return new ReplayStudio().isCompatible(fileFormatVersion, protocolVersion, MCVer.getProtocolVersion());
}
}
public ReplayFile openReplay(Path path) throws IOException {
return openReplay(path, path);
}
public ReplayFile openReplay(Path input, Path output) throws IOException {
return new ZipReplayFile(
new ReplayStudio(),
input != null ? input.toFile() : null,
output.toFile(),
getCachePathForReplay(output).toFile()
);
}
}

View File

@@ -0,0 +1,202 @@
package com.replaymod.core.files;
import com.replaymod.replaystudio.data.Marker;
import com.replaymod.replaystudio.data.ModInfo;
import com.replaymod.replaystudio.data.ReplayAssetEntry;
import com.replaymod.replaystudio.io.ReplayInputStream;
import com.replaymod.replaystudio.io.ReplayOutputStream;
import com.replaymod.replaystudio.lib.guava.base.Optional;
import com.replaymod.replaystudio.pathing.PathingRegistry;
import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.protocol.PacketTypeRegistry;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ReplayMetaData;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
public class DelegatingReplayFile implements ReplayFile {
private final ReplayFile delegate;
public DelegatingReplayFile(ReplayFile delegate) {
this.delegate = delegate;
}
@Override
public Optional<InputStream> get(String entry) throws IOException {
return this.delegate.get(entry);
}
@Override
public Optional<InputStream> getCache(String entry) throws IOException {
return this.delegate.getCache(entry);
}
@Override
public Map<String, InputStream> getAll(Pattern pattern) throws IOException {
return this.delegate.getAll(pattern);
}
@Override
public OutputStream write(String entry) throws IOException {
return this.delegate.write(entry);
}
@Override
public OutputStream writeCache(String entry) throws IOException {
return this.delegate.writeCache(entry);
}
@Override
public void remove(String entry) throws IOException {
this.delegate.remove(entry);
}
@Override
public void removeCache(String entry) throws IOException {
this.delegate.removeCache(entry);
}
@Override
public void save() throws IOException {
this.delegate.save();
}
@Override
public void saveTo(File target) throws IOException {
this.delegate.saveTo(target);
}
@Override
public ReplayMetaData getMetaData() throws IOException {
return this.delegate.getMetaData();
}
@Override
public void writeMetaData(PacketTypeRegistry registry, ReplayMetaData metaData) throws IOException {
this.delegate.writeMetaData(registry, metaData);
}
@Override
public ReplayInputStream getPacketData(PacketTypeRegistry registry) throws IOException {
return this.delegate.getPacketData(registry);
}
@Override
public ReplayOutputStream writePacketData() throws IOException {
return this.delegate.writePacketData();
}
@Override
public Map<Integer, String> getResourcePackIndex() throws IOException {
return this.delegate.getResourcePackIndex();
}
@Override
public void writeResourcePackIndex(Map<Integer, String> index) throws IOException {
this.delegate.writeResourcePackIndex(index);
}
@Override
public Optional<InputStream> getResourcePack(String hash) throws IOException {
return this.delegate.getResourcePack(hash);
}
@Override
public OutputStream writeResourcePack(String hash) throws IOException {
return this.delegate.writeResourcePack(hash);
}
@Override
public Map<String, Timeline> getTimelines(PathingRegistry pathingRegistry) throws IOException {
return this.delegate.getTimelines(pathingRegistry);
}
@Override
public void writeTimelines(PathingRegistry pathingRegistry, Map<String, Timeline> timelines) throws IOException {
this.delegate.writeTimelines(pathingRegistry, timelines);
}
@Override
public Optional<BufferedImage> getThumb() throws IOException {
return this.delegate.getThumb();
}
@Override
public void writeThumb(BufferedImage image) throws IOException {
this.delegate.writeThumb(image);
}
@Override
public Optional<InputStream> getThumbBytes() throws IOException {
return this.delegate.getThumbBytes();
}
@Override
public void writeThumbBytes(byte[] image) throws IOException {
this.delegate.writeThumbBytes(image);
}
@Override
public Optional<Set<UUID>> getInvisiblePlayers() throws IOException {
return this.delegate.getInvisiblePlayers();
}
@Override
public void writeInvisiblePlayers(Set<UUID> uuids) throws IOException {
this.delegate.writeInvisiblePlayers(uuids);
}
@Override
public Optional<Set<Marker>> getMarkers() throws IOException {
return this.delegate.getMarkers();
}
@Override
public void writeMarkers(Set<Marker> markers) throws IOException {
this.delegate.writeMarkers(markers);
}
@Override
public Collection<ReplayAssetEntry> getAssets() throws IOException {
return this.delegate.getAssets();
}
@Override
public Optional<InputStream> getAsset(UUID uuid) throws IOException {
return this.delegate.getAsset(uuid);
}
@Override
public OutputStream writeAsset(ReplayAssetEntry asset) throws IOException {
return this.delegate.writeAsset(asset);
}
@Override
public void removeAsset(UUID uuid) throws IOException {
this.delegate.removeAsset(uuid);
}
@Override
public Collection<ModInfo> getModInfo() throws IOException {
return this.delegate.getModInfo();
}
@Override
public void writeModInfo(Collection<ModInfo> modInfo) throws IOException {
this.delegate.writeModInfo(modInfo);
}
@Override
public void close() throws IOException {
this.delegate.close();
}
}

View File

@@ -0,0 +1,23 @@
package com.replaymod.core.files;
import com.replaymod.replaystudio.replay.ReplayFile;
import java.io.IOException;
public class ManagedReplayFile extends DelegatingReplayFile {
private Runnable onClose;
public ManagedReplayFile(ReplayFile delegate, Runnable onClose) {
super(delegate);
this.onClose = onClose;
}
@Override
public void close() throws IOException {
super.close();
onClose.run();
onClose = () -> {};
}
}

View File

@@ -0,0 +1,192 @@
package com.replaymod.core.files;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.gui.RestoreReplayGui;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ZipReplayFile;
import com.replaymod.replaystudio.studio.ReplayStudio;
import de.johni0702.minecraft.gui.container.GuiScreen;
import net.minecraft.util.Util;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class ReplayFilesService {
private final ReplayFoldersService folders;
private final Set<Path> lockedPaths = Collections.newSetFromMap(new ConcurrentHashMap<>());
public ReplayFilesService(ReplayFoldersService folders) {
this.folders = folders;
}
public ReplayFile open(Path path) throws IOException {
return open(path, path);
}
public ReplayFile open(Path input, Path output) throws IOException {
Path realInput = input != null ? input.toAbsolutePath().normalize() : null;
Path realOutput = output.toAbsolutePath().normalize();
if (realInput != null && !lockedPaths.add(realInput)) {
throw new FileLockedException(realInput);
}
if (!Objects.equals(realInput, realOutput) && !lockedPaths.add(realOutput)) {
if (realInput != null) {
lockedPaths.remove(realInput);
}
throw new FileLockedException(realOutput);
}
Runnable onClose = () -> {
if (realInput != null) {
lockedPaths.remove(realInput);
}
lockedPaths.remove(realOutput);
};
ReplayFile replayFile;
try {
replayFile = new ZipReplayFile(
new ReplayStudio(),
realInput != null ? realInput.toFile() : null,
realOutput.toFile(),
folders.getCachePathForReplay(realOutput).toFile()
);
} catch (IOException e) {
onClose.run();
throw e;
}
return new ManagedReplayFile(replayFile, onClose);
}
public void initialScan(ReplayMod core) {
// Move anything which is still in the recording folder into the regular replay folder
// so it can be opened and/or recovered
try (DirectoryStream<Path> paths = Files.newDirectoryStream(folders.getRecordingFolder())) {
for (Path path : paths) {
Path destination = folders.getReplayFolder().resolve(path.getFileName());
if (Files.exists(destination)) {
continue; // better play it save
}
Files.move(path, destination);
}
} catch (IOException e) {
e.printStackTrace();
}
// Restore corrupted replays
try (DirectoryStream<Path> paths = Files.newDirectoryStream(folders.getReplayFolder())) {
for (Path path : paths) {
String name = path.getFileName().toString();
if (name.endsWith(".mcpr.tmp") && Files.isDirectory(path)) {
Path original = path.resolveSibling(FilenameUtils.getBaseName(name));
Path noRecoverMarker = original.resolveSibling(original.getFileName() + ".no_recover");
if (Files.exists(noRecoverMarker)) {
// This file, when its markers are processed, doesn't actually result in any replays.
// So we don't really need to recover it either, let's just get rid of it.
FileUtils.deleteDirectory(path.toFile());
Files.delete(noRecoverMarker);
continue;
}
new RestoreReplayGui(core, GuiScreen.wrap(core.getMinecraft().currentScreen), original.toFile()).display();
}
}
} catch (IOException e) {
e.printStackTrace();
}
// Run general purpose, non-essential cleanup in a background thread
new Thread(this::cleanup, "replaymod-cleanup").start();
}
private void cleanup() {
final long DAYS = 24 * 60 * 60 * 1000;
// Cleanup any cache folders still remaining in the recording folder (we once used to put them there)
try {
Files.walkFileTree(folders.getReplayFolder(), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
String name = dir.getFileName().toString();
if (name.endsWith(".mcpr.cache")) {
FileUtils.deleteDirectory(dir.toFile());
return FileVisitResult.SKIP_SUBTREE;
}
return super.preVisitDirectory(dir, attrs);
}
});
} catch (IOException e) {
e.printStackTrace();
}
// Cleanup raw folder content three weeks after creation (these are pretty valuable for debugging)
try (DirectoryStream<Path> paths = Files.newDirectoryStream(folders.getRawReplayFolder())) {
for (Path path : paths) {
if (Files.getLastModifiedTime(path).toMillis() + 21 * DAYS < System.currentTimeMillis()) {
Files.delete(path);
}
}
} catch (IOException e) {
e.printStackTrace();
}
// Cleanup cache folders 7 days after last modification or when its replay is gone
try (DirectoryStream<Path> paths = Files.newDirectoryStream(folders.getCacheFolder())) {
for (Path path : paths) {
if (Files.isDirectory(path)) {
Path replay = folders.getReplayPathForCache(path);
long lastModified = Files.getLastModifiedTime(path).toMillis();
if (lastModified + 7 * DAYS < System.currentTimeMillis() || !Files.exists(replay)) {
FileUtils.deleteDirectory(path.toFile());
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
// Cleanup deleted corrupted replays
try (DirectoryStream<Path> paths = Files.newDirectoryStream(folders.getReplayFolder())) {
for (Path path : paths) {
String name = path.getFileName().toString();
if (name.endsWith(".mcpr.del") && Files.isDirectory(path)) {
long lastModified = Files.getLastModifiedTime(path).toMillis();
if (lastModified + 2 * DAYS < System.currentTimeMillis()) {
FileUtils.deleteDirectory(path.toFile());
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
// Cleanup leftover no_recover files
try (DirectoryStream<Path> paths = Files.newDirectoryStream(folders.getReplayFolder())) {
for (Path path : paths) {
String name = path.getFileName().toString();
if (name.endsWith(".no_recover")) {
Files.delete(path);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static class FileLockedException extends IOException {
public FileLockedException(Path path) {
super(path.toString());
}
}
}

View File

@@ -0,0 +1,69 @@
package com.replaymod.core.files;
import com.google.common.net.PercentEscaper;
import com.replaymod.core.Setting;
import com.replaymod.core.SettingsRegistry;
import net.minecraft.client.MinecraftClient;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
public class ReplayFoldersService {
private final Path mcDir = MinecraftClient.getInstance().runDirectory.toPath();
private final SettingsRegistry settings;
public ReplayFoldersService(SettingsRegistry settings) {
this.settings = settings;
}
public Path getReplayFolder() throws IOException {
return Files.createDirectories(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"));
}
/**
* Folder into which replays are recorded.
* 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"));
}
/**
* Folder in which replay cache files are stored.
* 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)));
try {
Files.setAttribute(path, "dos:hidden", true);
} catch (UnsupportedOperationException ignored) {
} catch (Exception e) {
e.printStackTrace();
}
return path;
}
private static final PercentEscaper CACHE_FILE_NAME_ENCODER = new PercentEscaper("-_ ", false);
public Path getCachePathForReplay(Path replay) throws IOException {
Path replayFolder = getReplayFolder();
Path cacheFolder = getCacheFolder();
Path relative = replayFolder.toAbsolutePath().relativize(replay.toAbsolutePath());
return cacheFolder.resolve(CACHE_FILE_NAME_ENCODER.escape(relative.toString()));
}
public Path getReplayPathForCache(Path cache) throws IOException {
String relative = URLDecoder.decode(cache.getFileName().toString(), "UTF-8");
Path replayFolder = getReplayFolder();
return replayFolder.resolve(relative);
}
}

View File

@@ -108,7 +108,7 @@ public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> {
}
private void tryRecover(Consumer<Float> progress) throws IOException {
ReplayFile replayFile = ReplayMod.instance.openReplay(file.toPath());
ReplayFile replayFile = ReplayMod.instance.files.open(file.toPath());
// Commit all not-yet-committed files into the main zip file.
// If we don't do this, then re-writing packet data below can actually overwrite uncommitted packet data!
replayFile.save();

View File

@@ -10,6 +10,10 @@ import org.spongepowered.asm.mixin.gen.Accessor;
import java.util.Queue;
//#if MC>=11800
//$$ import java.util.function.Supplier;
//#endif
//#if MC>=11400
import java.util.concurrent.CompletableFuture;
//#endif
@@ -49,7 +53,11 @@ public interface MinecraftAccessor {
//#endif
@Accessor("crashReport")
//#if MC>=11800
//$$ Supplier<CrashReport> getCrashReporter();
//#else
CrashReport getCrashReporter();
//#endif
//#if MC<11400
//$$ @Accessor

View File

@@ -13,6 +13,12 @@ import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.render.BufferBuilder;
import net.minecraft.util.Identifier;
import net.minecraft.util.Util;
import net.minecraft.util.math.Vec3d;
//#if MC>=11604
//#else
//$$ import net.minecraft.entity.Entity;
//#endif
//#if MC>=11600
import net.minecraft.resource.ResourcePackSource;
@@ -55,7 +61,6 @@ import org.lwjgl.glfw.GLFW;
//#if MC>=10904
import com.replaymod.render.blend.mixin.ParticleAccessor;
import net.minecraft.client.particle.Particle;
import net.minecraft.util.math.Vec3d;
//#endif
//#if MC>=10800
@@ -297,6 +302,12 @@ public class MCVer {
}
//#endif
//#if MC<=11601
//$$ public static Vec3d getTrackedPosition(Entity entity) {
//$$ return new Vec3d(entity.trackedX, entity.trackedY, entity.trackedZ);
//$$ }
//#endif
public static void openFile(File file) {
//#if MC>=11400
Util.getOperatingSystem().open(file);

View File

@@ -1,5 +1,9 @@
package com.replaymod.core.versions;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.replaymod.core.mixin.MinecraftAccessor;
import com.replaymod.gradle.remap.Pattern;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.options.KeyBinding;
@@ -10,11 +14,14 @@ import net.minecraft.client.render.Tessellator;
import net.minecraft.client.render.entity.EntityRenderDispatcher;
import net.minecraft.client.sound.PositionedSoundInstance;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.util.crash.CrashException;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.crash.CrashReportSection;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;
import net.minecraft.world.chunk.WorldChunk;
@@ -23,6 +30,12 @@ import net.minecraft.world.chunk.WorldChunk;
import org.lwjgl.opengl.GL11;
//#endif
//#if MC>=11600
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.math.Matrix4f;
//#else
//#endif
//#if MC>=11400
import net.minecraft.client.gui.widget.AbstractButtonWidget;
import net.minecraft.client.util.Window;
@@ -509,4 +522,55 @@ class Patterns {
GL11.glRotatef(angle, x, y, z);
//#endif
}
// FIXME preprocessor bug: there are mappings for this, not sure why it doesn't remap by itself
//#if MC>=11600
@Pattern
private static Matrix4f getPositionMatrix(MatrixStack.Entry stack) {
//#if MC>=11800
//$$ return stack.getPositionMatrix();
//#else
return stack.getModel();
//#endif
}
//#else
//$$ private static void getPositionMatrix() {}
//#endif
@SuppressWarnings("rawtypes") // preprocessor bug: doesn't work with generics
@Pattern
private static void Futures_addCallback(ListenableFuture future, FutureCallback callback) {
//#if MC>=11800
//$$ Futures.addCallback(future, callback, Runnable::run);
//#else
Futures.addCallback(future, callback);
//#endif
}
@Pattern
private static void setCrashReport(MinecraftClient mc, CrashReport report) {
//#if MC>=11800
//$$ mc.setCrashReportSupplier(() -> report);
//#else
mc.setCrashReport(report);
//#endif
}
@Pattern
private static CrashException crashReportToException(MinecraftClient mc) {
//#if MC>=11800
//$$ return new CrashException(((MinecraftAccessor) mc).getCrashReporter().get());
//#else
return new CrashException(((MinecraftAccessor) mc).getCrashReporter());
//#endif
}
@Pattern
private static Vec3d getTrackedPosition(Entity entity) {
//#if MC>=11604
return entity.getTrackedPosition();
//#else
//$$ return com.replaymod.core.versions.MCVer.getTrackedPosition(entity);
//#endif
}
}

View File

@@ -60,7 +60,7 @@ public class GuiEditReplay extends AbstractGuiPopup<GuiEditReplay> {
super(container);
this.inputPath = inputPath;
try (ReplayFile replayFile = ReplayMod.instance.openReplay(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);
timeline.setSize(300, 20)
@@ -147,7 +147,7 @@ public class GuiEditReplay extends AbstractGuiPopup<GuiEditReplay> {
ProgressPopup progressPopup = new ProgressPopup(this);
new Thread(() -> {
try (ReplayFile replayFile = ReplayMod.instance.openReplay(inputPath)) {
try (ReplayFile replayFile = ReplayMod.instance.files.open(inputPath)) {
replayFile.writeMarkers(markers);
replayFile.save();
} catch (IOException e) {

View File

@@ -4,6 +4,7 @@ import com.replaymod.core.ReplayMod;
import com.replaymod.core.versions.MCVer;
import com.replaymod.replaystudio.PacketData;
import com.replaymod.replaystudio.data.Marker;
import com.replaymod.replaystudio.filter.DimensionTracker;
import com.replaymod.replaystudio.filter.SquashFilter;
import com.replaymod.replaystudio.filter.StreamFilter;
import com.replaymod.replaystudio.io.ReplayInputStream;
@@ -48,7 +49,7 @@ public class MarkerProcessor {
public static final String MARKER_NAME_SPLIT = "_RM_SPLIT";
private static boolean hasWork(Path path) throws IOException {
try (ReplayFile inputReplayFile = ReplayMod.instance.openReplay(path)) {
try (ReplayFile inputReplayFile = ReplayMod.instance.files.open(path)) {
return inputReplayFile.getMarkers().or(HashSet::new).stream().anyMatch(m -> m.getName() != null && m.getName().startsWith("_RM_"));
}
}
@@ -112,7 +113,7 @@ public class MarkerProcessor {
ReplayMod mod = ReplayMod.instance;
if (!hasWork(path)) {
ReplayMetaData metaData;
try (ReplayFile inputReplayFile = mod.openReplay(path)) {
try (ReplayFile inputReplayFile = mod.files.open(path)) {
metaData = inputReplayFile.getMetaData();
}
return Collections.singletonList(Pair.of(path, metaData));
@@ -122,11 +123,12 @@ public class MarkerProcessor {
int splitCounter = 0;
PacketTypeRegistry registry = MCVer.getPacketTypeRegistry(true);
SquashFilter squashFilter = new SquashFilter();
DimensionTracker dimensionTracker = new DimensionTracker();
SquashFilter squashFilter = new SquashFilter(null, null);
List<Pair<Path, ReplayMetaData>> outputPaths = new ArrayList<>();
Path rawFolder = ReplayMod.instance.getRawReplayFolder();
Path rawFolder = ReplayMod.instance.folders.getRawReplayFolder();
Path inputPath = rawFolder.resolve(path.getFileName());
for (int i = 1; Files.exists(inputPath); i++) {
inputPath = inputPath.resolveSibling(replayName + "." + i + ".mcpr");
@@ -134,7 +136,7 @@ public class MarkerProcessor {
Files.createDirectories(inputPath.getParent());
Files.move(path, inputPath);
try (ReplayFile inputReplayFile = mod.openReplay(inputPath)) {
try (ReplayFile inputReplayFile = mod.files.open(inputPath)) {
List<Marker> markers = inputReplayFile.getMarkers().or(HashSet::new)
.stream().sorted(Comparator.comparing(Marker::getTime)).collect(Collectors.toList());
Iterator<Marker> markerIterator = markers.iterator();
@@ -151,7 +153,7 @@ public class MarkerProcessor {
while (nextPacket != null && outputFileSuffixes.hasNext()) {
Path outputPath = path.resolveSibling(replayName + outputFileSuffixes.next() + ".mcpr");
try (ReplayFile outputReplayFile = mod.openReplay(null, outputPath)) {
try (ReplayFile outputReplayFile = mod.files.open(null, outputPath)) {
long duration = 0;
Set<Marker> outputMarkers = new HashSet<>();
ReplayMetaData metaData = inputReplayFile.getMetaData();
@@ -180,7 +182,7 @@ public class MarkerProcessor {
cutFilter.release();
}
startCutOffset = nextMarker.getTime();
cutFilter = new SquashFilter();
cutFilter = new SquashFilter(dimensionTracker);
} else if (MARKER_NAME_END_CUT.equals(nextMarker.getName())) {
timeOffset += nextMarker.getTime() - startCutOffset;
if (cutFilter != null) {
@@ -208,6 +210,7 @@ public class MarkerProcessor {
continue;
}
dimensionTracker.onPacket(null, nextPacket);
if (hasFurtherOutputs) {
squashFilter.onPacket(null, nextPacket);
}

View File

@@ -1,6 +1,5 @@
package com.replaymod.extras.playeroverview;
import com.google.common.base.Optional;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.events.PreRenderHandCallback;
import com.replaymod.core.utils.Utils;
@@ -10,6 +9,7 @@ import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replay.camera.CameraEntity;
import com.replaymod.replay.events.ReplayClosedCallback;
import com.replaymod.replay.events.ReplayOpenedCallback;
import com.replaymod.replaystudio.lib.guava.base.Optional;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.PlayerEntity;

View File

@@ -67,7 +67,7 @@ public class RealtimeTimelinePlayer extends AbstractTimelinePlayer {
@Override
public long getTimePassed() {
if (firstFrame) return 0;
if (firstFrame) return startOffset;
if (loadingResources) return timeBeforeResourceLoading;
return System.currentTimeMillis() - startTime;
}

View File

@@ -87,6 +87,9 @@ public class GuiRecordingControls extends EventRegistrations {
if (!(guiScreen instanceof GameMenuScreen)) {
return;
}
if (buttonList.isEmpty()) {
return; // menu-less pause (F3+Esc)
}
Function<Integer, Integer> yPos =
MCVer.findButton(buttonList, "menu.returnToMenu", 1)
.map(Optional::of)

View File

@@ -154,7 +154,7 @@ public class GuiSavingReplay {
}
try {
Path replaysFolder = core.getReplayFolder();
Path replaysFolder = core.folders.getReplayFolder();
Path newPath = replaysFolder.resolve(Utils.replayNameToFileName(newName));
for (int i = 1; Files.exists(newPath); i++) {
newPath = replaysFolder.resolve(Utils.replayNameToFileName(newName + " (" + i + ")"));

View File

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

View File

@@ -41,12 +41,10 @@ import java.util.Collections;
//#endif
//#if MC>=10904
import com.replaymod.recording.mixin.EntityLivingBaseAccessor;
import net.minecraft.network.packet.s2c.play.EntityTrackerUpdateS2CPacket;
import net.minecraft.network.packet.s2c.play.PlaySoundS2CPacket;
import net.minecraft.network.packet.s2c.play.WorldEventS2CPacket;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.entity.data.DataTracker;
import net.minecraft.util.Hand;
import net.minecraft.sound.SoundCategory;
import net.minecraft.sound.SoundEvent;
@@ -73,10 +71,6 @@ public class RecordingEventHandler extends EventRegistrations {
private boolean wasSleeping;
private int lastRiding = -1;
private Integer rotationYawHeadBefore;
//#if MC>=10904
private boolean wasHandActive;
private Hand lastActiveHand;
//#endif
public RecordingEventHandler(PacketListener packetListener) {
this.packetListener = packetListener;
@@ -335,18 +329,6 @@ public class RecordingEventHandler extends EventRegistrations {
wasSleeping = false;
}
//#if MC>=10904
// Active hand (e.g. eating, drinking, blocking)
if (player.isUsingItem() ^ wasHandActive || player.getActiveHand() != lastActiveHand) {
wasHandActive = player.isUsingItem();
lastActiveHand = player.getActiveHand();
DataTracker dataManager = new DataTracker(null);
int state = (wasHandActive ? 1 : 0) | (lastActiveHand == Hand.OFF_HAND ? 2 : 0);
dataManager.startTracking(EntityLivingBaseAccessor.getLivingFlags(), (byte) state);
packetListener.save(new EntityTrackerUpdateS2CPacket(player.getEntityId(), dataManager, true));
}
//#endif
} catch(Exception e1) {
e1.printStackTrace();
}

View File

@@ -280,7 +280,7 @@ public class PacketListener extends ChannelInboundHandlerAdapter {
// We still have the replay, so we just save it (at least for a few weeks) in case they change their mind
String replayName = FilenameUtils.getBaseName(outputPath.getFileName().toString());
Path rawFolder = ReplayMod.instance.getRawReplayFolder();
Path rawFolder = ReplayMod.instance.folders.getRawReplayFolder();
Path rawPath = rawFolder.resolve(outputPath.getFileName());
for (int i = 1; Files.exists(rawPath); i++) {
rawPath = rawPath.resolveSibling(replayName + "." + i + ".mcpr");

View File

@@ -183,12 +183,16 @@ public class Util {
}
public static String getTileEntityId(BlockEntity tileEntity) {
//#if MC>=11800
//$$ NbtCompound nbt = tileEntity.createNbt();
//#else
CompoundTag nbt = new CompoundTag();
//#if MC>=11400
tileEntity.toTag(nbt);
//#else
//$$ tileEntity.writeToNBT(nbt);
//#endif
//#endif
return nbt.getString("id");
}

View File

@@ -228,7 +228,7 @@ public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> implements
ReplayHandler replayHandler;
ReplayFile replayFile = null;
try {
replayFile = mod.getCore().openReplay(next.getKey().toPath());
replayFile = mod.getCore().files.open(next.getKey().toPath());
replayHandler = mod.startReplay(replayFile, true, false);
} catch (IOException e) {
Utils.error(LOGGER, container, CrashReport.create(e, "Opening replay"), () -> {});

View File

@@ -0,0 +1 @@
// 1.18+

View File

@@ -24,7 +24,20 @@ import java.util.concurrent.locks.ReentrantLock;
public abstract class Mixin_BlockOnChunkRebuilds implements ForceChunkLoadingHook.IBlockOnChunkRebuilds {
@Shadow @Final private Queue<BlockBufferBuilderStorage> threadBuffers;
//#if MC>=11800
//$$ @org.spongepowered.asm.mixin.Unique
//$$ private boolean upload() {
//$$ boolean anything = false;
//$$ Runnable runnable;
//$$ while ((runnable = this.uploadQueue.poll()) != null) {
//$$ runnable.run();
//$$ anything = true;
//$$ }
//$$ return anything;
//$$ }
//#else
@Shadow public abstract boolean upload();
//#endif
@Shadow @Final private TaskExecutor<Runnable> mailbox;

View File

@@ -20,7 +20,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
public abstract class Mixin_ChromaKeyColorSky {
@Shadow @Final private MinecraftClient client;
//#if MC>=11400 || 10710>=MC
//#if MC>=11800
//$$ @Inject(method = "renderSky(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/util/math/Matrix4f;FLjava/lang/Runnable;)V",
//$$ at = @At(value = "INVOKE", target = "Ljava/lang/Runnable;run()V", remap = false, shift = At.Shift.AFTER),
//$$ cancellable = true)
//#elseif MC>=11400 || 10710>=MC
@Inject(method = "renderSky", at = @At("HEAD"), cancellable = true)
//#else
//$$ @Inject(method = "renderSky(FI)V", at = @At("HEAD"), cancellable = true)

View File

@@ -29,12 +29,10 @@ public abstract class Mixin_ChromaKeyForceSky {
//#if MC>=11500
@ModifyConstant(method = "render", constant = @Constant(intValue = 4))
//#else
//#if MC>=11400
//#elseif MC>=11400
//$$ @ModifyConstant(method = "renderCenter", constant = @Constant(intValue = 4))
//#else
//$$ @ModifyConstant(method = "updateCameraAndRender(FJ)V", constant = @Constant(intValue = 4))
//#endif
//$$ @ModifyConstant(method = "renderWorldPass", constant = @Constant(intValue = 4))
//#endif
private int forceSkyWhenChromaKeying(int value) {
EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) this.client.gameRenderer).replayModRender_getHandler();

View File

@@ -10,7 +10,12 @@ import org.spongepowered.asm.mixin.injection.ModifyArg;
@Mixin(GameRenderer.class)
public abstract class Mixin_PreserveDepthDuringHandRendering {
@ModifyArg(
// FIXME preprocessor bug: 1.8.9 uses method with `(FJ)V` when just name would be enough
//#if MC>=10809
method = "renderWorld",
//#else
//$$ method = "updateCameraAndRender(F)V",
//#endif
at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;clear(IZ)V"),
index = 0
)

View File

@@ -1,12 +1,12 @@
package com.replaymod.render.utils;
import com.google.common.base.Optional;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import com.replaymod.render.RenderSettings;
import com.replaymod.replaystudio.lib.guava.base.Optional;
import com.replaymod.replaystudio.pathing.PathingRegistry;
import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.pathing.serialize.TimelineSerialization;
@@ -81,10 +81,14 @@ public class RenderJob {
}
try (InputStream in = optIn.get();
InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
return new GsonBuilder()
List<RenderJob> jobs = new GsonBuilder()
.registerTypeAdapter(Timeline.class, new TimelineTypeAdapter())
.create()
.fromJson(reader, new TypeToken<List<RenderJob>>(){}.getType());
if (jobs == null) {
jobs = new ArrayList<>();
}
return jobs;
}
}
}

View File

@@ -55,6 +55,7 @@ import net.minecraft.network.packet.s2c.play.SignEditorOpenS2CPacket;
import net.minecraft.network.packet.s2c.play.StatisticsS2CPacket;
import net.minecraft.text.Text;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.Vec3d;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
@@ -453,8 +454,14 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
//#if MC>=11400
if (p instanceof ChunkDataS2CPacket) {
Runnable doLightUpdates = () -> {
if (mc.world != null) {
LightingProvider provider = mc.world.getChunkManager().getLightingProvider();
ClientWorld world = mc.world;
if (world != null) {
//#if MC>=11800
//$$ while (!world.hasNoChunkUpdaters()) {
//$$ world.runQueuedChunkUpdates();
//$$ }
//#endif
LightingProvider provider = world.getChunkManager().getLightingProvider();
while (provider.hasUpdates()) {
provider.doLightUpdates(Integer.MAX_VALUE, true, true);
}
@@ -604,14 +611,19 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
//#if MC>=11400
p = new GameJoinS2CPacket(
entId,
//#if MC>=11800
//$$ packet.hardcore(),
//#endif
GameMode.SPECTATOR,
//#if MC>=11600
GameMode.SPECTATOR,
//#endif
//#if MC<11800
//#if MC>=11500
packet.getSha256Seed(),
//#endif
false,
//#endif
//#if MC>=11600
//#if MC>=11603
packet.getDimensionIds(),
@@ -626,11 +638,17 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
//#else
//$$ packet.getDimension(),
//#endif
//#if MC>=11800
//$$ packet.sha256Seed(),
//#endif
0, // max players (has no getter -> never actually used)
//#if MC<11600
//$$ packet.getGeneratorType(),
//#endif
packet.getViewDistance(),
//#if MC>=11800
//$$ packet.simulationDistance(),
//#endif
packet.hasReducedDebugInfo()
//#if MC>=11500
, packet.showsDeathScreen()
@@ -1194,15 +1212,7 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
for (Entity entity : entitiesInChunk) {
// Skip interpolation of position updates coming from server
// (See: newX in EntityLivingBase or otherPlayerMPX in EntityOtherPlayerMP)
// Needs to be called at least 4 times thanks to
// EntityOtherPlayerMP#otherPlayerMPPosRotationIncrements (max vanilla value is 3)
for (int i = 0; i < 4; i++) {
//#if MC>=11400
entity.tick();
//#else
//$$ entity.onUpdate();
//#endif
}
forcePositionForVehicleAndSelf(entity);
// Check whether the entity has left the chunk
//#if MC>=11700
@@ -1273,6 +1283,26 @@ public class FullReplaySender extends ChannelDuplexHandler implements ReplaySend
return p; // During synchronous playback everything is sent normally
}
private void forcePositionForVehicleAndSelf(Entity entity) {
Entity vehicle = entity.getVehicle();
if (vehicle != null) {
forcePositionForVehicleAndSelf(vehicle);
}
// Skip interpolation of position updates coming from server
// (See: newX in EntityLivingBase or otherPlayerMPX in EntityOtherPlayerMP)
int ticks = 0;
Vec3d prevPos;
do {
prevPos = entity.getPos();
if (vehicle != null) {
entity.tickRiding();
} else {
entity.tick();
}
} while (prevPos.squaredDistanceTo(entity.getPos()) > 0.0001 && ticks++ < 100);
}
private static final class PacketData {
private static final com.github.steveice10.netty.buffer.ByteBuf byteBuf = com.github.steveice10.netty.buffer.Unpooled.buffer();
private static final NetOutput netOutput = new ByteBufNetOutput(byteBuf);

View File

@@ -159,7 +159,7 @@ public class ReplayModReplay implements Module {
}
public void startReplay(File file) throws IOException {
startReplay(core.openReplay(file.toPath()));
startReplay(core.files.open(file.toPath()));
}
public void startReplay(ReplayFile replayFile) throws IOException {

View File

@@ -22,7 +22,6 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.AbstractClientPlayerEntity;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.decoration.ItemFrameEntity;
import net.minecraft.entity.player.PlayerEntity;
@@ -249,14 +248,7 @@ public class CameraEntity
this.lastRenderX = to.lastRenderX;
this.lastRenderY = to.lastRenderY + yOffset;
this.lastRenderZ = to.lastRenderZ;
if (to instanceof LivingEntity) {
LivingEntity toLiving = (LivingEntity) to;
this.headYaw = toLiving.headYaw;
this.prevHeadYaw = toLiving.prevHeadYaw;
} else {
this.headYaw = to.yaw;
this.prevHeadYaw = to.prevYaw;
}
this.wrapArmYaw();
updateBoundingBox();
}
@@ -265,7 +257,7 @@ public class CameraEntity
public float getYaw(float tickDelta) {
Entity view = this.client.getCameraEntity();
if (view != null && view != this) {
return this.prevHeadYaw + (this.headYaw - this.prevHeadYaw) * tickDelta;
return this.prevYaw + (this.yaw - this.prevYaw) * tickDelta;
}
return super.getYaw(tickDelta);
}
@@ -365,6 +357,11 @@ public class CameraEntity
public boolean isSubmergedIn(Tag<Fluid> fluid) {
return falseUnlessSpectating(entity -> entity.isSubmergedIn(fluid));
}
@Override
public float getUnderwaterVisibility() {
return falseUnlessSpectating(__ -> true) ? super.getUnderwaterVisibility() : 1f;
}
//#else
//#if MC>=10800
//$$ @Override
@@ -707,7 +704,35 @@ public class CameraEntity
this.lastRenderYaw = this.renderYaw;
this.lastRenderPitch = this.renderPitch;
this.renderPitch = this.renderPitch + (this.pitch - this.renderPitch) * 0.5f;
this.renderYaw = this.renderYaw + (this.headYaw - this.renderYaw) * 0.5f;
this.renderYaw = this.renderYaw + wrapDegrees(this.yaw - this.renderYaw) * 0.5f;
this.wrapArmYaw();
}
/**
* Minecraft renders the arm offset based on the difference between {@link #yaw} and {@link #renderYaw}. It does not
* wrap around the difference though, so if {@link #yaw} just wrapped around from 350 to 10 but {@link #renderYaw}
* is still at 355, then the difference will be inappropriately large. To fix this, we always wrap the
* {@link #renderYaw} such that it is no more than 180 degrees away from {@link #yaw}, even if that requires going
* outside the normal range.
*/
private void wrapArmYaw() {
this.renderYaw = wrapDegreesTo(this.renderYaw, this.yaw);
this.lastRenderYaw = wrapDegreesTo(this.lastRenderYaw, this.renderYaw);
}
private static float wrapDegreesTo(float value, float towardsValue) {
while (towardsValue - value < -180) {
value -= 360;
}
while (towardsValue - value >= 180) {
value += 360;
}
return value;
}
private static float wrapDegrees(float value) {
value %= 360;
return wrapDegreesTo(value, 0);
}
public boolean canSpectate(Entity e) {

View File

@@ -0,0 +1,9 @@
package com.replaymod.replay.ext;
public interface EntityExt {
float replaymod$getTrackedYaw();
void replaymod$setTrackedYaw(float value);
float replaymod$getTrackedPitch();
void replaymod$setTrackedPitch(float value);
}

View File

@@ -118,7 +118,7 @@ public class GuiReplayViewer extends GuiScreen {
@Override
public void run() {
try {
File folder = mod.getCore().getReplayFolder().toFile();
File folder = mod.getCore().folders.getReplayFolder().toFile();
MCVer.openFile(folder);
} catch (IOException e) {
@@ -221,7 +221,7 @@ public class GuiReplayViewer extends GuiScreen {
this.mod = mod;
try {
list.setFolder(mod.getCore().getReplayFolder().toFile());
list.setFolder(mod.getCore().folders.getReplayFolder().toFile());
} catch (IOException e) {
throw new CrashException(CrashReport.create(e, "Getting replay folder"));
}
@@ -370,7 +370,7 @@ public class GuiReplayViewer extends GuiScreen {
Arrays.sort(files, Comparator.<File>comparingLong(f -> lastModified.computeIfAbsent(f, File::lastModified)).reversed());
for (final File file : files) {
if (Thread.interrupted()) break;
try (ReplayFile replayFile = ReplayMod.instance.openReplay(file.toPath())) {
try (ReplayFile replayFile = ReplayMod.instance.files.open(file.toPath())) {
final Image thumb = Optional.ofNullable(replayFile.getThumbBytes().orNull()).flatMap(stream -> {
try (InputStream in = stream) {
return Optional.of(Image.read(in));

View File

@@ -0,0 +1,13 @@
package com.replaymod.replay.mixin;
import net.minecraft.client.world.ClientWorld;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(ClientWorld.class)
public interface ClientWorldAccessor {
//#if MC>=11800
//$$ @Accessor
//$$ net.minecraft.world.EntityList getEntityList();
//#endif
}

View File

@@ -0,0 +1 @@
// 1.18+ only

View File

@@ -0,0 +1 @@
// 1.12.2 and below

View File

@@ -0,0 +1,36 @@
package com.replaymod.replay.mixin.entity_tracking;
import com.replaymod.replay.ext.EntityExt;
import net.minecraft.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@Mixin(Entity.class)
public abstract class Mixin_EntityExt implements EntityExt {
@Unique
private float trackedYaw;
@Unique
private float trackedPitch;
@Override
public float replaymod$getTrackedYaw() {
return this.trackedYaw;
}
@Override
public float replaymod$getTrackedPitch() {
return this.trackedPitch;
}
@Override
public void replaymod$setTrackedYaw(float value) {
this.trackedYaw = value;
}
@Override
public void replaymod$setTrackedPitch(float value) {
this.trackedPitch = value;
}
}

View File

@@ -0,0 +1,109 @@
package com.replaymod.replay.mixin.entity_tracking;
import com.replaymod.replay.ext.EntityExt;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.entity.Entity;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Every Entity on the client has at least two position/rotation states: The local one and the server ("tracked") one.
* When receiving an update from the server, the server one needs to be updated and the local one will then usually be
* interpolated to the server one within a few ticks.
*
* Minecraft however incorrectly implements the server rotation update for position-only update packets by
* interpolating to the local rotation, which might not yet match the server rotation, rather than to the previously
* received server rotation.
* Similarly, with 1.15 and later, it incorrectly implements the server position update for rotation-only update packets
* by interpolating to the local position rather than the server one.
*
* Each of these will cause the client position to be in an incorrect state until the next update packed for the
* respective rotation/position of that entity.
*
* This mixin fixes those two issues by redirecting to the server rotation/position respectively.
* Minecraft does not currently even track the server rotation, so we need to do that as well.
*/
@Mixin(ClientPlayNetworkHandler.class)
public class Mixin_FixPartialUpdates {
//
// Use correct rotation for position-only updates
//
//#if MC>=11700
//$$ @Redirect(method = "onEntityUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;getYaw()F"))
//#else
@Redirect(method = "onEntityUpdate", at = @At(value = "FIELD", target = "Lnet/minecraft/entity/Entity;yaw:F", opcode = Opcodes.GETFIELD))
//#endif
private float getTrackedYaw(Entity instance) {
return ((EntityExt) instance).replaymod$getTrackedYaw();
}
//#if MC>=11700
//$$ @Redirect(method = "onEntityUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;getPitch()F"))
//#else
@Redirect(method = "onEntityUpdate", at = @At(value = "FIELD", target = "Lnet/minecraft/entity/Entity;pitch:F", opcode = Opcodes.GETFIELD))
//#endif
private float getTrackedPitch(Entity instance) {
return ((EntityExt) instance).replaymod$getTrackedPitch();
}
//#if MC>=11500
//
// Use correct position for rotation-only updates
//
@Redirect(method = "onEntityUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;getX()D"))
private double getTrackedX(Entity instance) {
return instance.getTrackedPosition().getX();
}
@Redirect(method = "onEntityUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;getY()D"))
private double getTrackedY(Entity instance) {
return instance.getTrackedPosition().getY();
}
@Redirect(method = "onEntityUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;getZ()D"))
private double getTrackedZ(Entity instance) {
return instance.getTrackedPosition().getZ();
}
//#endif
//
// Track server rotation
//
private static final String ENTITY_UPDATE = "Lnet/minecraft/entity/Entity;updateTrackedPositionAndAngles(DDDFFIZ)V";
@Unique
private Entity entity;
@ModifyVariable(method = "onEntityUpdate", at = @At(value = "INVOKE", target = ENTITY_UPDATE), ordinal = 0)
private Entity captureEntity(Entity entity) {
return this.entity = entity;
}
@Inject(method = "onEntityUpdate", at = @At("RETURN"))
private void resetEntityField(CallbackInfo ci) {
this.entity = null;
}
@ModifyArg(method = "onEntityUpdate", at = @At(value = "INVOKE", target = ENTITY_UPDATE), index = 3)
private float captureTrackedYaw(float value) {
((EntityExt) this.entity).replaymod$setTrackedYaw(value);
return value;
}
@ModifyArg(method = "onEntityUpdate", at = @At(value = "INVOKE", target = ENTITY_UPDATE), index = 4)
private float captureTrackedPitch(float value) {
((EntityExt) this.entity).replaymod$setTrackedPitch(value);
return value;
}
}

View File

@@ -451,14 +451,14 @@ public class GuiPathing {
prevTime = time;
}
public void syncTimeButtonPressed() {
private Integer computeSyncTime(int cursor) {
// Current replay time
int time = replayHandler.getReplaySender().currentTimeStamp();
// Position of the cursor
int cursor = timeline.getCursorPosition();
// Get the last time keyframe before the cursor
mod.getCurrentTimeline().getTimePath().getKeyframes().stream()
.filter(it -> it.getTime() <= cursor).reduce((__, last) -> last).ifPresent(keyframe -> {
Keyframe keyframe = mod.getCurrentTimeline().getTimePath().getKeyframes().stream()
.filter(it -> it.getTime() <= cursor).reduce((__, last) -> last)
.orElse(null);
if (keyframe != null) {
// Cursor position at the keyframe
int keyframeCursor = (int) keyframe.getTime();
// Replay time at the keyframe
@@ -470,11 +470,49 @@ public class GuiPathing {
double speed = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) ? 1 : replayHandler.getOverlay().getSpeedSliderValue();
// Cursor time passed
int cursorPassed = (int) (timePassed / speed);
// Move cursor to new position
timeline.setCursorPosition(keyframeCursor + cursorPassed).ensureCursorVisibleWithPadding();
// Deselect keyframe to allow the user to add a new one right away
mod.setSelected(null, 0);
});
// Return new position
return keyframeCursor + cursorPassed;
} else {
// No keyframes before cursor
return null;
}
}
public void syncTimeButtonPressed() {
// Position of the cursor
int cursor = timeline.getCursorPosition();
// Update cursor once
Integer updatedCursor = computeSyncTime(cursor);
if (updatedCursor == null) {
return; // no keyframes before cursor, nothing we can do
}
cursor = updatedCursor;
// Repeatedly update until we find a fix point
while (true) {
updatedCursor = computeSyncTime(cursor);
if (updatedCursor == null) {
// Cursor has gotten stuck before in front of all keyframes.
// Let's just use the last value we got, this shouldn't happen with ordinary timelines anyway.
break;
}
if (updatedCursor == cursor) {
// Found the fix point, we can stop now
break;
}
if (updatedCursor < cursor) {
// We've gone backwards, we'll likely get stuck in a loop, so abort the whole thing
return;
}
// Found a new position, take it, repeat
cursor = updatedCursor;
}
// Move cursor to new position
timeline.setCursorPosition(cursor).ensureCursorVisibleWithPadding();
// Deselect keyframe to allow the user to add a new one right away
mod.setSelected(null, 0);
}
public boolean deleteButtonPressed() {

View File

@@ -4,6 +4,9 @@
"mixins": [],
"server": [],
"client": [
//#if MC>=11800
//$$ "ChunkInfoAccessor",
//#endif
"Mixin_ChromaKeyColorSky",
"Mixin_ChromaKeyDisableFog",
"Mixin_ChromaKeyForceSky",

View File

@@ -5,14 +5,22 @@
"mixins": [],
"server": [],
"client": [
"entity_tracking.Mixin_EntityExt",
"entity_tracking.Mixin_FixPartialUpdates",
"Mixin_FixNPCSkinCaching",
//#if MC>=11800
//$$ "Mixin_FixEntityNotTracking",
//#endif
//#if MC>=11600
"Mixin_MoveRealmsButton",
//#endif
//#if MC>=11400
"MixinCamera",
"MixinInGameHud",
//#else
//$$ "Mixin_FixHandOffsetTickDelta",
//#endif
"ClientWorldAccessor",
"EntityLivingBaseAccessor",
//#if MC>=11400
"Mixin_ShowSpectatedHand_NoOF",

View File

@@ -1 +1 @@
2.6.1
2.6.2

View File

@@ -0,0 +1,46 @@
package com.replaymod.replay.mixin;
import com.replaymod.replay.camera.CameraEntity;
import net.minecraft.client.entity.EntityPlayerSP;
import net.minecraft.client.renderer.ItemRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* In 1.12.2 and below, Vanilla does not respect the tickDelta value when getting the yaw/pitch of the player for
* computing the hand offset.
* This causes the hand movement to be jittery when spectating another player in a replay.
*/
@Mixin(ItemRenderer.class)
public abstract class Mixin_FixHandOffsetTickDelta {
@Redirect(method = "rotateArm", at = @At(value = "FIELD", target = "Lnet/minecraft/client/entity/EntityPlayerSP;rotationYaw:F"))
private float getYaw(
EntityPlayerSP instance,
//#if MC<10900
//$$ EntityPlayerSP arg,
//#endif
float tickDelta
) {
if (instance instanceof CameraEntity) {
return instance.prevRotationYaw + (instance.rotationYaw - instance.prevRotationYaw) * tickDelta;
} else {
return instance.rotationYaw;
}
}
@Redirect(method = "rotateArm", at = @At(value = "FIELD", target = "Lnet/minecraft/client/entity/EntityPlayerSP;rotationPitch:F"))
private float getPitch(
EntityPlayerSP instance,
//#if MC<10900
//$$ EntityPlayerSP arg,
//#endif
float tickDelta
) {
if (instance instanceof CameraEntity) {
return instance.prevRotationPitch + (instance.rotationPitch - instance.prevRotationPitch) * tickDelta;
} else {
return instance.rotationPitch;
}
}
}

View File

@@ -27,6 +27,7 @@ net.minecraft.client.gui.GuiYesNoCallback confirmResult() confirmClicked()
net.minecraft.util.text.ITextComponent getString() getUnformattedText()
net.minecraft.network.play.server.SPacketRespawn func_212643_b() getDimensionID()
net.minecraft.client.Minecraft getPackFinder() getResourcePackRepository()
net.minecraft.entity.Entity getPositionVec() getPositionVector()
net.minecraftforge.client.event.GuiScreenEvent.InitGuiEvent addWidget() addButton()
net.minecraftforge.client.event.GuiScreenEvent.InitGuiEvent removeWidget() removeButton()

0
versions/1.18.1/.gitkeep Normal file
View File

View File

@@ -0,0 +1,12 @@
// 1.18+
package com.replaymod.render.mixin;
import net.minecraft.client.render.chunk.ChunkBuilder;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(targets = "net.minecraft.client.render.WorldRenderer$ChunkInfo")
public interface ChunkInfoAccessor {
@Accessor
ChunkBuilder.BuiltChunk getChunk();
}

View File

@@ -0,0 +1,119 @@
package com.replaymod.render.mixin;
import com.replaymod.render.hooks.ForceChunkLoadingHook;
import com.replaymod.render.hooks.IForceChunkLoading;
import com.replaymod.render.utils.FlawlessFrames;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.Camera;
import net.minecraft.client.render.Frustum;
import net.minecraft.client.render.GameRenderer;
import net.minecraft.client.render.LightmapTextureManager;
import net.minecraft.client.render.WorldRenderer;
import net.minecraft.client.render.chunk.ChunkBuilder;
import net.minecraft.client.render.chunk.ChunkRendererRegionBuilder;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.math.Matrix4f;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
@Mixin(WorldRenderer.class)
public abstract class Mixin_ForceChunkLoading implements IForceChunkLoading {
private ForceChunkLoadingHook replayModRender_hook;
@Override
public void replayModRender_setHook(ForceChunkLoadingHook hook) {
this.replayModRender_hook = hook;
}
@Shadow private ChunkBuilder chunkBuilder;
@Shadow protected abstract void setupTerrain(Camera par1, Frustum par2, boolean par3, boolean par4);
@Shadow private Frustum frustum;
@Shadow private Frustum capturedFrustum;
@Shadow @Final private MinecraftClient client;
@Shadow @Final private ObjectArrayList<ChunkInfoAccessor> chunkInfos;
@Shadow private boolean field_34810;
@Shadow @Final private BlockingQueue<ChunkBuilder.BuiltChunk> builtChunks;
@Shadow private Future<?> field_34808;
@Shadow @Final private AtomicBoolean field_34809;
@Shadow protected abstract void applyFrustum(Frustum par1);
@Inject(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/WorldRenderer;setupTerrain(Lnet/minecraft/client/render/Camera;Lnet/minecraft/client/render/Frustum;ZZ)V"))
private void forceAllChunks(MatrixStack matrices, float tickDelta, long limitTime, boolean renderBlockOutline, Camera camera, GameRenderer gameRenderer, LightmapTextureManager lightmapTextureManager, Matrix4f matrix4f, CallbackInfo ci) {
if (replayModRender_hook == null) {
return;
}
if (FlawlessFrames.hasSodium()) {
return;
}
assert this.client.player != null;
ChunkRendererRegionBuilder chunkRendererRegionBuilder = new ChunkRendererRegionBuilder();
do {
// Determine which chunks shall be visible
setupTerrain(camera, this.frustum, this.capturedFrustum != null, this.client.player.isSpectator());
// Wait for async processing to be complete
if (this.field_34808 != null) {
try {
this.field_34808.get(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (TimeoutException e) {
e.printStackTrace();
}
}
// If that async processing did change the chunk graph, we need to re-apply the frustum (otherwise this is
// only done in the next setupTerrain call, which not happen this frame)
if (this.field_34809.compareAndSet(true, false)) {
this.applyFrustum((new Frustum(frustum)).method_38557(8)); // call based on the one in setupTerrain
}
// Schedule all chunks which need rebuilding (we schedule even important rebuilds because we wait for
// all of them anyway and this way we can take advantage of threading)
for (ChunkInfoAccessor chunkInfo : this.chunkInfos) {
ChunkBuilder.BuiltChunk builtChunk = chunkInfo.getChunk();
if (!builtChunk.needsRebuild()) {
continue;
}
// MC sometimes schedules invalid chunks when you're outside of loaded chunks (e.g. y > 256)
if (builtChunk.shouldBuild()) {
builtChunk.scheduleRebuild(this.chunkBuilder, chunkRendererRegionBuilder);
}
builtChunk.cancelRebuild();
}
// Upload all chunks
this.field_34810 |= ((ForceChunkLoadingHook.IBlockOnChunkRebuilds) this.chunkBuilder).uploadEverythingBlocking();
// Repeat until no more updates are needed
} while (this.field_34810 || !this.builtChunks.isEmpty());
}
}

View File

@@ -0,0 +1,38 @@
package com.replaymod.replay.mixin;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.entity.Entity;
import net.minecraft.util.math.Vec3d;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
@Mixin(ClientPlayNetworkHandler.class)
public class Mixin_FixEntityNotTracking {
@ModifyVariable(method = { "onEntityPosition", "onEntity", "onEntityPassengersSet" }, at = @At("RETURN"), ordinal = 0)
private Entity updatePositionIfNotTracked$0(Entity entity) {
if (entity != null) {
entity.streamSelfAndPassengers().forEach(this::updatePositionIfNotTracked);
}
return entity;
}
private void updatePositionIfNotTracked(Entity entity) {
if (entity != null && entity.world instanceof ClientWorldAccessor world) {
if (!world.getEntityList().has(entity)) {
// Skip interpolation of position updates coming from server
// (See: newX in EntityLivingBase or otherPlayerMPX in EntityOtherPlayerMP)
int ticks = 0;
Vec3d prevPos;
do {
prevPos = entity.getPos();
if (entity.hasVehicle()) {
entity.tickRiding();
} else {
entity.tick();
}
} while (prevPos.squaredDistanceTo(entity.getPos()) > 0.0001 && ticks++ < 100);
}
}
}
}

View File

@@ -1,5 +1,6 @@
net.minecraft.stats.StatisticsManager net.minecraft.stats.StatFileWriter
net.minecraft.init.MobEffects net.minecraft.potion.Potion
net.minecraft.util.math.Vec3d net.minecraft.util.Vec3
net.minecraft.util.text.TextComponentString net.minecraft.util.ChatComponentText
net.minecraft.util.text.TextComponentTranslation net.minecraft.util.ChatComponentTranslation
net.minecraft.util.text.Style net.minecraft.util.ChatStyle
@@ -7,6 +8,7 @@ net.minecraft.util.text.TextFormatting net.minecraft.util.EnumChatFormatting
net.minecraft.util.text.ITextComponent net.minecraft.util.IChatComponent
net.minecraft.network.datasync.EntityDataManager net.minecraft.entity.DataWatcher
net.minecraft.network.datasync.EntityDataManager.DataEntry net.minecraft.entity.DataWatcher.WatchableObject
net.minecraft.client.renderer.ItemRenderer rotateArm() rotateWithPlayerRotations()
net.minecraft.client.renderer.VertexBuffer net.minecraft.client.renderer.WorldRenderer
net.minecraft.client.particle.Particle net.minecraft.client.particle.EntityFX
net.minecraft.util.math.MathHelper net.minecraft.util.MathHelper

View File

@@ -0,0 +1 @@
net.minecraft.client.network.ClientPlayerEntity getUnderwaterVisibility() method_3140()

View File

@@ -0,0 +1,16 @@
net.minecraft.client.util.math.MatrixStack multiplyPositionMatrix() method_34425()
net.minecraft.network.packet.s2c.play.GameJoinS2CPacket playerEntityId() getEntityId()
net.minecraft.network.packet.s2c.play.GameJoinS2CPacket hardcore() isHardcore()
net.minecraft.network.packet.s2c.play.GameJoinS2CPacket gameMode() getGameMode()
net.minecraft.network.packet.s2c.play.GameJoinS2CPacket previousGameMode() getPreviousGameMode()
net.minecraft.network.packet.s2c.play.GameJoinS2CPacket dimensionIds() getDimensionIds()
net.minecraft.network.packet.s2c.play.GameJoinS2CPacket registryManager() getRegistryManager()
net.minecraft.network.packet.s2c.play.GameJoinS2CPacket dimensionType() getDimensionType()
net.minecraft.network.packet.s2c.play.GameJoinS2CPacket dimensionId() getDimensionId()
net.minecraft.network.packet.s2c.play.GameJoinS2CPacket sha256Seed() getSha256Seed()
net.minecraft.network.packet.s2c.play.GameJoinS2CPacket maxPlayers() getMaxPlayers()
net.minecraft.network.packet.s2c.play.GameJoinS2CPacket viewDistance() getViewDistance()
net.minecraft.network.packet.s2c.play.GameJoinS2CPacket reducedDebugInfo() hasReducedDebugInfo()
net.minecraft.network.packet.s2c.play.GameJoinS2CPacket showDeathScreen() showsDeathScreen()
net.minecraft.network.packet.s2c.play.GameJoinS2CPacket debugWorld() isDebugWorld()
net.minecraft.network.packet.s2c.play.GameJoinS2CPacket flatWorld() isFlatWorld()