Release 2.6.2
This commit is contained in:
BIN
.gitignore
vendored
BIN
.gitignore
vendored
Binary file not shown.
28
build.gradle
28
build.gradle
@@ -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'
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
2
jGui
Submodule jGui updated: 31bcfabe67...c79b62a73e
@@ -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"))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
202
src/main/java/com/replaymod/core/files/DelegatingReplayFile.java
Normal file
202
src/main/java/com/replaymod/core/files/DelegatingReplayFile.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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 = () -> {};
|
||||
}
|
||||
}
|
||||
192
src/main/java/com/replaymod/core/files/ReplayFilesService.java
Normal file
192
src/main/java/com/replaymod/core/files/ReplayFilesService.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 + ")"));
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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"), () -> {});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
// 1.18+
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
9
src/main/java/com/replaymod/replay/ext/EntityExt.java
Normal file
9
src/main/java/com/replaymod/replay/ext/EntityExt.java
Normal 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);
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
// 1.18+ only
|
||||
@@ -0,0 +1 @@
|
||||
// 1.12.2 and below
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
"mixins": [],
|
||||
"server": [],
|
||||
"client": [
|
||||
//#if MC>=11800
|
||||
//$$ "ChunkInfoAccessor",
|
||||
//#endif
|
||||
"Mixin_ChromaKeyColorSky",
|
||||
"Mixin_ChromaKeyDisableFog",
|
||||
"Mixin_ChromaKeyForceSky",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.6.1
|
||||
2.6.2
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
0
versions/1.18.1/.gitkeep
Normal 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();
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
1
versions/mapping-fabric-1.16.1-1.15.2.txt
Normal file
1
versions/mapping-fabric-1.16.1-1.15.2.txt
Normal file
@@ -0,0 +1 @@
|
||||
net.minecraft.client.network.ClientPlayerEntity getUnderwaterVisibility() method_3140()
|
||||
16
versions/mapping-fabric-1.18.1-1.17.1.txt
Normal file
16
versions/mapping-fabric-1.18.1-1.17.1.txt
Normal 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()
|
||||
Reference in New Issue
Block a user