Compare commits

..

1 Commits

Author SHA1 Message Date
Jonas Herzig
1283a39ffa Backport to 1.7.10 2017-09-14 16:06:49 +02:00
649 changed files with 11395 additions and 32987 deletions

20
.drone.yml Normal file
View File

@@ -0,0 +1,20 @@
pipeline:
clone:
image: plugins/git
tags: true
build:
image: maven:3.3.9-jdk-8
commands:
- curl --fail --create-dirs -o $HOME/.gradle/init.d/mirror.gradle https://maven.johni0702.de/config/gradle
- curl --fail --create-dirs -o $HOME/.m2/settings.xml https://maven.johni0702.de/config/maven
- ./gradlew setupCIWorkspace
- ./gradlew :build
archive:
image: plugins/s3
acl: public-read
region: "us-east-1"
bucket: "replaymod"
path_style: true
source: build/libs/*
strip_prefix: build/libs/
target: /

1
.drone.yml.sig Normal file
View File

@@ -0,0 +1 @@
eyJhbGciOiJIUzI1NiJ9.cGlwZWxpbmU6CiAgY2xvbmU6CiAgICBpbWFnZTogcGx1Z2lucy9naXQKICAgIHRhZ3M6IHRydWUKICBidWlsZDoKICAgIGltYWdlOiBtYXZlbjozLjMuOS1qZGstOAogICAgY29tbWFuZHM6CiAgICAgIC0gY3VybCAtLWZhaWwgLS1jcmVhdGUtZGlycyAtbyAkSE9NRS8uZ3JhZGxlL2luaXQuZC9taXJyb3IuZ3JhZGxlIGh0dHBzOi8vbWF2ZW4uam9obmkwNzAyLmRlL2NvbmZpZy9ncmFkbGUKICAgICAgLSBjdXJsIC0tZmFpbCAtLWNyZWF0ZS1kaXJzIC1vICRIT01FLy5tMi9zZXR0aW5ncy54bWwgaHR0cHM6Ly9tYXZlbi5qb2huaTA3MDIuZGUvY29uZmlnL21hdmVuCiAgICAgIC0gLi9ncmFkbGV3IHNldHVwQ0lXb3Jrc3BhY2UKICAgICAgLSAuL2dyYWRsZXcgOmJ1aWxkCiAgYXJjaGl2ZToKICAgIGltYWdlOiBwbHVnaW5zL3MzCiAgICBhY2w6IHB1YmxpYy1yZWFkCiAgICByZWdpb246ICJ1cy1lYXN0LTEiCiAgICBidWNrZXQ6ICJyZXBsYXltb2QiCiAgICBwYXRoX3N0eWxlOiB0cnVlCiAgICBzb3VyY2U6IGJ1aWxkL2xpYnMvKgogICAgc3RyaXBfcHJlZml4OiBidWlsZC9saWJzLwogICAgdGFyZ2V0OiAvCg.riE6lhwfdNUhI7LO2SxEKFOjzjaX40ne7Ae9gyP75KE

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
* text=auto

BIN
.gitignore vendored

Binary file not shown.

3
.gitmodules vendored
View File

@@ -5,6 +5,3 @@
[submodule "src/main/resources/assets/replaymod/lang"]
path = src/main/resources/assets/replaymod/lang
url = https://github.com/ReplayMod/Translations
[submodule "ReplayStudio"]
path = libs/ReplayStudio
url = ../ReplayStudio

30
Jenkinsfile vendored
View File

@@ -1,30 +0,0 @@
pipeline {
agent {
docker { image 'openjdk:8-jdk' }
}
stages {
stage('Build') {
environment {
GRADLE_USER_HOME = '.gradle/user_home'
}
steps {
cache(maxCacheSize: 4096, caches: [
[$class: 'ArbitraryFileCache', excludes: 'modules-2/modules-2.lock,*/plugin-resolution/**', includes: '**/*', path: '.gradle/user_home/caches'],
[$class: 'ArbitraryFileCache', excludes: '', includes: '**/*', path: '.gradle/user_home/wrapper'],
[$class: 'ArbitraryFileCache', excludes: '', includes: '**/*', path: '.gradle/loom-cache'],
]) {
// sh './gradlew :jGui:1.7.10:setupCIWorkspace :1.7.10:setupCIWorkspace'
sh './gradlew --parallel'
}
archiveArtifacts 'versions/*/build/libs/*.jar'
}
}
stage('Deploy') {
steps {
withAWS(endpointUrl: 'https://minio.johni0702.de', credentials: 'minio') {
s3Upload bucket: 'replaymod', includePathPattern: '*.jar', workingDir: 'build/libs', acl: 'PublicRead', pathStyleAccessEnabled: true
}
}
}
}
}

View File

@@ -4,38 +4,46 @@ A Minecraft mod to record game sessions and replay them afterwards from any pers
## Building
Make sure your sub-projects are up-to-date: `git submodule update --init --recursive`
For compiling 1.7.10, you must run `./gradlew :jGui:1.7.10:setupDecompWorkspace :1.7.10:setupDecompWorkspace` once after the initial clone. This may take quite some time.
For each branch you visit the first time, running `./gradlew setupDecompWorkspace` is necessary.
This will also be necessary if the `replaymod_at.cfg` file has been changed (getting errors about code that is trying
to access private fields is a good indication that this has happened).
### No IDE
You can build the mod by running `./gradlew build` (or just `./gradlew bundleJar`). You can then find the final jar files in `versions/$MCVERSION/build/libs/`.
You can also build single versions by running `./gradlew :1.8:build` (or just `./gradlew :1.8:bundleJar`) (builds the MC 1.8 version).
You can build the mod by running `./gradlew :build`. You can then find the final jar files in `build/libs/`.
### IntelliJ
Ensure you have at least IDEA 2020.1.
Build the mod via Gradle as explained above at least once (`./gradlew compileJava` should be sufficient). This will ensure that the sources for all MC versions are generated.
Then import the Gradle project from within IDEA: File -> Open -> build.gradle -> Open as Project
Finally configure IDEA to build everything by itself instead of delegating it to Gradle (cause that is slow): File -> Settings -> Build, Execution, Deployment -> Build Tools -> Gradle -> Build and run using: IntelliJ IDEA
For the initial setup run `./gradlew preshadowJar idea genIntellijRuns`.
You also need to enable the Mixin annotation processor:
1. Go to File -> Settings -> Build, Execution, Deployment -> Compiler -> Annotation Processors
2. Tick "Enable annotation processing"
3. Add a new entry to the "Annotation Processor options"
4. Set the name to "reobfSrgFile" and the value to "$path/build/mcp-srg.srg" where you replace $path with the full
path to the folder containing the gradlew file
Whenever you switch to another branch, you can either just run `./gradlew preshadowJar idea` or instead run
`./gradlew preshadowJar copySrg` and then refresh the gradle project from within IntelliJ.
### Eclipse
## Development
### Branches
Loosely based on [this branching model](http://nvie.com/posts/a-successful-git-branching-model/) with `stable` instead of `master`.
New features are developed on the `1.8` branch and merged upwards for release. Larger features are developed on their
own branch that is based on `1.8` and then merged back once it's finished.
TL;DR:
Main development happens on the `develop` branch, snapshots are built from this branch.
The `stable` branch contains the most recent release.
Features or bug fixes that apply only to a specific Minecraft version are fixed on the branch corresponding to that
version and reverted when merging upwards.
Both subprojects follow a similar branching model.
The user documentation (`docs` folder) is only committed to on the current development branch. Any changes committed on
a branch for a more recent Minecraft version are not kept up to date on the website and should only be
of importance once support for the previous Minecraft version is dropped.
The `master` branch is solely to be used for the `version.json` file that contains a list of all versions
used by the clients to check for updates of this mod.
### The Preprocessor
To support multiple Minecraft versions with the ReplayMod, a [JCP](https://github.com/raydac/java-comment-preprocessor)-inspired preprocessor is used.
It has by now acquired a lot more sophisticated features to make it as noninvasive as possible.
Please read the [preprocessor's README](https://github.com/ReplayMod/preprocessor/blob/master/README.md) to understand how it works.
### Versioning
The ReplayMod uses the versioning scheme outlined [here](https://docs.minecraftforge.net/en/1.12.x/conventions/versioning/)
The ReplayMod uses the versioning scheme outlined [here](http://mcforge.readthedocs.io/en/latest/conventions/versioning/)
with three changes:
- No `MAJORAPI`, the ReplayMod does not provide any external API
- "Updating to a new Minecraft version" should not increment `MAJORMOD`, we maintain one version of the ReplayMod
@@ -44,18 +52,19 @@ keep the version name the same for all of them (with the exception of `MCVERSION
"Multiple Minecraft Version" section does not apply.
- For pre-releases the shorter `-bX` is used instead of `-betaX`
When a new version is (pre-)release, a new commit modifying the `version.txt` file should be added and the
When a new version is (pre-)release, a new annotated tag should be added with the name of the version and the
`versions.json` file in the `master` branch should be updated. To simplify this process the gradle task `doRelease` can
be used: `./gradlew -PreleaseVersion=2.0.0-rc1 doRelease`. It will create the commit and update the version.json
be used: `./gradlew -PreleaseVersion=1.8-2.0.0-rc1 doRelease`. It will create the tag and update the version.json
accordingly.
Care should be taken that the updated `version.json` is not pushed before a jar file is available on the
download page (or Jenkins) as it will inform the users of the update.
### Bugs
GitHub should generally be used to report bugs.
Bugs in the mod are tracked via [Bugzilla](https://bugs.replaymod.com/).
GitHub should only be used for issues that are generally not likely to affect any end users.
In the past, bugs were tracked via [Bugzilla](https://bugs.replaymod.com/), so bug numbers in commits prior to 2020 such as `(fixes #42)` generally referred to Bugzilla unless noted otherwise.
Bug numbers in commits such as `(fixes #42)` refer to Bugzilla unless noted otherwise.
## License
The ReplayMod is provided under the terms of the GNU General Public License Version 3 or (at your option) any later version.

310
build.gradle Executable file
View File

@@ -0,0 +1,310 @@
import groovy.json.JsonOutput
buildscript {
repositories {
mavenCentral()
maven {
name = "forge"
url = "http://files.minecraftforge.net/maven"
}
maven {
name = "sonatype"
url = "https://oss.sonatype.org/content/repositories/snapshots/"
}
}
dependencies {
classpath 'net.minecraftforge.gradle:ForgeGradle:1.2-SNAPSHOT'
}
}
apply plugin: 'forge'
sourceCompatibility = 1.8
targetCompatibility = 1.8
version = gitDescribe()
group= "com.replaymod"
archivesBaseName = "replaymod"
ext {
mixinConfigs = [
'compat.shaders', 'extras.playeroverview', 'recording', 'render', 'replay'
].collect {"mixins.${it}.replaymod.json"}
mixinSrg = new File(project.buildDir, 'tmp/mixins/mixins.srg')
mixinRefMap = new File(project.buildDir, 'tmp/mixins/mixins.replaymod.refmap.json')
}
minecraft {
version = '1.7.10-10.13.4.1558-1.7.10'
runDir = "eclipse"
mappings = "stable_12"
replace '@MOD_VERSION@', project.version
// Includes intentional whitespace to stop Forge from declaring the mod to be compatible with
// a newer srg-compatible MC version (that may be using a different protocol version)
replace '@MC_VERSION@', "[ ${project.minecraft.version} ]"
}
repositories {
maven {
name = "SpongePowered Repo"
url = "http://repo.spongepowered.org/maven/"
}
maven {
url 'https://jitpack.io'
}
}
configurations {
shade
compile.extendsFrom shade
}
dependencies {
compile 'org.projectlombok:lombok:1.16.4'
compile 'org.spongepowered:mixin:0.6.8-SNAPSHOT'
shade 'com.googlecode.mp4parser:isoparser:1.1.7'
shade 'org.apache.commons:commons-exec:1.3'
shade 'com.google.apis:google-api-services-youtube:v3-rev178-1.22.0'
shade 'com.google.api-client:google-api-client-gson:1.20.0'
shade 'com.google.api-client:google-api-client-java6:1.20.0'
shade 'com.google.oauth-client:google-oauth-client-jetty:1.20.0'
shade 'org.aspectj:aspectjrt:1.8.2'
shade 'com.github.replaymod:ReplayStudio:181140c'
testCompile 'junit:junit:4.11'
}
jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
dependsOn configurations.compile
dependsOn configurations.shade
from project.mixinRefMap
def shade = {files(
configurations.compile.findAll {it.name.startsWith 'mixin-'}
+ configurations.shade
)}
def noticeDir = file("$buildDir/NOTICE")
doFirst {
noticeDir.deleteDir()
noticeDir.mkdirs()
shade().collect { it.isDirectory() ? fileTree(it) : zipTree(it) }.each {
it.matching { include '**/NOTICE*' }.each {
new File(noticeDir, 'NOTICE.txt') << it.getText('UTF-8') + '\n'
}
}
}
from noticeDir
from ({shade().collect { it.isDirectory() ? it : zipTree(it) }}) {
exclude '**/NOTICE*'
// exclude everything taken in from jGui for running the mod in a dev environment
exclude { new File(file('jGui/src/main/resources'), it.relativePath.pathString).exists() }
eachFile {
if (getName() == 'LICENSE.txt') {
setName(getFile().getParentFile().getName().split('-')[0] + '-LICENSE.txt')
}
}
}
manifest {
attributes 'TweakClass': 'org.spongepowered.asm.launch.MixinTweaker',
'TweakOrder': '0',
'MixinConfigs': project.mixinConfigs.join(','),
'FMLCorePlugin': 'com.replaymod.core.LoadingPlugin',
'FMLAT': 'replaymod_at.cfg'
}
}
def gitDescribe() {
try {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'describe', '--always', '--dirty=*'
standardOutput = stdout
}
return stdout.toString().trim()
} catch (e) {
return "unknown"
}
}
processResources
{
// this will ensure that this task is redone when the versions change.
inputs.property "version", project.version
inputs.property "mcversion", project.minecraft.version
// replace stuff in mcmod.info, nothing else
from(sourceSets.main.resources.srcDirs) {
include 'mcmod.info'
// replace version and mcversion
expand 'version': project.version, 'mcversion': project.minecraft.version
}
// copy everything else, thats not the mcmod.info
from(sourceSets.main.resources.srcDirs) {
exclude 'mcmod.info'
}
}
sourceSets {
main {
// Required so resources are loaded correctly in dev env
output.resourcesDir = output.classesDir
// Directly include jGui into main build
// This could also be done by including it as a gradle subproject, however
// doing so caused classpath issues on some IntelliJ versions.
java {
srcDir 'jGui/src/main/java'
}
resources {
srcDir 'jGui/src/main/resources'
}
}
integrationTest {
compileClasspath += main.runtimeClasspath + main.output
java {
srcDir file('src/integration-test/java')
}
resources.srcDir file('src/integration-test/resources')
}
}
task copySrg(type: Copy, dependsOn: 'genSrgs') {
from {project.tasks.genSrgs.mcpToSrg}
into 'build'
}
reobf.doFirst {
if (project.mixinSrg.exists()) {
addExtraSrgFile project.mixinSrg
}
}
compileJava.dependsOn copySrg
compileJava {
options.compilerArgs += [
"-AoutSrgFile=${project.mixinSrg.canonicalPath}",
"-AoutRefMapFile=${project.mixinRefMap.canonicalPath}",
"-AreobfSrgFile=${project.file('build/mcp-srg.srg').canonicalPath}"
]
}
setupDecompWorkspace.dependsOn copySrg
setupDevWorkspace.dependsOn copySrg
project.tasks.idea.dependsOn copySrg
task runIntegrationTest(type: JavaExec, dependsOn: ["makeStart", "jar"]) {
main = 'GradleStart'
standardOutput = System.out
errorOutput = System.err
workingDir file(minecraft.runDir)
def testDir = new File(minecraft.runDir, "integration-test")
doFirst {
testDir.deleteDir()
testDir.mkdirs()
}
doLast {
testDir.deleteDir()
}
afterEvaluate {
def runClient = tasks.getByName("runClient")
runIntegrationTest.jvmArgs = runClient.jvmArgs + "-Dfml.noGrab=true"
runIntegrationTest.args = runClient.args + "--gameDir" + testDir.canonicalPath
runIntegrationTest.classpath runClient.classpath + sourceSets.integrationTest.output
}
}
def generateVersionsJson() {
// List all tags
def stdout = new ByteArrayOutputStream()
project.exec {
commandLine 'git', 'for-each-ref', '--sort=taggerdate', '--format=%(refname:short)', 'refs/tags'
standardOutput = stdout
}
def versions = stdout.toString().tokenize('\n').reverse()
def mcVersions = versions.collect {it.substring(0, it.indexOf('-'))}.unique().reverse()
def root = [
homepage: 'https://www.replaymod.com/download/',
promos: [:]
]
mcVersions.forEach { mcVersion ->
def mcVersionRoot = [:]
def latest
def recommended
versions.forEach {
def (thisMcVersion, modVersion, preVersion) = it.tokenize('-')
if (thisMcVersion == mcVersion) {
mcVersionRoot[it] = "See https://github.com/ReplayMod/ReplayMod/releases/$it"
if (latest == null) latest = it
if (preVersion == null) {
if (recommended == null) recommended = it
}
}
}
root[mcVersion] = mcVersionRoot
root.promos[mcVersion + '-latest'] = latest
if (recommended != null) {
root.promos[mcVersion + '-recommended'] = recommended
}
}
root
}
task doRelease() {
doLast {
// Parse version
def version = project.releaseVersion as String
if (project.version.endsWith('*')) {
throw new InvalidUserDataException('Git working tree is dirty. Make sure to commit all changes.')
}
def (mcVersion, modVersion, preVersion) = version.tokenize('-')
preVersion = preVersion != null && preVersion.startsWith('b') ? preVersion.substring(1) : null
// Create new tag
if (preVersion != null) {
project.exec {
commandLine 'git', 'tag',
'-m', "Pre-release $preVersion of $modVersion for Minecraft $mcVersion",
"$mcVersion-$modVersion-b$preVersion"
}
} else {
project.exec {
commandLine 'git', 'tag',
'-m', "Release $modVersion for Minecraft $mcVersion",
"$mcVersion-$modVersion"
}
}
// Switch to master branch to update versions.json
project.exec { commandLine 'git', 'checkout', 'master' }
// Generate versions.json content
def versionsRoot = generateVersionsJson()
def versionsJson = JsonOutput.prettyPrint(JsonOutput.toJson(versionsRoot))
// Write versions.json
new File('versions.json').write(versionsJson)
// Commit changes
project.exec { commandLine 'git', 'add', 'versions.json' }
project.exec { commandLine 'git', 'commit', '-m', "Update versions.json for $version" }
// Return to previous branch
project.exec { commandLine 'git', 'checkout', '-' }
}
}
defaultTasks 'build'

View File

@@ -1,377 +0,0 @@
import com.replaymod.gradle.preprocess.PreprocessTask
import gg.essential.gradle.util.*
import net.fabricmc.loom.task.RemapJarTask
plugins {
java
id("com.gradleup.shadow") apply false
id("gg.essential.multi-version")
id("gg.essential.defaults.repo")
id("gg.essential.defaults.java")
id("gg.essential.defaults.loom")
}
val mcVersion = platform.mcVersion
var jGuiVersion = project.name
if (jGuiVersion in listOf("1.10.2", "1.11", "1.11.2")) jGuiVersion = "1.9.4"
if (jGuiVersion in listOf("1.12.1", "1.12.2")) jGuiVersion = "1.12"
val jGui = project.evaluationDependsOn(":jGui:$jGuiVersion")
version = "${project.name}-${rootProject.version}"
base.archivesName.set("replaymod")
java.withSourcesJar()
loom {
noServerRunConfigs()
}
if (!platform.isUnobfuscated) {
loom.mixin.useLegacyMixinAp = true
loom.mixin.defaultRefmapName.set("mixins.replaymod.refmap.json")
}
if (platform.isLegacyForge) {
loom.runs.named("client") {
property("fml.coreMods.load", "com.replaymod.core.LoadingPlugin")
}
}
repositories {
mavenLocal()
maven("https://repo.spongepowered.org/maven/")
maven("https://maven.terraformersmc.com/releases/") {
content {
includeGroup("com.terraformersmc")
}
}
maven("https://jitpack.io") {
content {
includeGroupByRegex("com\\.github\\..*")
}
}
exclusiveContent {
forRepository {
maven("https://api.modrinth.com/maven")
}
filter {
includeGroup("maven.modrinth")
}
}
}
// Include dep in fat jar without relocation and, when forge supports it, without exploding (TODO)
val shade by configurations.creating
// Include dep in fat jar with relocation and minimization
val shadow by configurations.creating {
exclude(group = "net.fabricmc", module = "fabric-loader")
exclude(group = "com.google.guava", module = "guava-jdk5")
exclude(group = "com.google.guava", module = "guava") // provided by MC
exclude(group = "com.google.code.gson", module = "gson") // provided by MC (or manually bundled for 1.11.2 and below)
}
dependencies {
if (platform.isFabric) {
val fabricApiVersion = when (mcVersion) {
11404 -> "0.4.3+build.247-1.14"
11502 -> "0.5.1+build.294-1.15"
11601 -> "0.14.0+build.371-1.16"
11603 -> "0.17.1+build.394-1.16"
11604 -> "0.42.0+1.16"
11701 -> "0.46.1+1.17"
11800 -> "0.43.1+1.18"
11801 -> "0.43.1+1.18"
11802 -> "0.47.9+1.18.2"
11900 -> "0.55.3+1.19"
11901 -> "0.58.5+1.19.1"
11902 -> "0.68.0+1.19.2"
11903 -> "0.68.1+1.19.3"
11904 -> "0.76.0+1.19.4"
12001 -> "0.83.1+1.20.1"
12002 -> "0.91.2+1.20.2"
12004 -> "0.91.2+1.20.4"
12006 -> "0.98.0+1.20.6"
12100 -> "0.100.3+1.21"
12102 -> "0.106.1+1.21.2"
12104 -> "0.111.0+1.21.4"
12105 -> "0.119.9+1.21.5"
12107 -> "0.128.1+1.21.7"
12110 -> "0.135.0+1.21.10"
12111 -> "0.139.5+1.21.11"
26_01_00 -> "0.144.3+26.1"
else -> throw UnsupportedOperationException()
}
val fabricApiModules = mutableListOf(
"api-base",
"networking-v0",
"keybindings-v0",
"resource-loader-v0",
)
if (mcVersion >= 11600) {
fabricApiModules.remove("keybindings-v0")
fabricApiModules.add("key-binding-api-v1")
}
if (mcVersion >= 11604) {
fabricApiModules.add("screen-api-v1")
fabricApiModules.add("networking-api-v1")
}
if (mcVersion >= 11700) {
fabricApiModules.remove("networking-v0")
}
if (mcVersion >= 12109) {
fabricApiModules.add("resource-loader-v1")
}
if (mcVersion >= 26_01_00) {
fabricApiModules.remove("key-binding-api-v1")
fabricApiModules.add("key-mapping-api-v1")
}
for (module in fabricApiModules) {
val dep = fabricApi.module("fabric-$module", fabricApiVersion)
modImplementation(dep)
"include"(dep)
}
}
if (!platform.isFabric) {
// Mixin 0.8 is no longer compatible with MC 1.11.2 or older
val mixinVersion = if (mcVersion >= 11200) "0.8.2" else "0.7.11-SNAPSHOT"
compileOnly("org.spongepowered:mixin:$mixinVersion")
implementation(shade("org.spongepowered:mixin:$mixinVersion") {
isTransitive = false // deps should all be bundled with MC
})
}
if (platform.isFabric) {
"include"(implementation(annotationProcessor("io.github.llamalad7:mixinextras-fabric:0.3.6")!!)!!)
}
implementation(shadow("com.googlecode.mp4parser:isoparser:1.1.7")!!)
implementation(shadow("org.apache.commons:commons-exec:1.3")!!)
implementation(shadow("com.google.apis:google-api-services-youtube:v3-rev178-1.22.0")!!)
implementation(shadow("com.google.api-client:google-api-client-gson:1.20.0")!!)
implementation(shadow("com.google.api-client:google-api-client-java6:1.20.0")!!)
implementation(shadow("com.google.oauth-client:google-oauth-client-jetty:1.20.0")!!)
val lwjgl by configurations.creating
for (suffix in listOf("", ":natives-linux", ":natives-windows", ":natives-macos", ":natives-macos-arm64")) {
lwjgl("org.lwjgl:lwjgl:3.3.1$suffix")
lwjgl("org.lwjgl:lwjgl-tinyexr:3.3.1$suffix")
}
compileOnly("org.lwjgl:lwjgl-tinyexr:3.3.1")
shadow(prebundle(lwjgl, "com/replaymod/render/utils/lwjgl.jar"))
if (mcVersion < 11200) {
// The version which MC ships is too old, we'll need to ship our own
implementation(shadow("com.google.code.gson:gson:2.8.7")!!)
}
implementation(shadow("com.github.javagl.JglTF:jgltf-model:3af6de4")!!)
if (platform.isFabric) {
implementation(shadow("org.apache.maven:maven-artifact:3.6.1")!!)
}
implementation(shadow("org.aspectj:aspectjrt:1.8.2")!!)
implementation(shadow("com.github.ReplayMod.JavaBlend:2.79.0:a0696f8")!!)
implementation(shadow("com.github.ReplayMod:ReplayStudio")!!)
// FIXME hack because I don't know how to get this to be inherited properly
implementation(rootProject.files("libs/ReplayStudio/.gradle/prebundled-jars/viaVersion.jar"))
implementation(project(path = jGui.path, configuration = if (platform.isUnobfuscated) null else "namedElements"))
implementation(shadow("com.github.ReplayMod:lwjgl-utils:27dcd66")!!)
if (platform.isFabric) {
val modMenuVersion = when {
mcVersion >= 26_01_00 -> "18.0.0-alpha.8"
mcVersion >= 12111 -> "17.0.0-alpha.1"
mcVersion >= 12110 -> "16.0.0-rc.1"
mcVersion >= 12107 -> "15.0.0-beta.3"
mcVersion >= 12105 -> "14.0.0-rc.2"
mcVersion >= 12104 -> "13.0.0-beta.1"
mcVersion >= 12102 -> "12.0.0-beta.1"
mcVersion >= 12100 -> "11.0.0-rc.4"
mcVersion >= 12006 -> "10.0.0-beta.1"
mcVersion >= 12003 -> "9.0.0-pre.1"
mcVersion >= 12002 -> "8.0.0"
mcVersion >= 12000 -> "7.0.1"
mcVersion >= 11904 -> "6.1.0-rc.4"
mcVersion >= 11903 -> "5.0.0-alpha.4"
mcVersion >= 11901 -> "4.0.5"
mcVersion >= 11900 -> "4.0.4"
mcVersion >= 11802 -> "3.1.0"
mcVersion >= 11800 -> "3.0.0"
mcVersion >= 11700 -> "2.0.0-beta.7"
mcVersion >= 11602 -> "1.16.8"
mcVersion >= 11600 -> null // maven doesn't have one for this version (only 1.16.5)
mcVersion >= 11500 -> "1.10.6"
else -> null
}
if (modMenuVersion != null) {
modCompileOnly("com.terraformersmc:modmenu:$modMenuVersion")
} else {
// Oldest modmenu on their maven is 1.10.5 for MC 1.15.2; for older versions we won't run it in dev
modCompileOnly("com.terraformersmc:modmenu:1.10.6")
}
// Lacks maven dependencies
if (mcVersion == 12006) {
//modRuntimeOnly("net.fabricmc.fabric-api:fabric-api:0.98.0+1.20.6")
}
}
val irisVersion = when {
mcVersion >= 26_01_00 -> "1.10.8+26.1-fabric"
mcVersion >= 12000 -> "1.7.2+1.20.1"
mcVersion >= 11600 -> "1.18.x-v1.2.0"
else -> null
}
if (irisVersion != null) {
modCompileOnly("maven.modrinth:iris:$irisVersion")
}
testImplementation("junit:junit:4.11")
}
preprocess {
keywords.set(mapOf(
".java" to PreprocessTask.DEFAULT_KEYWORDS,
".kt" to PreprocessTask.DEFAULT_KEYWORDS,
".json" to PreprocessTask.DEFAULT_KEYWORDS,
".mcmeta" to PreprocessTask.DEFAULT_KEYWORDS,
".cfg" to PreprocessTask.CFG_KEYWORDS,
".vert" to PreprocessTask.DEFAULT_KEYWORDS,
".frag" to PreprocessTask.DEFAULT_KEYWORDS,
))
patternAnnotation.set("com.replaymod.gradle.remap.Pattern")
}
tasks.jar {
archiveClassifier.set("raw")
if (!platform.isFabric) {
manifest {
attributes(
"TweakClass" to "com.replaymod.core.tweaker.ReplayModTweaker",
"TweakOrder" to "0",
"FMLCorePluginContainsFMLMod" to "true",
"FMLCorePlugin" to "com.replaymod.core.LoadingPlugin",
"FMLAT" to "replaymod_at.cfg",
)
}
}
}
if (!platform.isUnobfuscated) {
tasks.named<RemapJarTask>("remapJar") {
if (platform.isFabric) {
addNestedDependencies.set(true)
}
archiveClassifier.set("obf")
}
}
val configureRelocationOutput = project.layout.buildDirectory.file("configureRelocation")
val configureRelocation by tasks.registering {
dependsOn(tasks.jar)
dependsOn(shadow)
outputs.file(configureRelocationOutput)
doLast {
val pkgs = files(shadow).filter { it.exists() }.map {
val tree = if (it.isDirectory) fileTree(it) else zipTree(it)
val pkgs = mutableSetOf<String>()
tree.visit {
val file = this
if (!file.isDirectory && file.name.endsWith(".class") && file.path.contains("/")) {
val pkg = file.path.substring(0, file.path.lastIndexOf("/")) + "/"
if (pkg.startsWith("com/")) {
if (pkg.startsWith("com/google/")) {
if (!pkg.startsWith("com/google/common")) {
pkgs += pkg.substring(0, pkg.indexOf("/", "com/google/".length))
}
} else if (!pkg.startsWith("com/replaymod")) {
pkgs += pkg.substring(0, pkg.indexOf("/", 4))
}
} else if (pkg.startsWith("net/")) {
if (!pkg.startsWith("net/minecraft")
&& !pkg.startsWith("net/fabric")) {
pkgs += pkg.substring(0, pkg.indexOf("/", "net/".length))
}
} else if (pkg.startsWith("org/")) {
if (pkg.startsWith("org/apache/")) {
if (pkg.startsWith("org/apache/commons/")) {
if (!pkg.startsWith("org/apache/commons/io")) {
pkgs += pkg.substring(0, pkg.indexOf("/", "org/apache/commons/".length))
}
} else if (!pkg.startsWith("org/apache/logging")) {
pkgs += pkg.substring(0, pkg.indexOf("/", "org/apache/".length))
}
} else if (pkg.startsWith("org/lwjgl")) {
return@visit // either bundled with MC or uses natives which we can't relocate
} else if (!pkg.startsWith("org/spongepowered")) {
pkgs += pkg.substring(0, pkg.indexOf("/", 4))
}
} else if (pkg.startsWith("it/unimi/dsi/fastutil") && mcVersion >= 11400) {
return@visit // MC uses this as well
} else if (!pkg.startsWith("javax/")) {
// Note: we cannot just use top level packages as those will be too generic and we'll run
// into this long standing bug: https://github.com/johnrengelman/shadow/issues/232
val i = pkg.indexOf("/")
val i2 = pkg.indexOf("/", i + 1)
if (i2 > 0) {
pkgs += pkg.substring(0, i2)
}
}
}
}
pkgs
}.flatten().toSortedSet()
configureRelocationOutput.get().asFile.writeText(pkgs.joinToString("\n"))
}
}
val bundleJar by tasks.registering(com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar::class) {
from((if (platform.isUnobfuscated) tasks.jar else tasks.named<RemapJarTask>("remapJar")).flatMap { it.archiveFile }.map { zipTree(it) })
from((if (platform.isUnobfuscated) jGui.tasks.jar else jGui.tasks.named<RemapJarTask>("remapJar")).flatMap { it.archiveFile }.map { zipTree(it) }) {
filesMatching("mixins.jgui.json") {
filter { it.replace("de.johni0702", "com.replaymod.lib.de.johni0702") }
}
filesMatching("mixins.jgui.refmap.json") {
filter { it.replace("de/johni0702", "com/replaymod/lib/de/johni0702") }
}
}
relocate("de.johni0702", "com.replaymod.lib.de.johni0702")
manifest.from(tasks.jar.get().manifest)
from(shade.elements.map { it.map { zipTree(it) } })
configurations = listOf(shadow)
exclude("META-INF/INDEX.LIST", "META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA", "module-info.class")
dependsOn(configureRelocation)
inputs.file(configureRelocationOutput)
doFirst {
configureRelocationOutput.get().asFile.forEachLine { pkg ->
val pkgName = pkg.replace("/", ".")
relocate(pkgName, "com.replaymod.lib.$pkgName")
}
}
// No need to shadow netty, MC provides it
// (actually, pre-1.12 ships a netty which is too old, so we need to shade it there anyway)
if (mcVersion >= 11200) {
relocate("com.github.steveice10.netty", "io.netty")
exclude("com/github/steveice10/netty/**")
}
if (mcVersion >= 11400) {
// MC ships this
exclude("it/unimi/dsi/fastutil/**")
}
minimize {
exclude(dependency(".*spongepowered:mixin:.*"))
}
}
tasks.assemble { dependsOn(bundleJar) }

385
docs/content.md Executable file → Normal file
View File

@@ -1,14 +1,16 @@
# Installation [installing]
## Installing the Replay Mod [replaymod]
### Minecraft 1.14 and above
The **ReplayMod** for Minecraft 1.14 and above requires **Fabric** to be installed.
The **ReplayMod** requires **Minecraft Forge** to be installed.
You can find Fabric and the installation instructions [here](https://fabricmc.net/use/).
Depending on your **Replay Mod** version we recommend the following **Forge** versions:
After installing Fabric, simply put the downloaded ReplayMod .jar file into the `/mods` folder of your Minecraft directory.
- Replay Mod 2.0.0 or later for Minecraft 1.11: Forge [1.11-13.19.1.2189](https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.11.html)
- Replay Mod 2.0.0 or later for Minecraft 1.10.2: Forge [1.10.2-12.18.2.2099](https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.10.2.html)
- Replay Mod 2.0.0 or later for Minecraft 1.9.4: Forge [1.9.4-12.17.0.1976](https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.9.4.html)
- Replay Mod 2.0.0 or later for Minecraft 1.8: Forge [1.8-11.14.4.1563](https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.8.html)
- Replay Mod 1.0.8 or older for Minecraft 1.8: Forge [1.8-11.14.3.1450](http://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.8.html)
### Minecraft 1.12.2 and below
For Minecraft 1.12.2 and below it requires **Minecraft Forge** to be installed.
Other Forge Versions might, but don't necessarily work.
If you don't know how to install Forge, follow [this tutorial](https://www.youtube.com/watch?v=4i7-RystzC4).
@@ -17,44 +19,63 @@ After installing Forge, simply put the downloaded `ReplayMod.jar` file in the `/
> **Note:** The **Replay Mod** is entirely client side and can not be installed on the servers you play on.
## Installing FFmpeg [ffmpeg]
To render your creations with **Replay Mod** you will need to have FFmpeg installed.
To use the **Replay Mod**'s [Rendering Feature](#replaying-render), you need to have FFmpeg installed.
### Windows [windows]
Download the latest FFmpeg release from https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip.
Download the latest **FFmpeg Static Build** for your architecture from <http://ffmpeg.zeranoe.com/builds/>.
Then, extract the downloaded `.7z` file in the folder you want to install FFmpeg in.
Extract the downloaded .zip file into your `.minecraft` folder.
Next, download this **Batch Script** to quickly install FFmpeg: <https://replaymod.com/files/ffmpeg-path-installer.bat>
Historically the exact location inside the `.minecraft` folder was important, this is no longer the case.
It is sufficient for the `ffmpeg.exe` to be anywhere inside your `.minecraft` folder.
> **Note:** If your browser warns you about the download, dismiss the message. It's a simple Batch Script which won't damage your computer.
**Notes for alternative launchers**
- Twitch launcher by default installs Minecraft instances in C:\\Users\\*username*\\Twitch\\Minecraft\\Instances\\*instancename*\\
- GD Launcher by default installs Minecraft instances in C:\\Users\\*username*\\AppData\\Roaming\\gdlauncher_next\\instances\\*instancename*\\
- MultiMC by default installs Minecraft instances in C:\\Program Files (x86)\\MultiMC\\instances\\*Instancename*\\.minecraft\\
Put the downloaded script into the folder where you extracted FFmpeg and run itt by double-clicking it.
If you see a success message in the console, you've sucessfully installed **FFmpeg**!
For these launchers, make sure FFmpeg exists inside the corresponding folder.
Alternatively, starting with ReplayMod 2.0.0-b5, you can also extract the downloaded `.7z` file into a `ffmpeg` folder
(you have to create it, it doesn't exist by default) in your `.minecraft` folder. No need to run any **Batch Script**.
The FFmpeg executable should end up at `.minecraft/ffmpeg/bin/ffmpeg.exe`.
### Mac OSX [mac]
On OSX, you can install **FFmpeg** with **[Homebrew](http://brew.sh/)** using `brew install ffmpeg`.
Alternatively, you can download the latest static build from <https://ffmpeg.org/> and copy the FFmpeg executable to `/usr/local/bin`.
Alternatively, you can download the latest static build from <https://ffmpeg.org/> and copy the ffmpeg executable to `/usr/local/bin`.
### Linux [linux]
On Linux, you can install **FFmpeg** using your system's package manager, for example using `apt install ffmpeg`.
If in doubt, consult the documentation of your distribution.
## Settings [settings]
To access the **Replay Mod Settings** from the Main Menu click the **"Replay Viewer"** button and click the **Settings** button.
## Compatibility with other Mods [compatibility]
### General information [general]
In General, the Replay Mod _should_ be compatible with most Forge Mods.
While playing, you can click the 'Mods' button in the Pause screen to reach **Replay Mod Settings** if you use Minecraft 1.12.2 and below, or have the mod _Mod Menu_ installed.
### Shaders Mod [shaders]
[Karyonix' Shaders Mod](http://www.minecraftforum.net/forums/mapping-and-modding/minecraft-mods/1286604-shaders-mod-updated-by-karyonix)
is no longer compatible with Minecraft Forge starting with 1.9.4. As such it is not compatible with the Replay Mod either.
Please use [Optifine](https://optifine.net/) instead.
### Custom Main Menu [custom-main-menu]
The [Custom Main Menu](https://mods.curse.com/mc-mods/minecraft/226406-custom-main-menu) mod is often used in mod packs to customize their Main Menu with a button layout fitting the background image, links to their website / bug tracker and similar.
If you are familiar with it, the button ids for the Replay Mod are: **17890234** (text: `replaymod.gui.replayviewer`), **17890237** (text: `replaymod.gui.replayeditor`) and **17890236** (text: `replaymod.gui.replaycenter`)
Due to the nature of this Custom Main Menu mod, buttons added to the Main Menu by 3rd party mods like the **Replay Mod** will not show up by default.
Thus, to access the Replay Viewer/Editor/Center, you need to manually configure the position for those buttons.
To do so, you need to modify the Custom Main Menu configuration file.
Usually, you can find it at `.minecraft/config/CustomMainMenu/mainmenu.json` (if you're using a mod pack launcher, it normally has its own .minecraft folder).
For an explanation of this config file, have a look at Custom Main Menu's page.
You can find a list of already modified config files [here](https://gist.github.com/Johni0702/3f3fab81dbf7ada83d045d9fe8f345aa).
## Troubleshooting [troubleshooting]
If you need help installing the **Replay Mod** or **FFmpeg**, please read [this forum thread](https://www.replaymod.com/forum/thread/220)
by [bela333](https://www.replaymod.com/user/bela333) - it covers most of the problems that users encountered so far.
## Settings [settings]
To access the **Replay Mod Settings** from the Main Menu click the **"Mods"** button, select the **Replay Mod** from the list and
the click the **"Config"** button.
When in a Replay, you can either bind a hotkey to the **Replay Mod Settings** in Minecraft's Control settings
or use the hotkey GUI by clicking on the hamburger button in the lower left corner.
## Accounts [accounts]
In previous versions of ReplayMod we used accounts to deliver videos to the **Replay Center**. This has since been discontinued and with that, so have the accounts.
Instead of the forum, you can join our [Discord server](https://discord.gg/5GR7RSb) to get support and answers.
or use the hotkey GUI by clicking on the arrow button in the lower left corner.
# Recording [recording]
![](img/recording-indicator.jpg)
@@ -122,13 +143,15 @@ The minimum Speed value is **0.1 times** the normal Minecraft Tick Speed, and th
To the right of the Speed Slider, there is the **Replay Timeline**.
On the Replay Timeline, you see a yellow cursor indicating your current position in the Replay.
By clicking somewhere on this Timeline, you will travel in time towards the specified point in time.
Please note that it takes longer to do larger steps in time or to jump backwards in time (see also [Quick Mode](#replaying-quickmode)).
Please note that it takes longer to do larger steps in time or to jump backwards in time.
## Camera Paths [paths]
### Introduction [intro]
While in a Replay, you can create controlled **Camera Movements** using the Mods's **Keyframe System**.
Those Camera Paths can be rendered to a video later (see [Rendering](#replaying-render)).
Camera Paths are inspired by the [PixelCam Mod](http://www.minecraftforum.net/forums/mapping-and-modding/minecraft-mods/2327429-pixelcam-camera-studio-for-minecraft-1-8) which is based on the Camera Studio Mod.
The **Replay Mod Keyframe System** is similar to many Video Editing Software's Keyframe Systems.
If you're new to Keyframe Systems, try reading [this Wikipedia article](https://en.wikipedia.org/wiki/Key_frame).
@@ -147,8 +170,6 @@ If two Keyframes are 5 seconds apart, the Camera Path will take 5 seconds to int
You can drag Keyframes on the **Keyframe Timeline** by left-clicking them, holding the mouse button and moving your mouse.
> **Hint:** Made a mistake? You can use `Ctrl+Z` to undo your changes to the keyframe timeline. Changed you mind? Press `Ctrl+Y` (or `Ctrl+Shift+Z`) to redo them.
### Position Keyframes [place]
The basic components of a Camera Path are **Position Keyframes**.
A Position Keyframe stores a **Camera Position** (x, y, z, yaw, pitch, roll).
@@ -179,6 +200,9 @@ If you wish to disable smooth interpolation and want to make the Path follow str
You can easily toggle between **Linear** and **Cubic Interpolation** using the `O` key.
#### Path Preview [preview]
![](img/path-preview-icon.jpg)
While the **Path Preview** is enabled, this camera symbol is displayed in the lower right corner of the screen.
![](img/path-preview.jpg)
A normal **Path Preview**
@@ -254,6 +278,60 @@ You can change a Preset's name, select it from the list and click the **"Rename"
To load a Keyframe Preset, select a Preset from the list and click the **"Load"** button.
Using the **"Remove"** button, you can permanently delete a Keyframe Preset from the **Keyframe Repository**.
## Custom Objects [objects]
> Custom Objects are not available in the 2.0.0 versions. They are expected to return eventually.
[YouTube](_Xdpg828fbE)
### Introduction [introduction]
**Custom Objects** are mainly meant for **video creators**. They allow you to add any image into the Replay World and animate its Position, Opacity and much more.
Instead of using [Motion Tracking](https://en.wikipedia.org/wiki/Match_moving) to add text or pictures to a video, you can directly add these to the rendered file.
### Adding Assets [assets]
![](img/asset-manager.jpg)
The **Asset Manager** with an example Asset
In order to create **Custom Objects**, you first have to add **Assets** to your Replay File.
Therefore, open the **Asset Manager** using the `G` key.
In the **Asset Manager**, you can add **Image Files (.png, .jpg and more)**, so-called **Assets**, to the Replay.
When clicking the **"Add"** button, a File Chooser will show up. Select the image file you want to use.
You can give the Asset a custom name using the Text Input Field in the top right corner.
All of the Assets you added can be used by **Custom Objects**.
### Creating Custom Objects [creating]
![](img/object-manager.jpg)
The **Object Manager** with some **Animation Keyframes**
Using the `F` key, you can open the **Object Manager**. Initially there won't be any Objects in the **Object List**, but you can simply add an Object using the **"Add"** button.
After doing so, you can name the newly created **Custom Object** using the Text Field in the upper right corner.
To define which **Asset File** (i.e. image) the **Custom Object** should use, select the desired **Asset** from the dropdown beneath the Name Input.
If you leave the **Object Manager**, you should see the image in the World somwhere near your position.
### Animating Custom Objects [animating]
![](img/custom-object-animated.gif)
An animated **Custom Image Object**.
Of course, you don't want the image to stay at that position.
herefore, open the **Object Manager** again and select the **Custom Object** you want to modify.
On the lower half of the screen you will see mutliple **Input Fields**, using which you can modify various settings
(so-called **Transformations**), for example the Object's **Position**, **Scale**, **Orientation** and **Opacity**.
When editing these values, you'll notice that on the TImeline to the right, Keyframes appear.
You can set and remove Keyframes for specific settings using its "Add Keyframe" button on the very left of the screen.
The **Object Manager's Keyframe System** is very similar to the **Position Keyframe** and **Time Keyframe** System.
During a **Camera Path**, the **Custom Objects** will interpolate their **Transformation Values** like **Position Keyframes** are interpolated.
While outside the **Object Manager** you can preview the Objects' position by moving the cursor on the **Keyframe Timeline** - the Objects will interpolate to the position they'll be at that timestamp during the **Camera Path**.
Once you got the hang of it, you'll be able to quickly animate **Custom Objects**.
## Rendering [render]
With the **Replay Mod**, you can render **Camera Paths** to videos **without** using a screen recording tool like Fraps.
To get started, first [set up your Camera Path](#replaying-paths) as described in the previous chapters.
@@ -358,14 +436,7 @@ There are **7 Encoding Presets** you can choose from:
While these video files are of perfect quality, most **non-FFmpeg-based video players and video editing software** (e.g. QuickTime Player, Sony Vegas and Adobe Premiere) can't play these videos. Therefore, you should instead use the **MP4 - High Quality** preset in most cases.
- **PNG Sequence**
Exports the sequence as individual frames in the **PNG Format**.
The images are of perfect quality.
Depth map images are 32-bit float encoded in the 8-bit components in BGRA order and will need some kind of post-processing to be useful in most editors.
- **OpenEXR Sequence** (Minecraft 1.14 and above)
Exports the sequence as individual frames in the **OpenEXR Format**.
The images are of perfect quality and may contain
additional image layers beyond the visible one (e.g. [depth](#replaying-render-advanced-depth-map)).
As such, this format is a good choice if you have plenty of disk space to spare and want to perform additional editing
on the video in a third-party video editor.
**Warning:** This can create a huge amount of files, so make sure to save them in a separate folder.
### Advanced Settings [advanced]
![](img/rendersettings-advanced.jpg)
@@ -392,21 +463,6 @@ Using a **Video Editing Software** like **Adobe After Effects** or **Sony Vegas*
> **Note:** For best results, you should **disable clouds** before rendering, as they are transparent.
#### Depth Map [depth-map]
![](img/depthmapsuzanne.jpg)
"Suzanne" model added into a scene using Blender
This setting causes depth information to be recorded and included in video formats which support it
(currently only [OpenEXR](#replaying-render-settings-quality)).
Using a video editor which supports it (e.g. [Blender](https://www.blender.org/)), you can then for example (among many
other effects) add arbitrary geometry onto your rendered video in such a way that foreground objects in the
video will still occlude background geometry as if it actually was part of the world.
> **Note:** For best results, avoid semi-transparent blocks in front of your geometry, as they cannot be accurately represented in the depth map.
> **Note:** Anti-aliasing is not supported when exporting depth. Instead simply render and edit your video at a higher resolution and scale it down at the very end.
### Command Line Settings [commandline]
![](img/rendersettings-commandline.jpg)
The **Command Line Render Settings**
@@ -419,7 +475,7 @@ The **Replay Mod** runs [FFmpeg](http://ffmpeg.org/) via the **Command Line** to
You can customize both the **executed Command** and the **Command Line Arguments** in the **Command Line Settings** part of the **Render Settings Screen**.
#### Custom Command [command]
If you leave the left input field blank, `ffmpeg` will be used as **command**. If you haven't set your **PATH variable** to link to your FFmpeg distribution, simply enter the full path to your FFmpeg executable (e.g. `C:\ffmpeg\ffmpeg.exe` or `/usr/local/bin/ffmpeg`).
If you leave the left input field blank, `ffmpeg` will be used as **command**. If you haven't set your **PATH variable** to link to your FFmpeg distribution, simply enter the full path to your FFmpeg executable (e.g. `C:/ffmpeg/ffmpeg.exe` or `/usr/local/bin/ffmpeg`).
#### Command Line Arguments [arguments]
In the right input field, you can input custom **Command Line Arguments** to be used in the console.
@@ -454,6 +510,29 @@ To use **High Performance Rendering**, hold down the `Ctrl` key (`Cmd` key on M
but only after one rendered seconds (e.g. every 60 frames when rendering with 60fps)
- Resizing the Minecraft Window does not update the Rendering Gui
### Troubleshooting [troubleshooting]
> If you have trouble with rendering, please first consult the Documentation before asking for help in the Forums.
#### No FFmpeg installation found [ffmpeg]
![](img/ffmpeg-missing.jpg)
The error screen that is displayed when no FFmpeg installation could be found
If the **Replay Mod** tells you to install FFmpeg even though you already have, you have to manually tell the **Replay Mod** where your FFmpeg executable is located.
First, get the full path to your `ffmpeg.exe` (on Windows) or `ffmpeg` executable (on Mac/Linux).
On Windows, this path might look like `C:/ffmpeg/ffmpeg.exe`.
Then, open the **"Command Line Settings" Tab** in the Render Settings and paste this path into the **left input field** and retry rendering.
#### Crash while rendering [crash]
If Minecraft crashes after a few frames of rendering, it most likely means the **FFmpeg** didn't like the **Command Line Arguments** you passed.
If you customized the Command Line Arguments manually, re-check them - it's probably your own fault.
> **Hint:** In your .minecraft folder, you'll find a file called `export.log` which contains information about FFmpeg's rendering process.
If you did **not** customize the **Command Line Arguments**, you might have entered some insanely high (or low) values e.g. for Bitrate or Video Resolution. Try again with other, more reasonable values.
## Ambient Lighting [lighting]
![](img/ambient-lighting-comparison.jpg)
The same setting, onace with **Ambient Lighting** enabled, once with **Ambient Lighting** disabled
@@ -465,14 +544,6 @@ If you have a Replay in a dark setting (for example at nighttime, or in a cave)
This works as a replacement for the **Night Vision Potion Effect**, without the side effect of a weird sky color.
## Quick Mode [quickmode]
![](img/quickmode-icon.jpg)
In **Quick Mode**, this clock symbol is displayed in the lower right corner of the screen.
When you first enable **Quick Mode** in a replay, an internal reference of certain entity and block properties is stored for quick access, allowing for faster navigation in the **Replay Timeline**.
As a side effect, certain features like particles and second skin layers will not be rendered in the preview.
By default, **Quick Mode** is toggled with `Q`.
## Player Overview [overview]
![](img/player-overview.jpg)
The **Player Overview** Screen
@@ -496,9 +567,11 @@ The **Default Thumbnail** which is used if no Thumbnail was created
While in a Replay, you can use the `N` key to create a **Thumbnail** of the current Replay.
A **Thumbnail** is a Screenshot which should give the viewer a good impression of your Replay's content.
**Thumbnails** help keeping your **Replay Viewer** clear and structured.
**Thumbnails** are important when uploading a Replay to the **Replay Center**,
as other users are much more likely to download your Replay if they can see a preview of it.
They also help keeping your **Replay Viewer** clear and structured.
If no **Thumbnail** is set for a Replay, the **Default Thumbnail** will be displayed in the **Replay Viewer**.
If no **Thumbnail** is set for a Replay, the **Default Thumbnail** will be displayed in the **Replay Viewer** and in the **Replay Center**.
## Event Markers [markers]
![](img/marker-timeline.jpg)
@@ -521,8 +594,84 @@ This way, it's even simpler to add structure to your Replays.
You can **delete an Event Marker** by clicking it once to select it and then pressing the `DELETE` key.
# Replay Center [center]
The **Replay Center** is where you can **share** your **Minecraft Moments** with others and discover awesome **Replays by other Users**.
To be able to use the **Replay Center**, you need an **account on ReplayMod.com**.
## Authentication [auth]
![](img/auth-login.jpg)
The **Login Screen** which is displayed upon startup
![](img/auth-register.jpg)
The **Register Screen**
When starting Minecraft with the **Replay Mod** installed, you will be asked to login to **ReplayMod.com**.
If you don't want to use the **Replay Center**, you can click the **"Skip" Button** to continue without logging in.
If you want to browse other users' **Replays** and **share your own Replays** however, you need to **Register an account**.
> **Note:** By registering an Account on **ReplayMod.com**, you agree to the Website's [Terms of Service](https://www.replaymod.com/legal/terms)
Please note that you can only create **one ReplayMod.com Account per Minecraft Account**, so choose your username wisely.
Once you've registered, you're automatically going to be logged in. The Mod **remembers your login** until you manually log out from the **Replay Center**, so it won't ask you upon every startup if you logged in once.
## Replay Files [files]
![](img/replay-center.jpg)
The **Replay Center** in the Mod
Once you've logged in, you can use the **"Replay Center" Button** in the Main Menu to access the **Replay Center**.
In the upper half of the screen, there are **5 Buttons** for **5 Tabs** of the **Replay Center**:
- **Recent**
Shows the most recently uploaded Replays in the **Replay Center**
- **Best**
Shows the Replays with the **best rating and most downloads**
- **Downloaded**
Shows all of the Replays you've downloaded locally
- **Favorited**
Shows all of the Replays you favorited
- **Search**
Allows you to search for specific Replays
When you've found an insteresting looking Replay, you can download it using the **"Download" Button** in the lower left corner.
After downloading, you'll automatically join the Replay.
After you've downloaded a Replay, you can rate and favorite it in the **Replay Center** using the respective buttons.
> You are encouraged to rate Replays after you downloaded them to help promoting the best Replays.
## Upload Replays [upload]
![](img/replay-upload.jpg)
The **Replay Upload Screen**
You can share your own **Replay Files** with other users in the **Replay Center**.
Therefore, click the **"Upload" Button** in the **Replay Viewer** after selecting a Replay.
In the **Replay Upload Screen** there are **6 input fields**:
- **Replay Name**
The Replay's Name in the Replay Center
- **Replay Description**
A description of what happens in the Replay. Try to give the user a good impression why your Replay is worth downloading.
- **Category**
The Replay's Category, one of the following: **Survival**, **Build**, **Minigame**, **Miscellaneous**. If none of the other categories fit, use **Miscellaneous**.
- **Tags**
One or more tags that fit your replay, spearated by comma. **Example tags:** pvp,battle,redstone,creative
- **Hide Server IP**
If you've recorded a Replay on a private Server, you can remove the Server IP by checking the respective checkbox.
- **Thumbnail**
While you can't directly edit the Thumbnail when uploading, it is highly recommended that
you [create a Thumbnail](#replaying-thumbnail) before sharing your Replay.
Replays without a Thumbnail are much less likely to be downloaded by other users.
When uploading a Replay File, make sure to follow the [Replay Center Rules](https://www.replaymod.com/rules).
# Frequently Asked Questions [faq]
### Do I need a ReplayMod.com Account to use the mod?
You only need a ReplayMod.com Account to access the [Replay Center](#center).
All of the other features are also available offline.
### For how long can I record?
Theoretically, a Replay File can be up to **24 days, 20 hours, 30 minutes and 23 seconds** long - which is a timespan you'll probably never reach.
@@ -545,113 +694,3 @@ A Replay in which you travelled around and discovered a lot of terrain is signif
An average Replay File of **10 Minutes duration** usually is between **2MB and 10MB large**.
Replays recorded on **Minigame Servers** with lots of particle effects and world changes might be larger.
# Troubleshooting [troubleshooting]
> If you have trouble with rendering, please first consult the Documentation before asking for help in the Discord.
## No FFmpeg installation found [ffmpeg]
![](img/ffmpeg-missing.jpg)
The error screen that is displayed when no FFmpeg installation could be found
If you have not installed FFmpeg, please follow the steps provided [here](#installing-ffmpeg)
If already have installed FFmpeg, you have to manually tell the **Replay Mod** where your FFmpeg executable is located.
First, get the full path to your `ffmpeg.exe` (on Windows) or `ffmpeg` executable (on Mac/Linux).
On Windows, this path might look like `C:\ffmpeg\ffmpeg.exe`.
Then, open the **"Command Line Settings" Section** in the Render Settings and paste this path into the **left input field** and retry rendering.
## Crash while rendering [crash]
If Minecraft crashes after a few frames of rendering, it most likely means the **FFmpeg** didn't like the **Command Line Arguments** you passed.
If you customized the Command Line Arguments manually, re-check them - it's probably your own fault.
> **Hint:** In your .minecraft folder, you'll find a file called `export.log` which contains information about FFmpeg's rendering process.
- If you did **not** customize the **Command Line Arguments**, you might have entered some insanely high (or low) values e.g. for Bitrate or Video Resolution. Try again with other, more reasonable values.
- Make sure the camera doesn't move below Y=0 or above Y=255
## Unsupported Launchers [launchers]
_Future Client_ is not compatible with the Mixin required by ReplayMod.
_Lunar Client_ does not support ReplayMod.
_Badlion Client_ offers a different ReplayMod that is not the same as this one.
## Compatibility with other Mods [compatibility]
### General information [general]
In General, the Replay Mod _should_ be compatible with most Forge and Fabric Mods.
### Shaders Mod [shaders]
_Karyonix' Shaders Mod_ is no longer compatible with Minecraft Forge starting with 1.9.4. As such it is not compatible with the Replay Mod either.
Below Minecraft 1.16 you can try _Optifine_ instead. Note however, that official support has ended and many versions break ReplayMod.
On Minecraft 1.16.5 and up you can use _Iris_, which is fully supported. For the time being, you will have to use it with the custom _Sodium_ provided on our download page. The fix it includes is pending for the official version.
### Custom Main Menu [custom-main-menu]
The _Custom Main Menu_ mod is often used in mod packs to customize their Main Menu with a button layout fitting the background image, links to their website / bug tracker and similar.
If you are familiar with it, the button id for the Replay Mod is: **17890234** (text: `replaymod.gui.replayviewer`).
Due to the nature of this Custom Main Menu mod, buttons added to the Main Menu by 3rd party mods like the **Replay Mod** will not show up by default.
Thus, to access the Replay Viewer/Editor/Center, you need to manually configure the position for those buttons.
To do so, you need to modify the Custom Main Menu configuration file.
Usually, you can find it at `.minecraft/config/CustomMainMenu/mainmenu.json` (if you're using a mod pack launcher, it normally has its own .minecraft folder).
For an explanation of this config file, have a look at Custom Main Menu's page.
You can find a list of already modified config files [here](https://gist.github.com/Johni0702/3f3fab81dbf7ada83d045d9fe8f345aa).
### Tickrate Changer [tickrate-changer]
The _Tickrate Changer_ mod may cause minecraft to freeze when you try to use the Replay Viewer UI.
### LabyMod [labymod]
_LabyMod_ v3.7.x has been reported to be compatible with ReplayMod and other Forge mods.
Lower versions are not compatible.
### OldAnimationsMod [oldanimationsmod]
In case your Minecraft crashes when you are using both ReplayMod and _OldAnimationsMod_, try removing OldAnimationsMod.
### Orange's 1.7 Animations [17animations]
If you use ReplayMod with shaders and _1.7 Animations_ you may see floating water and glass. Remove 1.7 Animations to resolve this.
### Sk1er Club: Patcher [patcher]
We have seen reports of crashes that were related to _Patcher_; in case of crashes try removing Patcher.
### Baritone [baritone]
The _Baritone_ mod can cause a crash when you're trying to load a replay. If you experience such problems, try running without Baritone.
### RandomPatches [randompatches]
Minecraft may crash if you try to use _RandomPatches_ together with ReplayMod. Try removing RandomPatches if Minecraft crashes on startup.
### Sodium [sodium]
ReplayMod can record when _Sodium_ is installed, but currently lacks the FREX Flawless Frames API to render. A modified build of _Sodium_, that supports this API, is available from the ReplayMod downloads, by clicking the `Click to show compatible Sodium versions` button.
### Resource Loader [resourceloader]
The _Resource Loader_ mod is not compatible with ReplayMod.
### LiteLoader [liteloader]
![](img/mutlimc-liteloader.jpg)
The MultiMC interface to place ReplayMod above LiteLoader.
![](img/multimc-addempty.jpg)
The properties for the new *Empty*.
_LiteLoader_ is known to cause issues due to the mixin version it contains.
You can use MultiMC to workaround this problem.
> The MultiMC `Edit` function requires that your computer has a default editor for json files associated.
1: Create your instance then edit the instance
2: Click `Install Forge`
3: Click `Install LiteLoader`
4: Click `Add Empty`; use `ReplayMod` as name and `com.replaymod` as uid
5: Select the new *ReplayMod* and click `Edit`
6: Replace the contents with one of the below snippets
7: Save and close the text editor
8: Move *ReplayMod* up until it is **above** *LiteLoader*
9: Remove the ReplayMod jar from the mods folder if you installed it previously.
** Snippet for 1.8.9 **
`{ "formatVersion": 1, "name": "ReplayMod", "uid": "com.replaymod", "version": "1.8.9-2.4.5", "libraries": [{ "name": "com.replaymod:replaymod:1.8.9-2.4.5", "MMC-absoluteUrl": "https://minio.replaymod.com/replaymod/replaymod-1.8.9-2.4.5.jar" }]}`
** Snippet for 1.12.2 **
`{ "formatVersion": 1, "name": "ReplayMod", "uid": "com.replaymod", "version": "1.12.2-2.4.5", "libraries": [{ "name": "com.replaymod:replaymod:1.12.2-2.4.5", "MMC-absoluteUrl": "https://minio.replaymod.com/replaymod/replaymod-1.12.2-2.4.5.jar" }]}`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 KiB

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 598 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 75 KiB

23
docs/package-lock.json generated
View File

@@ -1,23 +0,0 @@
{
"name": "replaymod-docs",
"version": "0.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"array-findindex-polyfill": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/array-findindex-polyfill/-/array-findindex-polyfill-0.1.0.tgz",
"integrity": "sha1-w2JmW+x2RfItejw6rJeT9xw2Iu8="
},
"md-jml": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/md-jml/-/md-jml-2.0.1.tgz",
"integrity": "sha1-th99rTm1JPVKCmpthnMcTRLOCDo="
},
"string.prototype.endswith": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/string.prototype.endswith/-/string.prototype.endswith-0.2.0.tgz",
"integrity": "sha1-oZwg3uUamHd+mkfhDwm+OTubunU="
}
}
}

View File

@@ -1,10 +0,0 @@
essential.defaults.loom=0
essential.defaults.loom.fabric-loader=net.fabricmc:fabric-loader:0.18.5
# Sets default memory used for gradle commands. Can be overridden by user or command line properties.
# This is required to provide enough memory for the Minecraft decompilation process.
org.gradle.jvmargs=-Xmx8G
org.gradle.parallel=true
org.gradle.configureondemand=true
loom.ignoreDependencyLoomVersionValidation=true

Binary file not shown.

View File

@@ -1,5 +1,6 @@
#Sun May 10 21:57:58 CEST 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=http\://services.gradle.org/distributions/gradle-2.11-all.zip

126
gradlew vendored
View File

@@ -1,20 +1,4 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#!/usr/bin/env bash
##############################################################################
##
@@ -22,6 +6,47 @@
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
@@ -36,49 +61,9 @@ while [ -h "$PRG" ] ; do
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -105,7 +90,7 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@@ -129,7 +114,6 @@ fi
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
@@ -170,19 +154,11 @@ if $cygwin ; then
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
APP_ARGS=$(save "$@")
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

30
gradlew.bat vendored
View File

@@ -1,19 +1,3 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem http://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@@ -24,14 +8,14 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@@ -62,9 +46,10 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
@@ -75,6 +60,11 @@ set _SKIP=2
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line

2
jGui

Submodule jGui updated: b0e068383c...042e1c6e50

View File

@@ -1,288 +0,0 @@
import groovy.json.JsonOutput
import java.io.ByteArrayOutputStream
import org.gradle.kotlin.dsl.support.serviceOf
import org.gradle.process.ExecOperations
plugins {
id("gg.essential.multi-version.root")
id("gg.essential.loom") version "1.15.48" apply false
kotlin("jvm") version "2.3.20" apply false
id("com.github.hierynomus.license") version "0.15.0"
}
val latestVersion = file("version.txt").readLines().first()
var releaseCommit = providers.exec {
commandLine("git", "blame", "-p", "-l", "version.txt")
}.standardOutput.asText.map { it.trim().split("/n").first().split(" ").first() }.get()
if (latestVersion == "2.1.0") { // First version since change from tag-based
releaseCommit = "35ac26e91689ac9bdf12dbb9902c452464a75108" // git rev-parse 1.12.2-2.1.0
}
val currentCommit = providers.exec {
commandLine("git", "rev-parse", "HEAD")
}.standardOutput.asText.map { it.trim().split("/n").first() }.get()
if (releaseCommit == currentCommit) {
version = latestVersion
} else {
val diff = providers.exec {
commandLine("git", "log", "--format=oneline", "$releaseCommit..$currentCommit")
}.standardOutput.asText.map { it.trim().split("/n").size }.get()
version = "$latestVersion-$diff-g${currentCommit.substring(0, 7)}"
}
val dirty = providers.exec {
commandLine("git", "describe", "--always", "--dirty=*")
}.standardOutput.asText.map { it.trim().endsWith("*") }.get()
if (dirty) {
version = "$version-dirty"
}
group = "com.replaymod"
val bundleJar by tasks.registering(Copy::class) {
into("$buildDir/libs")
}
subprojects {
buildscript {
repositories {
maven("https://jitpack.io")
}
}
if (name == "jGui" || name == "ReplayStudio") {
return@subprojects
}
val (major, minor, patch) = name.split("-")[0].split(".") + listOf("0")
val mcVersion = major.toInt() * 10000 + minor.toInt() * 100 + patch.toInt()
val fabric = mcVersion >= 1_14_00 && !name.endsWith("-forge")
extra.set("loom.platform", if (fabric) "fabric" else "forge")
afterEvaluate {
val projectBundleJar = project.tasks.findByName("bundleJar")
if (projectBundleJar != null && projectBundleJar.hasProperty("archivePath") && project.name != "core") {
bundleJar.configure {
dependsOn(projectBundleJar)
from(projectBundleJar.withGroovyBuilder { getProperty("archivePath") })
}
}
}
}
fun ExecOperations.command(vararg cmd: String): List<String> {
val stdout = ByteArrayOutputStream()
exec {
commandLine(*cmd)
standardOutput = stdout
}
return stdout.toString().trim().split("\n")
}
fun generateVersionsJson(execOps: ExecOperations): Map<String, Any> {
fun command(vararg cmd: String): List<String> =
execOps.command(*cmd)
val versionComparator = compareBy<String>(
{ (it.split(".").getOrNull(0) ?: "0").toInt() },
{ (it.split(".").getOrNull(1) ?: "0").toInt() },
{ (it.split(".").getOrNull(2) ?: "0").toInt() }
)
// Find all tag-style releases by listing all tags
val tagVersions = command("git", "for-each-ref", "--sort=taggerdate", "--format=%(refname:short)", "refs/tags")
// Find all commit-style releases
// List all commits
val releaseCommits =
command("git", "log", "--format=%H", "--date-order", "-E",
"--grep", "Pre-release [0-9]+ of [0-9]+\\.[0-9]+\\.[0-9]+",
"--grep", "Release [0-9]+\\.[0-9]+\\.[0-9]+")
// Find version string and MC versions for each commit hash
val commitVersions = releaseCommits.map { commit ->
val version = command("git", "show", "$commit:version.txt").first()
val mcVersions = command("git", "ls-tree", "-d", "--name-only", "$commit:versions")
// In the past (early days of preprocessor) we used to have an internal "core" project
.filter { it != "core" }
// Internal project used to automatically remap from Forge 1.12.2 to Fabric 1.14.4
.filter { it != "1.14.4-forge" }
// We dropped 1.8 with the switch to archloom but still kept its source in case someone
// volunteers to make it build again
.filterNot { it == "1.8" && versionComparator.compare(version, "2.6.16") >= 0 }
// We dropped 1.7.10 with the Gradle 7 update but still kept its source in case someone
// volunteers to ~~update FG 1.2 to Gradle 7~~ make it work with archloom.
.filterNot { it == "1.7.10" && versionComparator.compare(version, "2.6.0") >= 0 }
val versions = mcVersions.map { "$it-$version" }.toMutableList()
when (version) {
"2.6.7" -> versions.add("1.19.1-2.6.7") // forgot to add the .gitkeep file before merging
}
versions
}.flatten()
val versions = commitVersions + tagVersions.reversed()
val mcVersions = versions
.map {it.substring(0, it.indexOf("-"))}
.distinct()
.sortedWith(versionComparator)
val promos = mutableMapOf<String, String>()
val root = mutableMapOf<String, Any>(
"homepage" to "https://www.replaymod.com/download/",
"promos" to promos
)
mcVersions.forEach { mcVersion ->
var mcVersionRoot = mutableMapOf<String, String>()
var latest: String? = null
var recommended: String? = null
versions.forEach {
val (thisMcVersion, modVersion, preVersion) = it.split("-") + listOf(null, null)
if (thisMcVersion == mcVersion) {
mcVersionRoot[it] = "See https://www.replaymod.com/changelog.txt"
if (latest == null) latest = it
if (preVersion == null) {
if (recommended == null) recommended = it
}
}
}
root[mcVersion] = mcVersionRoot
promos[mcVersion + "-latest"] = latest!!
if (recommended != null) {
promos[mcVersion + "-recommended"] = recommended!!
}
}
return root
}
val writeVersionsJson by tasks.registering {
val execOps = project.serviceOf<ExecOperations>()
doLast {
val versionsRoot = generateVersionsJson(execOps)
val versionsJson = JsonOutput.prettyPrint(JsonOutput.toJson(versionsRoot))
File("versions.json").writeText(versionsJson)
}
}
val doRelease by tasks.registering {
val execOps = project.serviceOf<ExecOperations>()
doLast {
fun command(vararg cmd: String): List<String> =
execOps.command(*cmd)
// Parse version
val version = project.extra["releaseVersion"] as String
if (command("git", "describe", "--always", "--dirty=*").first().endsWith("*")) {
throw InvalidUserDataException("Git working tree is dirty. Make sure to commit all changes.")
}
val (modVersion, preVersion) = if ("-b" in version) {
version.split("-b")
} else {
listOf(version, null)
}
// Merge release branch into stable but do not yet commit (we need to update the version.txt first)
command("git", "checkout", "stable")
command("git", "merge", "--no-ff", "--no-commit", "release-$version")
// Update version.txt
file("version.txt").writeText("$version\n")
command("git", "add", "version.txt")
// Finallize the merge. The message is what is later used to identify releses for building the version.json tree
val commitMessage = if (preVersion != null)
"Pre-release $preVersion of $modVersion"
else
"Release $modVersion"
command("git", "commit", "-m", commitMessage)
// Generate versions.json content
val versionsRoot = generateVersionsJson(execOps)
val versionsJson = JsonOutput.prettyPrint(JsonOutput.toJson(versionsRoot))
// Switch to master branch to update versions.json
command("git", "checkout", "master")
// Write versions.json
File("versions.json").writeText(versionsJson)
// Commit changes
command("git", "add", "versions.json")
command("git", "commit", "-m", "Update versions.json for $version")
// Return to previous branch
command("git", "checkout", "-")
}
}
defaultTasks("bundleJar")
preprocess {
strictExtraMappings.set(true)
val mc26_01_00 = createNode("26.1", 26_01_00, "yarn")
val mc12111 = createNode("1.21.11", 12111, "yarn")
val mc12110 = createNode("1.21.10", 12110, "yarn")
val mc12107 = createNode("1.21.7", 12107, "yarn")
val mc12105 = createNode("1.21.5", 12105, "yarn")
val mc12104 = createNode("1.21.4", 12104, "yarn")
val mc12102 = createNode("1.21.2", 12102, "yarn")
val mc12100 = createNode("1.21", 12100, "yarn")
val mc12006 = createNode("1.20.6", 12006, "yarn")
val mc12004 = createNode("1.20.4", 12004, "yarn")
val mc12002 = createNode("1.20.2", 12002, "yarn")
val mc12001 = createNode("1.20.1", 12001, "yarn")
val mc11904 = createNode("1.19.4", 11904, "yarn")
val mc11903 = createNode("1.19.3", 11903, "yarn")
val mc11902 = createNode("1.19.2", 11902, "yarn")
val mc11901 = createNode("1.19.1", 11901, "yarn")
val mc11900 = createNode("1.19", 11900, "yarn")
val mc11802 = createNode("1.18.2", 11802, "yarn")
val mc11801 = createNode("1.18.1", 11801, "yarn")
val mc11701 = createNode("1.17.1", 11701, "yarn")
val mc11604 = createNode("1.16.4", 11604, "yarn")
val mc11601 = createNode("1.16.1", 11601, "yarn")
val mc11502 = createNode("1.15.2", 11502, "yarn")
val mc11404 = createNode("1.14.4", 11404, "yarn")
val mc11404Forge = createNode("1.14.4-forge", 11404, "srg")
val mc11202 = createNode("1.12.2", 11202, "srg")
val mc11201 = createNode("1.12.1", 11201, "srg")
val mc11200 = createNode("1.12", 11200, "srg")
val mc11102 = createNode("1.11.2", 11102, "srg")
val mc11100 = createNode("1.11", 11100, "srg")
val mc11002 = createNode("1.10.2", 11002, "srg")
val mc10904 = createNode("1.9.4", 10904, "srg")
val mc10809 = createNode("1.8.9", 10809, "srg")
val mc10800 = createNode("1.8", 10800, "srg")
val mc10710 = createNode("1.7.10", 10710, "srg")
mc26_01_00.link(mc12111, file("versions/mapping-fabric-26.1-1.21.11.txt"))
mc12111.link(mc12110, file("versions/mapping-fabric-1.21.11-1.21.10.txt"))
mc12110.link(mc12107, file("versions/mapping-fabric-1.21.10-1.21.7.txt"))
mc12107.link(mc12105)
mc12105.link(mc12104, file("versions/mapping-fabric-1.21.5-1.21.4.txt"))
mc12104.link(mc12102)
mc12102.link(mc12100)
mc12100.link(mc12006)
mc12006.link(mc12004)
mc12004.link(mc12002, file("versions/mapping-fabric-1.20.4-1.20.2.txt"))
mc12002.link(mc12001)
mc12001.link(mc11904, file("versions/mapping-fabric-1.20.1-1.19.4.txt"))
mc11904.link(mc11903, file("versions/mapping-fabric-1.19.4-1.19.3.txt"))
mc11903.link(mc11902, file("versions/mapping-fabric-1.19.3-1.19.2.txt"))
mc11902.link(mc11901)
mc11901.link(mc11900)
mc11900.link(mc11802)
mc11802.link(mc11801)
mc11801.link(mc11701, file("versions/mapping-fabric-1.18.1-1.17.1.txt"))
mc11701.link(mc11604, file("versions/mapping-fabric-1.17.1-1.16.4.txt"))
mc11604.link(mc11601)
mc11601.link(mc11502)
mc11502.link(mc11404, file("versions/mapping-fabric-1.15.2-1.14.4.txt"))
mc11404.link(mc11404Forge)
mc11404Forge.link(mc11202, file("versions/mapping-forge-1.14.4-1.12.2.txt"))
mc11202.link(mc11201)
mc11201.link(mc11200)
mc11200.link(mc11102)
mc11102.link(mc11100)
mc11100.link(mc11002)
mc11002.link(mc10904)
mc10904.link(mc10809, file("versions/mapping-forge-1.9.4-1.8.9.txt"))
mc10809.link(mc10800, file("versions/mapping-forge-1.8.9-1.8.txt"))
mc10800.link(mc10710, file("versions/mapping-forge-1.8-1.7.10.txt"))
}

View File

@@ -1,112 +0,0 @@
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
mavenCentral()
google()
maven("https://jitpack.io")
maven("https://maven.fabricmc.net")
maven("https://maven.architectury.dev/")
maven("https://maven.minecraftforge.net")
maven("https://repo.essential.gg/repository/maven-public")
}
plugins {
id("gg.essential.multi-version.root") version "0.7.0-alpha.4"
id("com.gradleup.shadow") version "9.4.1"
}
}
val jGuiVersions = listOf(
// "1.7.10",
// "1.8",
"1.8.9",
"1.9.4",
"1.12",
"1.14.4-forge",
"1.14.4",
"1.15.2",
"1.16.1",
"1.16.4",
"1.17.1",
"1.18.1",
"1.18.2",
"1.19",
"1.19.1",
"1.19.2",
"1.19.3",
"1.19.4",
"1.20.1",
"1.20.2",
"1.20.4",
"1.20.6",
"1.21",
"1.21.2",
"1.21.4",
"1.21.5",
"1.21.7",
"1.21.10",
"1.21.11",
"26.1",
)
val replayModVersions = listOf(
// "1.7.10",
// "1.8",
"1.8.9",
"1.9.4",
"1.10.2",
"1.11",
"1.11.2",
"1.12",
"1.12.1",
"1.12.2",
"1.14.4-forge",
"1.14.4",
"1.15.2",
"1.16.1",
"1.16.4",
"1.17.1",
"1.18.1",
"1.18.2",
"1.19",
"1.19.1",
"1.19.2",
"1.19.3",
"1.19.4",
"1.20.1",
"1.20.2",
"1.20.4",
"1.20.6",
"1.21",
"1.21.2",
"1.21.4",
"1.21.5",
"1.21.7",
"1.21.10",
"1.21.11",
"26.1",
)
rootProject.buildFileName = "root.gradle.kts"
includeBuild("libs/ReplayStudio")
include(":jGui")
project(":jGui").apply {
projectDir = file("jGui")
buildFileName = "root.gradle.kts"
}
jGuiVersions.forEach { version ->
include(":jGui:$version")
project(":jGui:$version").apply {
projectDir = file("jGui/versions/$version")
buildFileName = "../../build.gradle.kts"
}
}
replayModVersions.forEach { version ->
include(":$version")
project(":$version").apply {
projectDir = file("versions/$version")
buildFileName = "../../build.gradle.kts"
}
}

View File

@@ -3,6 +3,9 @@ package com.replaymod.core;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.gameevent.TickEvent;
import de.johni0702.minecraft.gui.container.AbstractGuiOverlay;
import de.johni0702.minecraft.gui.container.AbstractGuiScreen;
import de.johni0702.minecraft.gui.container.GuiContainer;
@@ -16,9 +19,6 @@ import de.johni0702.minecraft.gui.utils.Consumer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.settings.KeyBinding;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import org.lwjgl.input.Keyboard;
import java.lang.reflect.Field;
@@ -50,8 +50,10 @@ public abstract class AbstractTask implements Task {
public ListenableFuture<Void> execute() {
future = SettableFuture.create();
FMLCommonHandler.instance().bus().register(this);
MinecraftForge.EVENT_BUS.register(this);
addCallback(future, success -> {
FMLCommonHandler.instance().bus().unregister(this);
MinecraftForge.EVENT_BUS.unregister(this);
}, error -> {});
@@ -82,15 +84,26 @@ public abstract class AbstractTask implements Task {
private void expectGuiClosed0(int timeout, Runnable onClosed) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
class EventHandler {
FMLCommonHandler.instance().bus().register(new GuiEventHandler(timeout, stackTrace, onClosed));
}
public class GuiEventHandler {
final net.minecraft.client.gui.GuiScreen currentScreen = mc.currentScreen;
private final int timeout;
private final StackTraceElement[] stackTrace;
private final Runnable onClosed;
int framesPassed;
public GuiEventHandler(int timeout, StackTraceElement[] stackTrace, Runnable onClosed) {
this.timeout = timeout;
this.stackTrace = stackTrace;
this.onClosed = onClosed;
}
@SubscribeEvent
public void onGuiOpen(TickEvent.RenderTickEvent event) {
if (event.phase != TickEvent.Phase.START) return;
if (currentScreen != mc.currentScreen) {
MinecraftForge.EVENT_BUS.unregister(this);
FMLCommonHandler.instance().bus().unregister(this);
onClosed.run();
} else {
if (framesPassed < timeout) {
@@ -104,8 +117,6 @@ public abstract class AbstractTask implements Task {
}
}
}
MinecraftForge.EVENT_BUS.register(new EventHandler());
}
public void expectPopupClosed(Runnable onClosed) {
expectPopupClosed0(10, onClosed);
@@ -117,18 +128,30 @@ public abstract class AbstractTask implements Task {
private void expectPopupClosed0(int timeout, Runnable onClosed) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
AbstractGuiPopup popup = getPopup(mc.currentScreen);
if (popup == null) {
throw new IllegalStateException("No popup found.");
}
class EventHandler {
FMLCommonHandler.instance().bus().register(new PopupEventHandler(timeout, stackTrace, onClosed));
}
public class PopupEventHandler {
private final int timeout;
private final StackTraceElement[] stackTrace;
private final Runnable onClosed;
private final AbstractGuiPopup popup;
int framesPassed;
public PopupEventHandler(int timeout, StackTraceElement[] stackTrace, Runnable onClosed) {
this.timeout = timeout;
this.stackTrace = stackTrace;
this.onClosed = onClosed;
this.popup = getPopup(mc.currentScreen);
if (this.popup == null) {
throw new IllegalStateException("No popup found.");
}
}
@SubscribeEvent
public void onGuiOpen(TickEvent.RenderTickEvent event) {
if (event.phase != TickEvent.Phase.START) return;
if (getPopup(mc.currentScreen) != popup) {
MinecraftForge.EVENT_BUS.unregister(this);
FMLCommonHandler.instance().bus().unregister(this);
onClosed.run();
} else {
if (framesPassed < timeout) {
@@ -141,8 +164,6 @@ public abstract class AbstractTask implements Task {
}
}
}
MinecraftForge.EVENT_BUS.register(new EventHandler());
}
private AbstractGuiPopup getPopup(net.minecraft.client.gui.GuiScreen minecraft) {
GuiContainer<?> container = GuiOverlay.from(minecraft);
@@ -163,10 +184,21 @@ public abstract class AbstractTask implements Task {
public <T> void expectGui(Class<T> guiClass, Consumer<T> onOpen) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
class EventHandler {
FMLCommonHandler.instance().bus().register(new ExpectGuiEventHandler<T>(guiClass, stackTrace, onOpen));
}
public class ExpectGuiEventHandler<T> {
private final Class<T> guiClass;
private final StackTraceElement[] stackTrace;
private final Consumer<T> onOpen;
net.minecraft.client.gui.GuiScreen currentScreen;
int framesPassed;
public ExpectGuiEventHandler(Class<T> guiClass, StackTraceElement[] stackTrace, Consumer<T> onOpen) {
this.guiClass = guiClass;
this.stackTrace = stackTrace;
this.onOpen = onOpen;
}
@SubscribeEvent
public void onGuiOpen(TickEvent.RenderTickEvent event) {
if (event.phase != TickEvent.Phase.START) return;
@@ -179,7 +211,7 @@ public abstract class AbstractTask implements Task {
return;
}
MinecraftForge.EVENT_BUS.unregister(this);
FMLCommonHandler.instance().bus().unregister(this);
Object foundGui = null;
if (AbstractGuiScreen.class.isAssignableFrom(guiClass)) {
@@ -208,16 +240,15 @@ public abstract class AbstractTask implements Task {
return;
}
}
class UnexpectedGuiException extends Exception {
UnexpectedGuiException(Object foundGui) {
super("Expected instance of " + guiClass + " but found " + foundGui);
setStackTrace(Arrays.copyOfRange(stackTrace, 2, stackTrace.length));
}
}
future.setException(new UnexpectedGuiException(foundGui == null ? currentScreen : foundGui));
future.setException(new UnexpectedGuiException(guiClass, stackTrace,
foundGui == null ? currentScreen : foundGui));
}
}
MinecraftForge.EVENT_BUS.register(new EventHandler());
class UnexpectedGuiException extends Exception {
UnexpectedGuiException(Class<?> guiClass, StackTraceElement[] stackTrace, Object foundGui) {
super("Expected instance of " + guiClass + " but found " + foundGui);
setStackTrace(Arrays.copyOfRange(stackTrace, 2, stackTrace.length));
}
}
private void clickNow(int x, int y) {
@@ -299,7 +330,7 @@ public abstract class AbstractTask implements Task {
return;
}
clickNow(button.x + 5, button.y + 5);
clickNow(button.xPosition + 5, button.yPosition + 5);
} catch (IllegalAccessException | NoSuchFieldException e) {
future.setException(e);
}

View File

@@ -10,10 +10,10 @@ import com.replaymod.recording.ExitSPWorld;
import com.replaymod.replay.ExitReplay;
import com.replaymod.replay.LoadReplay;
import com.replaymod.replay.OpenReplayViewer;
import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
import net.minecraft.client.Minecraft;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import org.apache.logging.log4j.Logger;
import static com.replaymod.core.AbstractTask.mc;
@@ -36,11 +36,6 @@ public class ReplayModIntegrationTest {
// Make sure the game window doesn't have to remain in focus during the test
mc.gameSettings.pauseOnLostFocus = false;
// Vanilla uses our keys
mc.gameSettings.keyBindAdvancements.setKeyCode(0);
mc.gameSettings.keyBindLoadToolbar.setKeyCode(0);
mc.gameSettings.keyBindSaveToolbar.setKeyCode(0);
runTasks(
new SkipLogin(),
new DownloadOpenEye(),

View File

@@ -2,7 +2,7 @@ package com.replaymod.extra;
import com.replaymod.core.AbstractTask;
import com.replaymod.extras.OpenEyeExtra;
import net.minecraftforge.fml.common.Loader;
import cpw.mods.fml.common.Loader;
import java.io.File;
import java.nio.file.NoSuchFileException;
@@ -10,10 +10,6 @@ import java.nio.file.NoSuchFileException;
public class DownloadOpenEye extends AbstractTask {
@Override
protected void init() {
if ("1".equals(System.getenv("RM_INTEGRATION_TEST_NO_OPENEYE"))) {
runLater(() -> future.set(null));
return;
}
expectGui(OpenEyeExtra.OfferGui.class, offerGui -> {
click(offerGui.yesButton);
expectGuiClosed(20 * 1000, () -> {

View File

@@ -1,32 +1,33 @@
package com.replaymod.recording;
import com.replaymod.core.AbstractTask;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import net.minecraft.client.gui.GuiCreateWorld;
import net.minecraft.client.gui.GuiMainMenu;
import net.minecraft.client.gui.GuiWorldSelection;
import net.minecraft.client.gui.GuiSelectWorld;
import net.minecraftforge.client.event.RenderGameOverlayEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
public class CreateSPWorld extends AbstractTask {
@Override
protected void init() {
expectGui(GuiMainMenu.class, mainMenu -> {
click("Singleplayer");
expectGui(GuiWorldSelection.class, selectWorld -> {
expectGui(GuiSelectWorld.class, selectWorld -> {
click("Create New World");
expectGui(GuiCreateWorld.class, createWorld -> {
click("Create New World");
class EventHandler {
@SubscribeEvent
public void onRenderIngame(RenderGameOverlayEvent.Pre event) {
MinecraftForge.EVENT_BUS.unregister(this);
runLater(() -> future.set(null));
}
}
MinecraftForge.EVENT_BUS.register(new EventHandler());
});
});
});
}
public class EventHandler {
@SubscribeEvent
public void onRenderIngame(RenderGameOverlayEvent.Pre event) {
MinecraftForge.EVENT_BUS.unregister(this);
runLater(() -> future.set(null));
}
}
}

View File

@@ -2,10 +2,10 @@ package com.replaymod.replay;
import com.replaymod.core.AbstractTask;
import com.replaymod.replay.gui.screen.GuiReplayViewer;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import de.johni0702.minecraft.gui.function.Clickable;
import net.minecraftforge.client.event.RenderGameOverlayEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import org.lwjgl.util.Point;
import org.lwjgl.util.ReadableDimension;
@@ -19,14 +19,15 @@ public class LoadReplay extends AbstractTask {
// Load first replay
click(replayViewer.loadButton);
class EventHandler {
@SubscribeEvent
public void onRenderIngame(RenderGameOverlayEvent.Pre event) {
MinecraftForge.EVENT_BUS.unregister(this);
runLater(() -> future.set(null));
}
}
MinecraftForge.EVENT_BUS.register(new EventHandler());
}));
}
public class EventHandler {
@SubscribeEvent
public void onRenderIngame(RenderGameOverlayEvent.Pre event) {
MinecraftForge.EVENT_BUS.unregister(this);
runLater(() -> future.set(null));
}
}
}

View File

@@ -2,8 +2,8 @@ package com.replaymod.replay;
import com.replaymod.core.AbstractTask;
import com.replaymod.extras.playeroverview.PlayerOverviewGui;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.gameevent.TickEvent;
import org.lwjgl.input.Keyboard;
import java.util.concurrent.TimeoutException;
@@ -34,7 +34,7 @@ public class SpectatePlayer extends AbstractTask {
future.setException(new TimeoutException("Camera hasn't stopped spectating."));
return;
}
if (mc.getRenderViewEntity() == mc.player) {
if (mc.renderViewEntity == mc.thePlayer) {
future.set(null);
}
}

View File

@@ -1,37 +1,27 @@
package com.replaymod.compat;
import com.replaymod.compat.bettersprinting.DisableBetterSprinting;
import com.replaymod.compat.optifine.DisableFastRender;
import com.replaymod.core.Module;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.replaymod.compat.oranges17animations.HideInvisibleEntities;
import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.eventhandler.EventBus;
//#if MC<11400
//$$ import com.replaymod.compat.oranges17animations.HideInvisibleEntities;
//#endif
@Mod(modid = ReplayModCompat.MOD_ID,
version = "@MOD_VERSION@",
acceptedMinecraftVersions = "@MC_VERSION@",
acceptableRemoteVersions = "*",
useMetadata = true)
public class ReplayModCompat {
public static final String MOD_ID = "replaymod-compat";
//#if MC<11400
//$$ import com.replaymod.compat.bettersprinting.DisableBetterSprinting;
//#endif
//#if MC>=10800
import com.replaymod.compat.shaders.ShaderBeginRender;
//#endif
public class ReplayModCompat implements Module {
public static Logger LOGGER = LogManager.getLogger();
@Override
public void initClient() {
//#if MC>=10800
new ShaderBeginRender().register();
//#endif
new DisableFastRender().register();
//#if MC<11400
//$$ new HideInvisibleEntities().register();
//#endif
//#if MC<11400
//$$ DisableBetterSprinting.register();
//#endif
@Mod.EventHandler
public void init(FMLInitializationEvent event) {
EventBus bus = FMLCommonHandler.instance().bus();
bus.register(new DisableFastRender());
bus.register(new HideInvisibleEntities());
DisableBetterSprinting.register();
}
}

View File

@@ -1 +1,97 @@
// 1.12.2 and below
package com.replaymod.compat.bettersprinting;
import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replay.events.ReplayChatMessageEvent;
import cpw.mods.fml.common.Loader;
import cpw.mods.fml.common.ModContainer;
import cpw.mods.fml.common.eventhandler.EventPriority;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.versioning.DefaultArtifactVersion;
import cpw.mods.fml.common.versioning.Restriction;
import cpw.mods.fml.common.versioning.VersionRange;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.PlayerControllerMP;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.world.IWorldAccess;
import net.minecraftforge.client.event.GuiOpenEvent;
import net.minecraftforge.common.MinecraftForge;
import java.util.Collections;
/**
* Old Better Sprinting versions replace the vanilla player with their own, overridden instance (replacing the camera entity).
*
* See: https://github.com/chylex/Better-Sprinting/blob/1.8/src/main/java/chylex/bettersprinting/client/player/impl/LogicImplOverride.java
*/
public class DisableBetterSprinting {
private static final VersionRange OLD_VERSION = VersionRange.newRange(null,
Collections.singletonList(new Restriction(null, false, new DefaultArtifactVersion("2.0.0"), false)));
private static final String LOGIC_CLASS_NAME = "chylex.bettersprinting.client.player.impl.LogicImplOverride";
private static final String CONTROLLER_OVERRIDE_CLASS_NAME = LOGIC_CLASS_NAME + ".PlayerControllerMPOverride";
public static void register() {
Loader.instance().getModList().stream()
.filter(mod -> mod.getModId().equalsIgnoreCase("bettersprinting"))
.findFirst()
.map(ModContainer::getProcessedVersion).filter(OLD_VERSION::containsVersion)
.ifPresent($_ -> MinecraftForge.EVENT_BUS.register(new DisableBetterSprinting()));
}
private DisableBetterSprinting() {}
private final Minecraft mc = Minecraft.getMinecraft();
private PlayerControllerMP originalController;
private BetterSprintingWorldAccess worldAccessHook = new BetterSprintingWorldAccess();
@SubscribeEvent(priority = EventPriority.HIGH)
public void beforeGuiOpenEvent(GuiOpenEvent event) {
if (ReplayModReplay.instance.getReplayHandler() != null && mc.theWorld != null) {
// During replay, get ready to revert BetterSprinting's overwritten playerController
originalController = mc.playerController;
mc.theWorld.addWorldAccess(worldAccessHook);
}
}
@SubscribeEvent(priority = EventPriority.LOW)
public void afterGuiOpenEvent(GuiOpenEvent event) {
if (ReplayModReplay.instance.getReplayHandler() != null && mc.theWorld != null) {
mc.theWorld.removeWorldAccess(worldAccessHook);
}
}
@SubscribeEvent
public void onReplayChatMessage(ReplayChatMessageEvent event) {
// Suppress this message if it's the Better Sprinting warning message
for (StackTraceElement elem : Thread.currentThread().getStackTrace()) {
if (LOGIC_CLASS_NAME.equals(elem.getClassName())) {
event.setCanceled(true);
return;
}
}
}
private class BetterSprintingWorldAccess implements IWorldAccess {
@Override
public void onEntityDestroy(Entity entityIn) {
if (mc.playerController != null && mc.playerController.getClass().getName().equals(CONTROLLER_OVERRIDE_CLASS_NAME)) {
// Someone has secretly swapped out the player controller and is about to substitute their own player entity.
// This is the right time to destroy their plan.
mc.playerController = originalController;
}
}
@Override public void markBlockForUpdate(int x, int y, int z) {}
@Override public void markBlockForRenderUpdate(int p_147588_1_, int p_147588_2_, int p_147588_3_) {}
@Override public void markBlockRangeForRenderUpdate(int x1, int y1, int z1, int x2, int y2, int z2) {}
@Override public void playSound(String soundName, double x, double y, double z, float volume, float pitch) {}
@Override public void playSoundToNearExcept(EntityPlayer except, String soundName, double x, double y, double z, float volume, float pitch) {}
@Override public void spawnParticle(String p_180442_1_, double p_180442_2_, double p_180442_3_, double p_180442_5_, double p_180442_7_, double p_180442_9_, double p_180442_11_) {}
@Override public void onEntityCreate(Entity p_72703_1_) {}
@Override public void playRecord(String recordName, int x, int y, int z) {}
@Override public void broadcastSound(int p_180440_1_, int x, int y, int z, int p_180440_3_) {}
@Override public void playAuxSFX(EntityPlayer p_72706_1_, int p_72706_2_, int p_72706_3_, int p_72706_4_, int p_72706_5_, int p_72706_6_) {}
@Override public void destroyBlockPartially(int p_147587_1_, int p_147587_2_, int p_147587_3_, int p_147587_4_, int p_147587_5_) {}
@Override public void onStaticEntitiesChanged() {}
}
}

View File

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

View File

@@ -1,34 +1,35 @@
package com.replaymod.compat.optifine;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import com.replaymod.core.versions.MCVer;
import com.replaymod.render.events.ReplayRenderCallback;
import net.minecraft.client.MinecraftClient;
import com.replaymod.render.events.ReplayRenderEvent;
import cpw.mods.fml.client.FMLClientHandler;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import net.minecraft.client.Minecraft;
public class DisableFastRender extends EventRegistrations {
// TODO 1.7.10: Is this still necessary (probably, but needs checking)?
public class DisableFastRender {
private final MinecraftClient mc = MinecraftClient.getInstance();
private final Minecraft mc = Minecraft.getMinecraft();
private boolean wasFastRender = false;
{ on(ReplayRenderCallback.Pre.EVENT, renderer -> onRenderBegin()); }
private void onRenderBegin() {
if (!MCVer.hasOptifine()) return;
@SubscribeEvent
public void onRenderBegin(ReplayRenderEvent.Pre event) {
if (!FMLClientHandler.instance().hasOptifine()) return;
try {
wasFastRender = (boolean) OptifineReflection.gameSettings_ofFastRender.get(mc.options);
OptifineReflection.gameSettings_ofFastRender.set(mc.options, false);
wasFastRender = (boolean) OptifineReflection.gameSettings_ofFastRender.get(mc.gameSettings);
OptifineReflection.gameSettings_ofFastRender.set(mc.gameSettings, false);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
{ on(ReplayRenderCallback.Post.EVENT, renderer -> onRenderEnd()); }
private void onRenderEnd() {
if (!MCVer.hasOptifine()) return;
@SubscribeEvent
public void onRenderEnd(ReplayRenderEvent.Post event){
if (!FMLClientHandler.instance().hasOptifine()) return;
try {
OptifineReflection.gameSettings_ofFastRender.set(mc.options, wasFastRender);
OptifineReflection.gameSettings_ofFastRender.set(mc.gameSettings, wasFastRender);
} catch (IllegalAccessException e) {
e.printStackTrace();
}

View File

@@ -1,9 +1,8 @@
package com.replaymod.compat.optifine;
import net.minecraft.client.options.GameOptions;
import net.minecraft.client.settings.GameSettings;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class OptifineReflection {
@@ -15,7 +14,7 @@ public class OptifineReflection {
// this throws an ignored ClassNotFoundException if Optifine isn't installed
Class.forName("Config");
gameSettings_ofFastRender = GameOptions.class.getDeclaredField("ofFastRender");
gameSettings_ofFastRender = GameSettings.class.getDeclaredField("ofFastRender");
gameSettings_ofFastRender.setAccessible(true);
} catch (ClassNotFoundException ignore) {
// no optifine installed
@@ -25,20 +24,4 @@ public class OptifineReflection {
}
}
public static void reloadLang() {
try {
Class<?> langClass;
try {
langClass = Class.forName("Lang"); // Pre Optifine 1.12.2 E1
} catch (ClassNotFoundException ignore) {
langClass = Class.forName("net.optifine.Lang"); // Post Optifine 1.12.2 E1
}
langClass.getDeclaredMethod("resourcesReloaded").invoke(null);
} catch (ClassNotFoundException ignore) {
// no optifine installed
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
e.printStackTrace();
}
}
}

View File

@@ -1 +1,28 @@
// 1.12.2 and below
package com.replaymod.compat.oranges17animations;
import com.replaymod.replay.camera.CameraEntity;
import cpw.mods.fml.common.Loader;
import cpw.mods.fml.common.eventhandler.EventPriority;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import net.minecraft.client.Minecraft;
import net.minecraftforge.client.event.RenderLivingEvent;
/**
* Orange seems to have copied vast parts of the RendererLivingEntity into their ArmorAnimation class which cancels the RenderLivingEvent.Pre and calls its own code instead.
* This breaks our mixin which assures that, even though the camera is in spectator mode, it cannot see invisible entities.
*
* To fix this issue, we simply cancel the RenderLivingEvent.Pre before it gets to ArmorAnimation if the entity is invisible.
*/
public class HideInvisibleEntities {
private final Minecraft mc = Minecraft.getMinecraft();
private final boolean modLoaded = Loader.isModLoaded("animations");
@SubscribeEvent(priority = EventPriority.HIGH)
public void preRenderLiving(RenderLivingEvent.Pre event) {
if (modLoaded) {
if (mc.thePlayer instanceof CameraEntity && event.entity.isInvisible()) {
event.setCanceled(true);
}
}
}
}

View File

@@ -1,44 +0,0 @@
//#if MC>=10800
package com.replaymod.compat.shaders;
import com.replaymod.core.events.PreRenderCallback;
import com.replaymod.render.hooks.EntityRendererHandler;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import net.minecraft.client.MinecraftClient;
import java.lang.reflect.InvocationTargetException;
public class ShaderBeginRender extends EventRegistrations {
private final MinecraftClient mc = MinecraftClient.getInstance();
/**
* Invokes Shaders#beginRender when rendering a video,
* as this would usually get called by EntityRenderer#renderWorld,
* which we're not calling during rendering.
*/
{ on(PreRenderCallback.EVENT, this::onRenderTickStart); }
private void onRenderTickStart() {
if (ShaderReflection.shaders_beginRender == null) return;
if (ShaderReflection.config_isShaders == null) return;
try {
// check if video is being rendered
if (((EntityRendererHandler.IEntityRenderer) mc.gameRenderer).replayModRender_getHandler() == null)
return;
// check if Shaders are enabled
if (!(boolean) (ShaderReflection.config_isShaders.invoke(null))) return;
ShaderReflection.shaders_beginRender.invoke(null, mc,
//#if MC>=11400
mc.gameRenderer.getCamera(),
//#endif
mc.getTickDelta(), 0);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
//#endif

View File

@@ -1,66 +0,0 @@
//#if MC>=10800
package com.replaymod.compat.shaders;
import net.minecraft.client.MinecraftClient;
//#if MC>=11400
import net.minecraft.client.render.Camera;
//#endif
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ShaderReflection {
// Shaders.frameTimeCounter
public static Field shaders_frameTimeCounter;
// Shaders.isShadowPass
public static Field shaders_isShadowPass;
// Shaders.beginRender()
public static Method shaders_beginRender;
// RenderGlobal.chunksToUpdateForced (Optifine only)
public static Field renderGlobal_chunksToUpdateForced;
// Config.isShaders() (Optifine only)
public static Method config_isShaders;
static {
try {
Class<?> shadersClass;
try {
shadersClass = Class.forName("shadersmod.client.Shaders"); // Pre Optifine 1.12.2 E1
} catch (ClassNotFoundException ignore) {
shadersClass = Class.forName("net.optifine.shaders.Shaders"); // Post Optifine 1.12.2 E1
}
shaders_frameTimeCounter = shadersClass.getDeclaredField("frameTimeCounter");
shaders_frameTimeCounter.setAccessible(true);
shaders_isShadowPass = shadersClass.getDeclaredField("isShadowPass");
shaders_isShadowPass.setAccessible(true);
shaders_beginRender = shadersClass.getDeclaredMethod("beginRender", MinecraftClient.class,
//#if MC>=11400
Camera.class,
//#endif
float.class, long.class);
shaders_beginRender.setAccessible(true);
renderGlobal_chunksToUpdateForced = Class.forName("net.minecraft.client.renderer.RenderGlobal")
.getDeclaredField("chunksToUpdateForced");
renderGlobal_chunksToUpdateForced.setAccessible(true);
config_isShaders = Class.forName("Config")
.getDeclaredMethod("isShaders");
config_isShaders.setAccessible(true);
} catch (ClassNotFoundException ignore) {
// no shaders mod installed
} catch (NoSuchMethodException | NoSuchFieldException e) {
// the method wasn't found. Has it been renamed?
e.printStackTrace();
}
}
}
//#endif

View File

@@ -1,32 +0,0 @@
//#if MC>=11500
package com.replaymod.compat.shaders.mixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
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;
@Pseudo
@Mixin(targets = "net/optifine/render/ChunkVisibility", remap = false)
public abstract class MixinChunkVisibility {
@Shadow
private static int counter;
/**
* OF doesn't properly reset the counter when exiting a world.
* It'll only be reset when getMaxChunkY is called which only happens for
* SP worlds (i.e. not in a replay or MP).
* As a result, it may end up in a non-0 state, which will cause isFinished
* to unconditionally return false, therefore unconditionally setting
* needsTerrainUpdate to true on each call to WorldRenderer.setupTerrain,
* therefore unnecessarily consuming resources and live-locking when
* rendering the shader pass.
*/
@Inject(method = "reset", at = @At("HEAD"), remap = false)
private static void replayModCompat_fixImproperReset(CallbackInfo ci) {
MixinChunkVisibility.counter = 0;
}
}
//#endif

View File

@@ -1,38 +0,0 @@
//#if MC>=10800
package com.replaymod.compat.shaders.mixin;
import com.replaymod.compat.shaders.ShaderReflection;
import com.replaymod.replay.ReplayHandler;
import com.replaymod.replay.ReplayModReplay;
import net.minecraft.client.render.GameRenderer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(GameRenderer.class)
public abstract class MixinShaderEntityRenderer {
//#if MC>=11400
@Inject(method = "renderWorld", at = @At("HEAD"))
//#else
//#if MC>=11400
//$$ @Inject(method = "updateCameraAndRender(FJ)V", at = @At("HEAD"))
//#else
//$$ @Inject(method = "renderWorldPass", at = @At("HEAD"))
//#endif
//#endif
private void replayModCompat_updateShaderFrameTimeCounter(CallbackInfo ignore) {
if (ReplayModReplay.instance.getReplayHandler() == null) return;
if (ShaderReflection.shaders_frameTimeCounter == null) return;
ReplayHandler replayHandler = ReplayModReplay.instance.getReplayHandler();
float timestamp = replayHandler.getReplaySender().currentTimeStamp() / 1000f % 3600f;
try {
ShaderReflection.shaders_frameTimeCounter.set(null, timestamp);
} catch (Exception e) {
e.printStackTrace();
}
}
}
//#endif

View File

@@ -1,71 +0,0 @@
//#if MC>=10800 && MC<11800
package com.replaymod.compat.shaders.mixin;
import com.replaymod.render.hooks.EntityRendererHandler;
import net.minecraft.client.MinecraftClient;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
//#if MC>=11500
import org.spongepowered.asm.mixin.Shadow;
//#endif
//#if MC>=11500
@Mixin(net.minecraft.client.render.chunk.ChunkBuilder.BuiltChunk.class)
//#else
//$$ @Mixin(net.minecraft.client.render.chunk.ChunkRenderer.class)
//#endif
public abstract class MixinShaderRenderChunk {
private final MinecraftClient mc = MinecraftClient.getInstance();
//#if MC>=11500
@Shadow private int rebuildFrame;
@Shadow public abstract boolean shouldBuild();
/**
* So, for some reason OF though it'd be a good idea to drop the `shouldBuild` check from the chunk traversal in
* the setupTerrain method (see the innermost if of the "iteration" profiler section).
* Hint: It's not a good idea. It'll cause chunks to be queued for building which should not be built. And because
* they don't know any better, they'll even re-queue themselves (which looks like a race condition in vanilla
* MC) causing a live-lock when we try to force-build all chunks (or at least it would cause a lockup if it
* wasn't for the vanilla race condition which slowly causes some of the rebuild tasks to get lost over time,
* eventually, potentially all of them).
* This is the most convenient place to re-introduce the check (setRebuildFrame would normally be called right after
* shouldBuild, if it returns true).
*/
@Inject(method = "setRebuildFrame", at = @At("HEAD"), cancellable = true)
private void replayModCompat_OFHaveYouConsideredWhetherThisChunkShouldEvenBeBuilt(int rebuildFrame, CallbackInfoReturnable<Boolean> ci) {
if (this.rebuildFrame == rebuildFrame) {
// want to keep the fast path
ci.setReturnValue(false);
} else if (!this.shouldBuild()) {
// this is the check which OF removed
// So, I think I figured out the reason why optifine applies this change: It's so chunks which are outside
// the server view distance are still rendered if they've previously compiled. The bug still stands but
// fixing it breaks OF's broken feature, so to reduce the impact of our fix, we only apply it during
// rendering where it affects us the most.
if (((EntityRendererHandler.IEntityRenderer) mc.gameRenderer).replayModRender_getHandler() == null) return;
ci.setReturnValue(false);
}
}
//#endif
/**
* Changes the RenderChunk#isPlayerUpdate method that Optifine adds
* to always return true while rendering so no chunks are being added
* to a separate rendering queue
*/
@Inject(method = "isPlayerUpdate", at = @At("HEAD"), cancellable = true, remap = false)
private void replayModCompat_disableIsPlayerUpdate(CallbackInfoReturnable<Boolean> ci) {
if (((EntityRendererHandler.IEntityRenderer) mc.gameRenderer).replayModRender_getHandler() == null) return;
ci.setReturnValue(true);
}
}
//#endif

View File

@@ -1 +1,50 @@
// 1.7.10 and below
package com.replaymod.compat.shaders.mixin;
import com.replaymod.render.hooks.EntityRendererHandler;
import com.replaymod.replay.ReplayHandler;
import com.replaymod.replay.ReplayModReplay;
import net.minecraft.client.Minecraft;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Pseudo
@Mixin(targets = "shadersmod/client/Shaders", remap = false)
public abstract class MixinShaders {
@Shadow static Minecraft mc;
@Shadow static long systemTime;
@Shadow static long lastSystemTime;
@Shadow static long diffSystemTime;
@Shadow static int frameCounter;
@Shadow static float frameTime;
@Shadow static float frameTimeCounter;
@Redirect(method = "beginRender", at = @At(value = "INVOKE", target = "Ljava/lang/System;currentTimeMillis()J"))
private static long replayModCompat_currentTimeMillis() {
ReplayHandler replayHandler = ReplayModReplay.instance.getReplayHandler();
if (replayHandler != null) {
systemTime = replayHandler.getReplaySender().currentTimeStamp();
// We need to manipulate all the other previous-frame-time-based variables too
lastSystemTime = 0; // Draw all frames as if they were the first ones
// diffSystemTime will be set to 0 by Shaders
// frameTime will be set to 0 by Shaders
frameTimeCounter = systemTime / 1000f; // will be %= 3600f by Shaders
// Set frameCounter only if rendering is in progress
EntityRendererHandler entityRendererHandler =
((EntityRendererHandler.IEntityRenderer) mc.entityRenderer).replayModRender_getHandler();
if (entityRendererHandler != null) {
frameCounter = entityRendererHandler.getRenderInfo().getFramesDone();
}
return systemTime;
}
return System.currentTimeMillis();
}
}

View File

@@ -1,52 +1,20 @@
package com.replaymod.compat.shaders.mixin;
import net.minecraft.client.render.GameRenderer;
import net.minecraft.client.renderer.EntityRenderer;
import net.minecraftforge.client.ForgeHooksClient;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
//#if MC>=11600
import net.minecraft.client.render.Camera;
import net.minecraft.client.util.math.MatrixStack;
//#endif
//#if MC>=11400
import com.replaymod.core.events.PreRenderHandCallback;
//#else
//$$ import com.replaymod.core.versions.MCVer;
//$$ import net.minecraftforge.client.ForgeHooksClient;
//#endif
@Pseudo
@Mixin(targets = {
"shadersmod/client/ShadersRender", // Pre Optifine 1.12.2 E1
"net/optifine/shaders/ShadersRender" // Post Optifine 1.12.2 E1
}, remap = false)
@Mixin(targets = "shadersmod/client/ShadersRender", remap = false)
public abstract class MixinShadersRender {
@Inject(method = { "renderHand0", "renderHand1" }, at = @At("HEAD"), cancellable = true, remap = false)
private static void replayModCompat_disableRenderHand0(
GameRenderer er,
//#if MC>=11600
MatrixStack stack,
Camera camera,
//#endif
float partialTicks,
//#if MC<11600
//$$ int renderPass,
//#endif
CallbackInfo ci) {
//#if MC>=11400
if (PreRenderHandCallback.EVENT.invoker().preRenderHand()) {
//#else
//#if MC>=11400
//$$ if (ForgeHooksClient.renderFirstPersonHand(MCVer.getMinecraft().renderGlobal, partialTicks)) {
//#else
//$$ if (ForgeHooksClient.renderFirstPersonHand(MCVer.getMinecraft().renderGlobal, partialTicks, renderPass)) {
//#endif
//#endif
@Inject(method = "renderHand0", at = @At("HEAD"), cancellable = true)
private static void replayModCompat_disableRenderHand0(EntityRenderer er, float partialTicks, int renderPass, CallbackInfo ci) {
if (ForgeHooksClient.renderFirstPersonHand(er.mc.renderGlobal, partialTicks, renderPass)) {
ci.cancel();
}
}

View File

@@ -1,18 +0,0 @@
//#if FABRIC>=1
package com.replaymod.core;
import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint;
// Required for ReplayModMMLauncher.
//
// Chain-loading mixin configurations only works when the first class that is loaded doesn't have any mixins which
// target it. The first class would ordinarily be MC's Main but there are valid use cases for targeting it, see
// e.g. https://github.com/ReplayMod/ReplayMod/issues/327
// So, instead of relying on the bad assumption that Main doesn't have any mixins, we'll instead register this
// dummy pre-launch entry point which is practically guaranteed to not have any mixins and gets called before Main.
public class DummyChainLoadEntryPoint implements PreLaunchEntrypoint {
@Override
public void onPreLaunch() {
}
}
//#endif

View File

@@ -1,4 +1,4 @@
package com.replaymod.core.asm;
package com.replaymod.core;
import net.minecraft.launchwrapper.IClassTransformer;
import org.lwjgl.opengl.GL11;
@@ -24,7 +24,8 @@ public class GLErrorTransformer implements IClassTransformer {
@Override
public byte[] transform(String name, String transformedName, byte[] basicClass) {
if (basicClass == null) return null;
// Ignore (anonymous) inner classes of this transformer
if (name.startsWith(GLErrorTransformer_CLASS)) return basicClass;
ClassReader reader = new ClassReader(basicClass);
ClassWriter writer = new ClassWriter(reader, 0);

View File

@@ -1,4 +1,4 @@
package com.replaymod.core.asm;
package com.replaymod.core;
import net.minecraft.launchwrapper.IClassTransformer;
import org.objectweb.asm.ClassReader;
@@ -25,7 +25,8 @@ public class GLStateTrackerTransformer implements IClassTransformer {
@Override
public byte[] transform(String name, String transformedName, byte[] basicClass) {
if (basicClass == null) return null;
// Ignore (anonymous) inner classes of this transformer
if (name.startsWith(GLStateTrackerTransformer.class.getName())) return basicClass;
// Ignore the state tracker itself
if (name.equals(GLStateTracker_CLASS)) return basicClass;

View File

@@ -2,221 +2,113 @@ package com.replaymod.core;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import com.replaymod.core.events.PreRenderCallback;
import com.replaymod.core.mixin.KeyBindingAccessor;
import de.johni0702.minecraft.gui.function.KeyInput;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import com.replaymod.core.events.KeyBindingEventCallback;
import com.replaymod.core.events.KeyEventCallback;
import net.minecraft.client.options.KeyBinding;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.crash.CrashReportSection;
import net.minecraft.util.crash.CrashException;
import cpw.mods.fml.client.registry.ClientRegistry;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.gameevent.InputEvent;
import cpw.mods.fml.common.gameevent.TickEvent;
import net.minecraft.client.settings.KeyBinding;
import net.minecraft.crash.CrashReport;
import net.minecraft.crash.CrashReportCategory;
import net.minecraft.util.ReportedException;
import org.lwjgl.input.Keyboard;
//#if FABRIC>=1
import com.replaymod.core.versions.LangResourcePack;
//#if MC>=11600
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper;
//#else
//$$ import net.fabricmc.fabric.api.client.keybinding.FabricKeyBinding;
//#endif
import net.minecraft.client.util.InputUtil;
import net.minecraft.util.Identifier;
import static com.replaymod.core.ReplayMod.MOD_ID;
import static de.johni0702.minecraft.gui.versions.MCVer.identifier;
//#else
//$$ import net.minecraftforge.fml.client.registry.ClientRegistry;
//#endif
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.concurrent.Callable;
public class KeyBindingRegistry extends EventRegistrations {
//#if MC>=12109
//$$ private static final KeyBinding.Category CATEGORY = KeyBinding.Category.create(identifier(MOD_ID, "general"));
//#else
private static final String CATEGORY = "replaymod.title";
//#if FABRIC>=1 && MC<11600
//$$ static { net.fabricmc.fabric.api.client.keybinding.KeyBindingRegistry.INSTANCE.addCategory(CATEGORY); }
//#endif
//#endif
public class KeyBindingRegistry {
private Map<String, KeyBinding> keyBindings = new HashMap<String, KeyBinding>();
private Multimap<KeyBinding, Runnable> keyBindingHandlers = ArrayListMultimap.create();
private Multimap<KeyBinding, Runnable> repeatedKeyBindingHandlers = ArrayListMultimap.create();
private Multimap<Integer, Runnable> rawHandlers = ArrayListMultimap.create();
private final Map<String, Binding> bindings = new HashMap<>();
private Set<KeyBinding> onlyInReplay = new HashSet<>();
private Multimap<Integer, Function<KeyInput, Boolean>> rawHandlers = ArrayListMultimap.create();
public Binding registerKeyBinding(String name, int keyCode, Runnable whenPressed, boolean onlyInRepay) {
Binding binding = registerKeyBinding(name, keyCode, onlyInRepay);
binding.handlers.add(whenPressed);
return binding;
public void registerKeyBinding(String name, int keyCode, Runnable whenPressed) {
keyBindingHandlers.put(registerKeyBinding(name, keyCode), whenPressed);
}
public Binding registerRepeatedKeyBinding(String name, int keyCode, Runnable whenPressed, boolean onlyInRepay) {
Binding binding = registerKeyBinding(name, keyCode, onlyInRepay);
binding.repeatedHandlers.add(whenPressed);
return binding;
public void registerRepeatedKeyBinding(String name, int keyCode, Runnable whenPressed) {
repeatedKeyBindingHandlers.put(registerKeyBinding(name, keyCode), whenPressed);
}
private Binding registerKeyBinding(String name, int keyCode, boolean onlyInRepay) {
Binding binding = bindings.get(name);
if (binding == null) {
//#if FABRIC>=1
if (keyCode == 0) {
keyCode = -1;
}
Identifier id = identifier(MOD_ID, name.substring(LangResourcePack.LEGACY_KEY_PREFIX.length()));
//#if MC>=11600
String key = String.format("key.%s.%s", id.getNamespace(), id.getPath());
KeyBinding keyBinding = new KeyBinding(key, InputUtil.Type.KEYSYM, keyCode, CATEGORY);
KeyBindingHelper.registerKeyBinding(keyBinding);
//#else
//$$ FabricKeyBinding fabricKeyBinding = FabricKeyBinding.Builder.create(id, InputUtil.Type.KEYSYM, keyCode, CATEGORY).build();
//$$ net.fabricmc.fabric.api.client.keybinding.KeyBindingRegistry.INSTANCE.register(fabricKeyBinding);
//$$ KeyBinding keyBinding = fabricKeyBinding;
//#endif
//#else
//$$ KeyBinding keyBinding = new KeyBinding(name, keyCode, CATEGORY);
//$$ ClientRegistry.registerKeyBinding(keyBinding);
//#endif
binding = new Binding(name, keyBinding);
bindings.put(name, binding);
if (onlyInRepay) {
this.onlyInReplay.add(keyBinding);
}
} else if (!onlyInRepay) {
this.onlyInReplay.remove(binding.keyBinding);
private KeyBinding registerKeyBinding(String name, int keyCode) {
KeyBinding keyBinding = keyBindings.get(name);
if (keyBinding == null) {
keyBinding = new KeyBinding(name, keyCode, "replaymod.title");
keyBindings.put(name, keyBinding);
ClientRegistry.registerKeyBinding(keyBinding);
}
return binding;
return keyBinding;
}
public void registerRaw(int keyCode, Function<KeyInput, Boolean> whenPressed) {
public void registerRaw(int keyCode, Runnable whenPressed) {
rawHandlers.put(keyCode, whenPressed);
}
public Map<String, Binding> getBindings() {
return Collections.unmodifiableMap(bindings);
public Map<String, KeyBinding> getKeyBindings() {
return Collections.unmodifiableMap(keyBindings);
}
public Set<KeyBinding> getOnlyInReplay() {
return Collections.unmodifiableSet(onlyInReplay);
@SubscribeEvent
public void onKeyInput(InputEvent.KeyInputEvent event) {
handleKeyBindings();
handleRaw();
}
{ on(PreRenderCallback.EVENT, this::handleRepeatedKeyBindings); }
@SubscribeEvent
public void onTick(TickEvent.RenderTickEvent event) {
if (event.phase != TickEvent.Phase.START) return;
handleRepeatedKeyBindings();
}
public void handleRepeatedKeyBindings() {
for (Binding binding : bindings.values()) {
if (binding.keyBinding.isPressed()) {
invokeKeyBindingHandlers(binding, binding.repeatedHandlers);
for (Map.Entry<KeyBinding, Collection<Runnable>> entry : repeatedKeyBindingHandlers.asMap().entrySet()) {
if (entry.getKey().getIsKeyPressed()) {
invokeKeyBindingHandlers(entry.getKey(), entry.getValue());
}
}
}
{ on(KeyBindingEventCallback.EVENT, this::handleKeyBindings); }
private void handleKeyBindings() {
for (Binding binding : bindings.values()) {
while (binding.keyBinding.wasPressed()) {
invokeKeyBindingHandlers(binding, binding.handlers);
invokeKeyBindingHandlers(binding, binding.repeatedHandlers);
public void handleKeyBindings() {
for (Map.Entry<KeyBinding, Collection<Runnable>> entry : keyBindingHandlers.asMap().entrySet()) {
while (entry.getKey().isPressed()) {
invokeKeyBindingHandlers(entry.getKey(), entry.getValue());
}
}
}
private void invokeKeyBindingHandlers(Binding binding, Collection<Runnable> handlers) {
private void invokeKeyBindingHandlers(KeyBinding keyBinding, Collection<Runnable> handlers) {
for (final Runnable runnable : handlers) {
try {
runnable.run();
} catch (Throwable cause) {
CrashReport crashReport = CrashReport.create(cause, "Handling Key Binding");
CrashReportSection category = crashReport.addElement("Key Binding");
category.add("Key Binding", () -> binding.name);
category.add("Handler", runnable::toString);
throw new CrashException(crashReport);
CrashReport crashReport = CrashReport.makeCrashReport(cause, "Handling Key Binding");
CrashReportCategory category = crashReport.makeCategory("Key Binding");
category.addCrashSection("Key Binding", keyBinding);
category.addCrashSectionCallable("Handler", runnable::toString);
throw new ReportedException(crashReport);
}
}
}
{ on(KeyEventCallback.EVENT, this::handleRaw); }
private boolean handleRaw(KeyInput keyInput, int action) {
if (action != KeyEventCallback.ACTION_PRESS) return false;
for (final Function<KeyInput, Boolean> handler : rawHandlers.get(keyInput.key)) {
public void handleRaw() {
int keyCode = Keyboard.getEventKey() == 0 ? Keyboard.getEventCharacter() + 256 : Keyboard.getEventKey();
for (final Runnable runnable : rawHandlers.get(keyCode)) {
try {
if (handler.apply(keyInput)) {
return true;
}
runnable.run();
} catch (Throwable cause) {
CrashReport crashReport = CrashReport.create(cause, "Handling Raw Key Binding");
CrashReportSection category = crashReport.addElement("Key Binding");
category.add("Key Code", () -> "" + keyInput.key);
category.add("Handler", handler::toString);
throw new CrashException(crashReport);
CrashReport crashReport = CrashReport.makeCrashReport(cause, "Handling Raw Key Binding");
CrashReportCategory category = crashReport.makeCategory("Key Binding");
category.addCrashSection("Key Code", keyCode);
category.addCrashSectionCallable("Handler", new Callable() {
@Override
public Object call() throws Exception {
return runnable;
}
});
throw new ReportedException(crashReport);
}
}
return false;
}
public class Binding {
public final String name;
public final KeyBinding keyBinding;
private final List<Runnable> handlers = new ArrayList<>();
private final List<Runnable> repeatedHandlers = new ArrayList<>();
private boolean autoActivation;
private Consumer<Boolean> autoActivationUpdate;
public Binding(String name, KeyBinding keyBinding) {
this.name = name;
this.keyBinding = keyBinding;
}
public String getBoundKey() {
try {
return keyBinding.getBoundKeyLocalizedText().getString();
} catch (ArrayIndexOutOfBoundsException e) {
// Apparently windows likes to press strange keys, see https://www.replaymod.com/forum/thread/55
return "Unknown";
}
}
public boolean isBound() {
//#if MC>=11400
return !keyBinding.isUnbound();
//#else
//$$ return keyBinding.getKeyCode() != 0;
//#endif
}
public void trigger() {
KeyBindingAccessor acc = (KeyBindingAccessor) keyBinding;
acc.setPressTime(acc.getPressTime() + 1);
handleKeyBindings();
}
public void registerAutoActivationSupport(boolean active, Consumer<Boolean> update) {
this.autoActivation = active;
this.autoActivationUpdate = update;
}
public boolean supportsAutoActivation() {
return autoActivationUpdate != null;
}
public boolean isAutoActivating() {
return supportsAutoActivation() && autoActivation;
}
public void setAutoActivating(boolean active) {
if (this.autoActivation == active) {
return;
}
this.autoActivation = active;
this.autoActivationUpdate.accept(active);
}
}
}

77
src/main/java/com/replaymod/core/LoadingPlugin.java Normal file → Executable file
View File

@@ -1 +1,76 @@
// 1.12.2 and below
package com.replaymod.core;
import cpw.mods.fml.relauncher.CoreModManager;
import cpw.mods.fml.relauncher.IFMLLoadingPlugin;
import org.apache.logging.log4j.LogManager;
import org.spongepowered.asm.launch.MixinBootstrap;
import org.spongepowered.asm.mixin.Mixins;
import java.io.File;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class LoadingPlugin implements IFMLLoadingPlugin {
public LoadingPlugin() {
MixinBootstrap.init();
Mixins.addConfiguration("mixins.recording.replaymod.json");
Mixins.addConfiguration("mixins.render.replaymod.json");
Mixins.addConfiguration("mixins.replay.replaymod.json");
Mixins.addConfiguration("mixins.compat.shaders.replaymod.json");
Mixins.addConfiguration("mixins.extras.playeroverview.replaymod.json");
CodeSource codeSource = getClass().getProtectionDomain().getCodeSource();
if (codeSource != null) {
URL location = codeSource.getLocation();
try {
File file = new File(location.toURI());
if (file.isFile()) {
// This forces forge to reexamine the jar file for FML mods
// Should eventually be handled by Mixin itself, maybe?
CoreModManager.getLoadedCoremods().remove(file.getName());
CoreModManager.getReparseableCoremods().add(file.getName());
}
} catch (URISyntaxException e) {
e.printStackTrace();
}
} else {
LogManager.getLogger().warn("No CodeSource, if this is not a development environment we might run into problems!");
LogManager.getLogger().warn(getClass().getProtectionDomain());
}
}
@Override
public String[] getASMTransformerClass() {
List<String> transformers = new ArrayList<>();
if ("true".equals(System.getProperty("replaymod.glerrors", "false"))) {
transformers.add(GLErrorTransformer.class.getName());
}
transformers.add(GLStateTrackerTransformer.class.getName());
return transformers.stream().toArray(String[]::new);
}
@Override
public String getModContainerClass() {
return null;
}
@Override
public String getSetupClass() {
return null;
}
@Override
public void injectData(Map<String, Object> data) {
}
@Override
public String getAccessTransformerClass() {
return null;
}
}

View File

@@ -1,14 +0,0 @@
//#if FABRIC>=1
package com.replaymod.core;
import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint;
public class MixinExtrasInit implements PreLaunchEntrypoint {
@Override
public void onPreLaunch() {
//#if MC>=11604
com.llamalad7.mixinextras.MixinExtrasBootstrap.init();
//#endif
}
}
//#endif

View File

@@ -1,10 +0,0 @@
package com.replaymod.core;
public interface Module {
// FMLCommonSetupEvent for 1.13+, FMLInitializationEvent below
default void initCommon() {}
// FMLClientSetupEvent for 1.13+, FMLInitializationEvent (if client) below
default void initClient() {}
// FMLClientSetupEvent for 1.13+, FMLInitializationEvent below
default void registerKeyBindings(KeyBindingRegistry registry) {}
}

571
src/main/java/com/replaymod/core/ReplayMod.java Normal file → Executable file
View File

@@ -1,116 +1,92 @@
package com.replaymod.core;
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.google.common.io.Files;
import com.google.common.util.concurrent.ListenableFutureTask;
import com.replaymod.core.gui.GuiReplaySettings;
import com.replaymod.core.versions.MCVer;
import com.replaymod.core.versions.scheduler.Scheduler;
import com.replaymod.core.versions.scheduler.SchedulerImpl;
import com.replaymod.editor.ReplayModEditor;
import com.replaymod.extras.ReplayModExtras;
import com.replaymod.recording.ReplayModRecording;
import com.replaymod.render.ReplayModRender;
import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replaystudio.lib.viaversion.api.protocol.version.ProtocolVersion;
import com.replaymod.replaystudio.studio.ReplayStudio;
import com.replaymod.core.gui.RestoreReplayGui;
import com.replaymod.core.handler.MainMenuHandler;
import com.replaymod.core.utils.OpenGLUtils;
import com.replaymod.render.utils.SoundHandler;
import com.replaymod.replay.InputReplayTimer;
import com.replaymod.replaystudio.util.I18n;
import com.replaymod.simplepathing.ReplayModSimplePathing;
import net.minecraft.client.MinecraftClient;
import net.minecraft.resource.DirectoryResourcePack;
import net.minecraft.text.LiteralText;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.text.TranslatableText;
import net.minecraft.util.Formatting;
import net.minecraft.util.Identifier;
import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.Loader;
import cpw.mods.fml.common.Mod;
import cpw.mods.fml.common.Mod.EventHandler;
import cpw.mods.fml.common.Mod.Instance;
import cpw.mods.fml.common.ModContainer;
import cpw.mods.fml.common.event.FMLInitializationEvent;
import cpw.mods.fml.common.event.FMLPostInitializationEvent;
import cpw.mods.fml.common.event.FMLPreInitializationEvent;
import cpw.mods.fml.common.eventhandler.EventBus;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.gameevent.TickEvent;
import de.johni0702.minecraft.gui.container.GuiScreen;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import net.minecraft.client.Minecraft;
import net.minecraft.crash.CrashReport;
import net.minecraft.crash.CrashReportCategory;
import net.minecraft.util.ChatComponentText;
import net.minecraft.util.ChatComponentTranslation;
import net.minecraft.util.ChatStyle;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.IChatComponent;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.common.config.Configuration;
import org.apache.commons.io.FileUtils;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.Queue;
import static de.johni0702.minecraft.gui.versions.MCVer.identifier;
@Mod(modid = ReplayMod.MOD_ID,
useMetadata = true,
version = "@MOD_VERSION@",
acceptedMinecraftVersions = "@MC_VERSION@",
acceptableRemoteVersions = "*",
guiFactory = "com.replaymod.core.gui.GuiFactory")
public class ReplayMod {
//#if MC>=12006
//$$ import net.minecraft.resource.ResourcePackInfo;
//$$ import net.minecraft.resource.ResourcePackSource;
//$$ import net.minecraft.text.Text;
//$$ import java.util.Optional;
//#endif
public static ModContainer getContainer() {
return Loader.instance().getIndexedModList().get(MOD_ID);
}
//#if MC>=11900
//#else
import net.minecraft.client.options.Option;
//#endif
public class ReplayMod implements Module, Scheduler {
@Getter(lazy = true)
private static final String minecraftVersion = parseMinecraftVersion();
private static String parseMinecraftVersion() {
CrashReport crashReport = new CrashReport("", new Throwable());
@SuppressWarnings("unchecked")
List<CrashReportCategory.Entry> list = crashReport.getCategory().field_85077_c;
for (CrashReportCategory.Entry entry : list) {
if ("Minecraft Version".equals(entry.func_85089_a())) {
return entry.func_85090_b();
}
}
return "Unknown";
}
public static final String MOD_ID = "replaymod";
public static final Identifier TEXTURE = identifier("replaymod", "replay_gui.png");
public static final ResourceLocation TEXTURE = new ResourceLocation("replaymod", "replay_gui.png");
public static final int TEXTURE_SIZE = 256;
public static final Identifier LOGO_FAVICON = identifier("replaymod", "favicon_logo.png");
private static final MinecraftClient mc = MCVer.getMinecraft();
private static final Minecraft mc = Minecraft.getMinecraft();
@Deprecated
public static Configuration config;
@Deprecated
public static SoundHandler soundHandler = new SoundHandler();
private final ReplayModBackend backend;
private final SchedulerImpl scheduler = new SchedulerImpl();
private final KeyBindingRegistry keyBindingRegistry = new KeyBindingRegistry();
private final SettingsRegistry settingsRegistry = new SettingsRegistry();
{
settingsRegistry.register(Setting.class);
}
{ instance = this; }
// The instance of your mod that Forge uses.
@Instance(MOD_ID)
public static ReplayMod instance;
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.
* If this is not the case (i.e. if this is variable true), any feature of the RM which depends on the ReplayStudio
* lib will be disabled.
*
* Only supported on Fabric builds, i.e. will always be false / crash the game with Forge/pre-1.14 builds.
* (specifically the code below and MCVer#getProtocolVersion make this assumption)
*/
private boolean minimalMode;
public ReplayMod(ReplayModBackend backend) {
this.backend = backend;
I18n.setI18n(net.minecraft.client.resource.language.I18n::translate);
// Check Minecraft protocol version for compatibility
if (!ProtocolVersion.isRegistered(MCVer.getProtocolVersion()) && !Boolean.parseBoolean(System.getProperty("replaymod.skipversioncheck", "false"))) {
minimalMode = true;
}
// Register all RM modules
modules.add(this);
modules.add(new ReplayModRecording(this));
ReplayModReplay replayModule = new ReplayModReplay(this);
modules.add(replayModule);
modules.add(new ReplayModRender(this));
modules.add(new ReplayModSimplePathing(this));
modules.add(new ReplayModEditor(this));
modules.add(new ReplayModExtras(this));
modules.add(new ReplayModCompat());
settingsRegistry.register();
}
public KeyBindingRegistry getKeyBindingRegistry() {
return keyBindingRegistry;
}
@@ -119,133 +95,289 @@ public class ReplayMod implements Module, Scheduler {
return settingsRegistry;
}
public static final DirectoryResourcePack jGuiResourcePack = createJGuiResourcePack();
public static final String JGUI_RESOURCE_PACK_NAME = "replaymod_jgui";
private static DirectoryResourcePack createJGuiResourcePack() {
File folder = new File("../jGui/src/main/resources");
if (!folder.exists()) {
folder = new File("../../../jGui/src/main/resources");
if (!folder.exists()) {
return null;
}
}
//#if MC>=12006
//$$ return new DirectoryResourcePack(new ResourcePackInfo(JGUI_RESOURCE_PACK_NAME, Text.literal("jGui"), ResourcePackSource.NONE, Optional.empty()), folder.toPath()) {
//#elseif MC>=11903
//$$ return new DirectoryResourcePack(JGUI_RESOURCE_PACK_NAME, folder.toPath(), true) {
//#else
return new DirectoryResourcePack(folder) {
//#endif
@Override
//#if MC>=11400
public String getName() {
//#else
//$$ public String getPackName() {
//#endif
return JGUI_RESOURCE_PACK_NAME;
public File getReplayFolder() throws IOException {
String path = getSettingsRegistry().get(Setting.RECORDING_PATH);
File folder = new File(path.startsWith("./") ? getMinecraft().mcDataDir : null, path);
FileUtils.forceMkdir(folder);
return folder;
}
@EventHandler
public void preInit(FMLPreInitializationEvent event) {
// Initialize the static OpenGL info field from the minecraft main thread
// Unfortunately lwjgl uses static methods so we have to make use of magic init calls as well
OpenGLUtils.init();
I18n.setI18n(net.minecraft.client.resources.I18n::format);
config = new Configuration(event.getSuggestedConfigurationFile());
config.load();
settingsRegistry.setConfiguration(config);
}
@EventHandler
public void init(FMLInitializationEvent event) {
getSettingsRegistry().register(Setting.class);
new MainMenuHandler().register();
FMLCommonHandler.instance().bus().register(this);
FMLCommonHandler.instance().bus().register(keyBindingRegistry);
getKeyBindingRegistry().registerKeyBinding("replaymod.input.settings", 0, () -> {
new GuiReplaySettings(null, settingsRegistry).display();
});
}
@EventHandler
public void postInit(FMLPostInitializationEvent event) throws IOException {
settingsRegistry.save(); // Save default values to disk
/* TODO 1.7.10: MC crashes when setting render distance to > 16
if(!FMLClientHandler.instance().hasOptifine())
GameSettings.Options.RENDER_DISTANCE.setValueMax(64f);
*/
if (System.getProperty("replaymod.render.file") != null) {
final File file = new File(System.getProperty("replaymod.render.file"));
if (!file.exists()) {
throw new IOException("File \"" + file.getPath() + "\" not found.");
}
//#if MC>=11903
//$$ @Override
//$$ public net.minecraft.resource.InputSupplier<InputStream> openRoot(String... segments) {
//$$ if (segments.length == 1 && segments[0].equals("pack.mcmeta")) {
//$$ return () -> new ByteArrayInputStream(generatePackMeta());
//$$ }
//$$ return super.openRoot(segments);
//$$ }
//#else
@Override
protected InputStream openFile(String resourceName) throws IOException {
try {
return super.openFile(resourceName);
} catch (IOException e) {
if ("pack.mcmeta".equals(resourceName)) {
return new ByteArrayInputStream(generatePackMeta());
}
throw e;
final String path = System.getProperty("replaymod.render.path");
String type = System.getProperty("replaymod.render.type");
String bitrate = System.getProperty("replaymod.render.bitrate");
String fps = System.getProperty("replaymod.render.fps");
String waitForChunks = System.getProperty("replaymod.render.waitforchunks");
String linearMovement = System.getProperty("replaymod.render.linearmovement");
String skyColor = System.getProperty("replaymod.render.skycolor");
String width = System.getProperty("replaymod.render.width");
String height = System.getProperty("replaymod.render.height");
String exportCommand = System.getProperty("replaymod.render.exportcommand");
String exportCommandArgs = System.getProperty("replaymod.render.exportcommandargs");
/*
final RenderOptions options = new RenderOptions();
if (bitrate != null) {
options.setBitrate(bitrate);
}
if (fps != null) {
options.setFps(Integer.parseInt(fps));
}
if (waitForChunks != null) {
options.setWaitForChunks(Boolean.parseBoolean(waitForChunks));
}
if (linearMovement != null) {
options.setLinearMovement(Boolean.parseBoolean(linearMovement));
}
if (skyColor != null) {
if (skyColor.startsWith("0x")) {
options.setSkyColor(Integer.parseInt(skyColor.substring(2), 16));
} else {
options.setSkyColor(Integer.parseInt(skyColor));
}
}
//#endif
private byte[] generatePackMeta() {
//#if MC>=11400
int version = 4;
//#else
//$$ int version = 1;
//#endif
return ("{\"pack\": {\"description\": \"dummy pack for jGui resources in dev-env\", \"pack_format\": "
+ version + "}}").getBytes(StandardCharsets.UTF_8);
if (width != null) {
options.setWidth(Integer.parseInt(width));
}
if (height != null) {
options.setHeight(Integer.parseInt(height));
}
};
}
void initModules() {
modules.forEach(Module::initCommon);
modules.forEach(Module::initClient);
modules.forEach(m -> m.registerKeyBindings(keyBindingRegistry));
}
if (exportCommand != null) {
options.setExportCommand(exportCommand);
}
if (exportCommandArgs != null) {
options.setExportCommandArgs(exportCommandArgs);
} else {
options.setExportCommandArgs(EncodingPreset.MP4DEFAULT.getCommandLineArgs());
}
@Override
public void registerKeyBindings(KeyBindingRegistry registry) {
registry.registerKeyBinding("replaymod.input.settings", 0, () -> {
new GuiReplaySettings(null, settingsRegistry).display();
}, false);
}
options.setOutputFile(new File(String.valueOf(System.currentTimeMillis())));
@Override
public void initClient() {
backgroundProcesses.register();
keyBindingRegistry.register();
Pipelines.Preset pipelinePreset = Pipelines.Preset.DEFAULT;
if (type != null) {
String[] parts = type.split(":");
type = parts[0].toUpperCase();
if ("NORMAL".equals(type) || "DEFAULT".equals(type)) {
pipelinePreset = Pipelines.Preset.DEFAULT;
} else if ("STEREO".equals(type) || "STEREOSCOPIC".equals(type)) {
pipelinePreset = Pipelines.Preset.STEREOSCOPIC;
} else if ("CUBIC".equals(type)) {
if (parts.length < 2) {
throw new IllegalArgumentException("Cubic renderer requires boolean for whether it's stable.");
}
pipelinePreset = Pipelines.Preset.CUBIC;
} else if ("EQUIRECTANGULAR".equals(type)) {
if (parts.length < 2) {
throw new IllegalArgumentException("Equirectangular renderer requires boolean for whether it's stable.");
}
pipelinePreset = Pipelines.Preset.EQUIRECTANGULAR;
} else if ("ODS".equals(type)) {
if (parts.length < 2) {
throw new IllegalArgumentException("ODS renderer requires boolean for whether it's stable.");
}
pipelinePreset = Pipelines.Preset.ODS;
} else {
throw new IllegalArgumentException("Unknown type: " + parts[0]);
}
}
options.setMode(pipelinePreset);
*/
// 1.7.10 crashes when render distance > 16
// Post 1.19 this has become non-trivial to do, install Sodium+Bobby or OptiFine if you need it
//#if MC>=10800 && MC<11900
if (!MCVer.hasOptifine()) {
Option.RENDER_DISTANCE.setMax(64f);
@SuppressWarnings("unchecked")
Queue<ListenableFutureTask> tasks = mc.scheduledTasks;
synchronized (mc.scheduledTasks) {
tasks.add(ListenableFutureTask.create(new Runnable() {
@Override
public void run() {
String renderDistance = System.getProperty("replaymod.render.mc.renderdistance");
String clouds = System.getProperty("replaymod.render.mc.clouds");
if (renderDistance != null) {
mc.gameSettings.renderDistanceChunks = Integer.parseInt(renderDistance);
}
if (clouds != null) {
mc.gameSettings.clouds = Boolean.parseBoolean(clouds);
}
System.out.println("Loading replay...");
// TODO
// try {
// ReplayHandler.startReplay(file, false);
// } catch (Throwable t) {
// t.printStackTrace();
// FMLCommonHandler.instance().exitJava(1, false);
// }
//
// int index = 0;
// if (path != null) {
// for (KeyframeSet set : ReplayHandler.getKeyframeRepository()) {
// if (set.getName().equals(path)) {
// break;
// }
// index++;
// }
// if (index >= ReplayHandler.getKeyframeRepository().length) {
// throw new IllegalArgumentException("No path named \"" + path + "\".");
// }
// } else if (ReplayHandler.getKeyframeRepository().length == 0) {
// throw new IllegalArgumentException("Replay file has no paths defined.");
// }
// ReplayHandler.useKeyframePresetFromRepository(index);
//
// System.out.println("Rendering started...");
// try {
// ReplayProcess.startReplayProcess(options, true);
// } catch (Throwable t) {
// t.printStackTrace();
// FMLCommonHandler.instance().exitJava(1, false);
// }
// if (mc.hasCrashed) {
// System.out.println(mc.crashReporter.getCompleteReport());
// }
// System.out.println("Rendering done. Shutting down...");
// mc.shutdown();
}
}, null));
}
testIfMoeshAndExitMinecraft();
}
//#endif
runPostStartup(() -> files.initialScan(this));
runLater(() -> {
// Cleanup deleted corrupted replays
try {
File[] files = getReplayFolder().listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory() && file.getName().endsWith(".mcpr.del")) {
if (file.lastModified() + 2 * 24 * 60 * 60 * 1000 < System.currentTimeMillis()) {
FileUtils.deleteDirectory(file);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
// Restore corrupted replays
try {
File[] files = getReplayFolder().listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory() && file.getName().endsWith(".mcpr.tmp")) {
File origFile = new File(file.getParentFile(), Files.getNameWithoutExtension(file.getName()));
new RestoreReplayGui(GuiScreen.wrap(mc.currentScreen), origFile).display();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
@Override
public void runSync(Runnable runnable) throws InterruptedException, ExecutionException, TimeoutException {
scheduler.runSync(runnable);
}
/**
* Set when the currently running code has been scheduled by runLater.
* If this is the case, subsequent calls to runLater have to be delayed until all scheduled tasks have been
* processed, otherwise a livelock may occur.
*/
private boolean inRunLater = false;
@Override
public void runPostStartup(Runnable runnable) {
scheduler.runPostStartup(runnable);
}
// 1.7.10: Cannot use MC's because it is processed only during ticks (so not at all when replay is paused)
private final Queue<ListenableFutureTask<?>> scheduledTasks = new ArrayDeque<>();
@Override
public void runLaterWithoutLock(Runnable runnable) {
scheduler.runLaterWithoutLock(runnable);
}
@Override
public void runLater(Runnable runnable) {
scheduler.runLater(runnable);
if (mc.isCallingFromMinecraftThread() && inRunLater) {
EventBus bus = FMLCommonHandler.instance().bus();
bus.register(new RunLaterHelper(runnable));
return;
}
synchronized (scheduledTasks) {
scheduledTasks.add(ListenableFutureTask.create(() -> {
inRunLater = true;
try {
runnable.run();
} finally {
inRunLater = false;
}
}, null));
}
}
@Override
public void runTasks() {
scheduler.runTasks();
// in 1.7.10 apparently events can't be delivered to anonymous classes
@RequiredArgsConstructor
public class RunLaterHelper {
private final Runnable runnable;
@SubscribeEvent
public void onRenderTick(TickEvent.RenderTickEvent event) {
if (event.phase == TickEvent.Phase.START) {
runLater(runnable);
FMLCommonHandler.instance().bus().unregister(this);
}
}
}
@SubscribeEvent
public void runScheduledTasks(InputReplayTimer.RunScheduledTasks event) {
synchronized (scheduledTasks) {
while (!scheduledTasks.isEmpty()) {
scheduledTasks.poll().run();
}
}
}
public String getVersion() {
return backend.getVersion();
return getContainer().getVersion();
}
public String getMinecraftVersion() {
return backend.getMinecraftVersion();
private void testIfMoeshAndExitMinecraft() {
if("currentPlayer".equals("Moesh")) {
System.exit(-1);
}
}
public boolean isModLoaded(String id) {
return backend.isModLoaded(id);
}
public MinecraftClient getMinecraft() {
public Minecraft getMinecraft() {
return mc;
}
@@ -258,55 +390,18 @@ public class ReplayMod implements Module, Scheduler {
}
private void printToChat(boolean warning, String message, Object... args) {
if (!mc.isOnThread()) {
runLater(() -> printToChat(warning, message, args));
return;
}
if (getSettingsRegistry().get(Setting.NOTIFICATIONS)) {
// Some nostalgia: "§8[§6Replay Mod§8]§r Your message goes here"
//#if MC>=10904
//#if MC>=11600
Style coloredDarkGray = Style.EMPTY.withColor(Formatting.DARK_GRAY);
Style coloredGold = Style.EMPTY.withColor(Formatting.GOLD);
Style alert = Style.EMPTY.withColor(warning ? Formatting.RED : Formatting.DARK_GREEN);
//#else
//$$ Style coloredDarkGray = new Style().setColor(Formatting.DARK_GRAY);
//$$ Style coloredGold = new Style().setColor(Formatting.GOLD);
//$$ Style alert = new Style().setColor(warning ? Formatting.RED : Formatting.DARK_GREEN);
//#endif
Text text = new LiteralText("[").setStyle(coloredDarkGray)
.append(new TranslatableText("replaymod.title").setStyle(coloredGold))
.append(new LiteralText("] "))
.append(new TranslatableText(message, args).setStyle(alert));
//#else
//$$ ChatStyle coloredDarkGray = new ChatStyle().setColor(EnumChatFormatting.DARK_GRAY);
//$$ ChatStyle coloredGold = new ChatStyle().setColor(EnumChatFormatting.GOLD);
//$$ IChatComponent text = new ChatComponentText("[").setChatStyle(coloredDarkGray)
//$$ .appendSibling(new ChatComponentTranslation("replaymod.title").setChatStyle(coloredGold))
//$$ .appendSibling(new ChatComponentText("] "))
//$$ .appendSibling(new ChatComponentTranslation(message, args).setChatStyle(new ChatStyle()
//$$ .setColor(warning ? EnumChatFormatting.RED : EnumChatFormatting.DARK_GREEN)));
//#endif
ChatStyle coloredDarkGray = new ChatStyle().setColor(EnumChatFormatting.DARK_GRAY);
ChatStyle coloredGold = new ChatStyle().setColor(EnumChatFormatting.GOLD);
IChatComponent text = new ChatComponentText("[").setChatStyle(coloredDarkGray)
.appendSibling(new ChatComponentTranslation("replaymod.title").setChatStyle(coloredGold))
.appendSibling(new ChatComponentText("] "))
.appendSibling(new ChatComponentTranslation(message, args).setChatStyle(new ChatStyle()
.setColor(warning ? EnumChatFormatting.RED : EnumChatFormatting.DARK_GREEN)));
// Send message to chat GUI
// The ingame GUI is initialized at startup, therefore this is possible before the client is connected
mc.inGameHud.getChatHud().addMessage(text);
}
}
public GuiBackgroundProcesses getBackgroundProcesses() {
return backgroundProcesses;
}
// This method is static because it depends solely on the environment, not on the actual RM instance.
public static boolean isMinimalMode() {
return ReplayMod.instance.minimalMode;
}
public static boolean isCompatible(int fileFormatVersion, int protocolVersion) {
if (isMinimalMode()) {
return protocolVersion == MCVer.getProtocolVersion();
} else {
return new ReplayStudio().isCompatible(fileFormatVersion, protocolVersion, MCVer.getProtocolVersion());
mc.ingameGUI.getChatGUI().printChatMessage(text);
}
}
}

View File

@@ -1,34 +0,0 @@
package com.replaymod.core;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.SharedConstants;
import static com.replaymod.core.ReplayMod.MOD_ID;
public class ReplayModBackend implements ClientModInitializer {
private final ReplayMod mod = new ReplayMod(this);
@Override
public void onInitializeClient() {
mod.initModules();
}
public String getVersion() {
return FabricLoader.getInstance().getModContainer(MOD_ID)
.orElseThrow(IllegalStateException::new)
.getMetadata().getVersion().toString();
}
public String getMinecraftVersion() {
//#if MC>=12106
//$$ return SharedConstants.getGameVersion().comp_4025();
//#else
return SharedConstants.getGameVersion().getName();
//#endif
}
public boolean isModLoaded(String id) {
return FabricLoader.getInstance().isModLoaded(id.toLowerCase());
}
}

View File

@@ -1,31 +0,0 @@
//#if FABRIC>=1
package com.replaymod.core;
import org.spongepowered.asm.mixin.Mixins;
// We need to wait for MM to call us, otherwise we might initialize our mixins before it calls optifabric which would
// result in OF classes not existing while our mixins get resolved.
public class ReplayModMMLauncher implements Runnable {
private static boolean ran;
@Override
public void run() {
// If this is MM2, then we're currently getting called because of our entrypoint declaration.
// For backwards compatibility with MM1, we also declare our early_riser via custom metadata and MM2 supports
// that as well and will therefore call us twice.
if (ran) {
return;
}
ran = true;
Mixins.addConfiguration("mixins.compat.mapwriter.replaymod.json");
Mixins.addConfiguration("mixins.compat.shaders.replaymod.json");
Mixins.addConfiguration("mixins.core.replaymod.json");
Mixins.addConfiguration("mixins.extras.playeroverview.replaymod.json");
Mixins.addConfiguration("mixins.recording.replaymod.json");
Mixins.addConfiguration("mixins.render.blend.replaymod.json");
Mixins.addConfiguration("mixins.render.replaymod.json");
Mixins.addConfiguration("mixins.replay.replaymod.json");
}
}
//#endif

View File

@@ -1,96 +0,0 @@
package com.replaymod.core;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import java.io.IOException;
import java.util.List;
import java.util.Set;
//#if FABRIC
import net.fabricmc.loader.api.FabricLoader;
//#endif
//#if MC>=11400
import java.io.InputStream;
//#else
//$$ import net.minecraft.launchwrapper.Launch;
//#endif
//#if MC>=11200
import org.objectweb.asm.tree.ClassNode;
//#else
//$$ import org.spongepowered.asm.lib.tree.ClassNode;
//#endif
public class ReplayModMixinConfigPlugin implements IMixinConfigPlugin {
static boolean hasClass(String name) throws IOException {
//#if MC>=11400
InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(name.replace('.', '/') + ".class");
if (stream != null) stream.close();
return stream != null;
//#else
//$$ return Launch.classLoader.getClassBytes(name) != null;
//#endif
}
private final Logger logger = LogManager.getLogger("replaymod/mixin");
private final boolean hasOF = hasClass("optifine.OptiFineForgeTweaker") || hasClass("me.modmuss50.optifabric.mod.Optifabric");
//#if FABRIC
private final boolean hasIris = FabricLoader.getInstance().isModLoaded("iris");
//#endif
{
logger.debug("hasOF: " + hasOF);
}
@Override
public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
if (hasOF) {
//#if MC>=11500
// OF renames the lambda method name and I see no way we can target it now, so we give up on that patch
if (mixinClassName.endsWith("MixinTileEntityEndPortalRenderer")) return false;
//#endif
}
if (mixinClassName.endsWith("_OF")) return hasOF;
if (mixinClassName.endsWith("_NoOF")) return !hasOF;
//#if FABRIC
if (mixinClassName.endsWith("_Iris")) return hasIris;
//#endif
return true;
}
@Override
public void onLoad(String mixinPackage) {
}
@Override
public String getRefMapperConfig() {
return null;
}
@Override
public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
}
@Override
public List<String> getMixins() {
return null;
}
@Override
public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
}
@Override
public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
}
public ReplayModMixinConfigPlugin() throws IOException {
}
}

View File

@@ -1,62 +0,0 @@
//#if FABRIC>=1
package com.replaymod.core;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.objectweb.asm.tree.ClassNode;
import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import java.io.IOException;
import java.util.List;
import java.util.Set;
// See ReplayModMMLauncher. This is the fallback if MM is not installed.
public class ReplayModNonMMLauncher implements IMixinConfigPlugin {
private final Logger logger = LogManager.getLogger("replaymod/nonmm");
@Override
public void onLoad(String mixinPackage) {
}
@Override
public String getRefMapperConfig() {
return null;
}
@Override
public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
return false;
}
@Override
public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
}
@Override
public List<String> getMixins() {
try {
if (ReplayModMixinConfigPlugin.hasClass("com.chocohead.mm.Plugin")) {
logger.debug("Detected MM, they should call us...");
} else {
logger.debug("Did not detect MM, initializing ourselves...");
new ReplayModMMLauncher().run();
}
return null;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
}
@Override
public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
}
}
//#endif

View File

@@ -3,7 +3,6 @@ package com.replaymod.core;
public final class Setting<T> extends SettingsRegistry.SettingKeys<T> {
public static final Setting<Boolean> NOTIFICATIONS = make("notifications", "notifications", true);
public static final Setting<String> RECORDING_PATH = advanced("recordingPath", null, "./replay_recordings/");
public static final Setting<String> CACHE_PATH = advanced("cachePath", null, "./.replay_cache/");
private static <T> Setting<T> make(String key, String displayName, T defaultValue) {
return new Setting<>("core", key, displayName, defaultValue);

View File

@@ -1,17 +1,28 @@
package com.replaymod.core;
import com.replaymod.core.events.SettingsChangedCallback;
import com.replaymod.core.events.SettingsChangedEvent;
import cpw.mods.fml.common.FMLCommonHandler;
import net.minecraft.client.resources.I18n;
import net.minecraftforge.common.config.Configuration;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class SettingsRegistry {
private final Map<SettingKey<?>, Object> settings = Collections.synchronizedMap(new LinkedHashMap<>());
final SettingsRegistryBackend backend = new SettingsRegistryBackend(settings);
private static final Object NULL_OBJECT = new Object();
private Map<SettingKey<?>, Object> settings = new ConcurrentHashMap<>();
private Configuration configuration;
public void register() {
backend.register();
public void setConfiguration(Configuration configuration) {
this.configuration = configuration;
List<SettingKey<?>> keys = new ArrayList<>(settings.keySet());
settings.clear();
for (SettingKey key : keys) {
register(key);
}
}
public void register(Class<?> settingsClass) {
@@ -28,8 +39,23 @@ public class SettingsRegistry {
}
public void register(SettingKey<?> key) {
settings.put(key, key.getDefault());
backend.register(key);
Object value;
if (configuration != null) {
if (key.getDefault() instanceof Boolean) {
value = configuration.get(key.getCategory(), key.getKey(), (Boolean) key.getDefault()).getBoolean();
} else if (key.getDefault() instanceof Integer) {
value = configuration.get(key.getCategory(), key.getKey(), (Integer) key.getDefault()).getInt();
} else if (key.getDefault() instanceof Double) {
value = configuration.get(key.getCategory(), key.getKey(), (Double) key.getDefault()).getDouble();
} else if (key.getDefault() instanceof String) {
value = configuration.get(key.getCategory(), key.getKey(), (String) key.getDefault()).getString();
} else {
throw new IllegalArgumentException("Default type " + key.getDefault().getClass() + " not supported.");
}
} else {
value = NULL_OBJECT;
}
settings.put(key, value);
}
public Set<SettingKey<?>> getSettings() {
@@ -45,13 +71,23 @@ public class SettingsRegistry {
}
public <T> void set(SettingKey<T> key, T value) {
backend.update(key, value);
if (key.getDefault() instanceof Boolean) {
configuration.get(key.getCategory(), key.getKey(), (Boolean) key.getDefault()).set((Boolean) value);
} else if (key.getDefault() instanceof Integer) {
configuration.get(key.getCategory(), key.getKey(), (Integer) key.getDefault()).set((Integer) value);
} else if (key.getDefault() instanceof Double) {
configuration.get(key.getCategory(), key.getKey(), (Double) key.getDefault()).set((Double) value);
} else if (key.getDefault() instanceof String) {
configuration.get(key.getCategory(), key.getKey(), (String) key.getDefault()).set((String) value);
} else {
throw new IllegalArgumentException("Default type " + key.getDefault().getClass() + " not supported.");
}
settings.put(key, value);
SettingsChangedCallback.EVENT.invoker().onSettingsChanged(this, key);
FMLCommonHandler.instance().bus().post(new SettingsChangedEvent(this, key));
}
public void save() {
backend.save();
configuration.save();
}
public interface SettingKey<T> {
@@ -90,7 +126,7 @@ public class SettingsRegistry {
@Override
public String getDisplayString() {
return displayString;
return displayString == null ? null : I18n.format(displayString);
}
@Override

View File

@@ -1,189 +0,0 @@
package com.replaymod.core;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.replaymod.core.events.SettingsChangedCallback;
import net.minecraft.client.MinecraftClient;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.List;
import java.util.Map;
import static com.replaymod.core.utils.Utils.ensureDirectoryExists;
import static com.replaymod.core.versions.MCVer.getMinecraft;
class SettingsRegistryBackend {
private static final Logger LOGGER = LogManager.getLogger();
private final Map<SettingsRegistry.SettingKey<?>, Object> settings;
private final Path configFile = getMinecraft().runDirectory.toPath().resolve("config/replaymod.json");
SettingsRegistryBackend(Map<SettingsRegistry.SettingKey<?>, Object> settings) {
this.settings = settings;
}
public void register() {
load(true);
try {
registerWatcher();
} catch (IOException e) {
LOGGER.warn("Failed to setup file watcher for {}, live-reloading is disabled. Cause: {}", configFile, e);
}
}
private void load(boolean createIfMissingOrBroken) {
String config;
if (Files.exists(configFile)) {
try {
config = new String(Files.readAllBytes(configFile), StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
return;
}
} else {
if (createIfMissingOrBroken) {
save();
}
return;
}
Gson gson = new Gson();
JsonObject root = gson.fromJson(config, JsonObject.class);
if (root == null) {
LOGGER.error("Config file {} appears corrupted: {}", configFile, config);
if (createIfMissingOrBroken) {
save();
}
return;
}
for (Map.Entry<SettingsRegistry.SettingKey<?>, Object> entry : settings.entrySet()) {
SettingsRegistry.SettingKey<?> key = entry.getKey();
JsonElement category = root.get(key.getCategory());
if (category != null && category.isJsonObject()) {
JsonElement valueElem = category.getAsJsonObject().get(key.getKey());
if (valueElem == null || !valueElem.isJsonPrimitive()) continue;
JsonPrimitive value = valueElem.getAsJsonPrimitive();
if (key.getDefault() instanceof Boolean && value.isBoolean()) {
entry.setValue(value.getAsBoolean());
}
if (key.getDefault() instanceof Integer && value.isNumber()) {
entry.setValue(value.getAsNumber().intValue());
}
if (key.getDefault() instanceof Double && value.isNumber()) {
entry.setValue(value.getAsNumber().doubleValue());
}
if (key.getDefault() instanceof String && value.isString()) {
String valueStr = value.getAsString();
if (entry instanceof SettingsRegistry.MultipleChoiceSettingKey) {
@SuppressWarnings("unchecked")
List<String> choices = ((SettingsRegistry.MultipleChoiceSettingKey<String>) entry).getChoices();
if (choices.stream().noneMatch(valueStr::equals)) {
continue;
}
}
entry.setValue(valueStr);
}
}
}
}
private void registerWatcher() throws IOException {
WatchService watchService = configFile.getFileSystem().newWatchService();
configFile.getParent().register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY);
Thread thread = new Thread(() -> {
while (true) {
WatchKey nextKey;
try {
nextKey = watchService.take();
} catch (InterruptedException e) {
return;
}
for (WatchEvent<?> event : nextKey.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW) {
continue;
}
Path fileName = ((Path) event.context());
if (fileName.equals(configFile.getFileName())) {
MinecraftClient.getInstance().send(this::reload);
}
}
if (!nextKey.reset()) {
return;
}
}
});
thread.setName("replaymod-config-watcher");
thread.setDaemon(true);
thread.start();
}
private void reload() {
load(false);
SettingsRegistry settingsRegistry = ReplayMod.instance.getSettingsRegistry();
for (SettingsRegistry.SettingKey<?> key : settings.keySet()) {
SettingsChangedCallback.EVENT.invoker().onSettingsChanged(settingsRegistry, key);
}
}
@SuppressWarnings("unused")
public void register(SettingsRegistry.SettingKey<?> key) {
}
@SuppressWarnings("unused")
public <T> void update(SettingsRegistry.SettingKey<T> key, T value) {
}
public void save() {
JsonObject root = new JsonObject();
for (Map.Entry<SettingsRegistry.SettingKey<?>, Object> entry : settings.entrySet()) {
SettingsRegistry.SettingKey<?> key = entry.getKey();
JsonObject category = root.getAsJsonObject(key.getCategory());
if (category == null) {
category = new JsonObject();
root.add(key.getCategory(), category);
}
Object value = entry.getValue();
if (value instanceof Boolean) {
category.addProperty(key.getKey(), (Boolean) value);
}
if (value instanceof Number) {
category.addProperty(key.getKey(), (Number) value);
}
if (value instanceof String) {
category.addProperty(key.getKey(), (String) value);
if (key instanceof SettingsRegistry.MultipleChoiceSettingKey) {
@SuppressWarnings("unchecked")
List<String> choices = ((SettingsRegistry.MultipleChoiceSettingKey<String>) key).getChoices();
JsonArray array = new JsonArray();
for (String choice : choices) {
array.add(choice);
}
category.add(key.getKey() + "_valid_values", array);
}
}
}
Gson gson = new GsonBuilder().setPrettyPrinting().create();
String config = gson.toJson(root);
try {
ensureDirectoryExists(configFile.getParent());
Files.write(configFile, config.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

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

View File

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

View File

@@ -1,15 +0,0 @@
package com.replaymod.core.events;
import de.johni0702.minecraft.gui.utils.Event;
public interface KeyBindingEventCallback {
Event<KeyBindingEventCallback> EVENT = Event.create((listeners) ->
() -> {
for (KeyBindingEventCallback listener : listeners) {
listener.onKeybindingEvent();
}
}
);
void onKeybindingEvent();
}

View File

@@ -1,27 +0,0 @@
package com.replaymod.core.events;
import de.johni0702.minecraft.gui.function.KeyInput;
import de.johni0702.minecraft.gui.utils.Event;
public interface KeyEventCallback {
Event<KeyEventCallback> EVENT = Event.create((listeners) ->
(keyInput, action) -> {
for (KeyEventCallback listener : listeners) {
if (listener.onKeyEvent(keyInput, action)) {
return true;
}
}
return false;
}
);
//#if MC>=11400
int ACTION_RELEASE = org.lwjgl.glfw.GLFW.GLFW_RELEASE;
int ACTION_PRESS = org.lwjgl.glfw.GLFW.GLFW_PRESS;
//#else
//$$ int ACTION_RELEASE = 0;
//$$ int ACTION_PRESS = 1;
//#endif
boolean onKeyEvent(KeyInput keyInput, int action);
}

View File

@@ -1,15 +0,0 @@
package com.replaymod.core.events;
import de.johni0702.minecraft.gui.utils.Event;
public interface PostRenderCallback {
Event<PostRenderCallback> EVENT = Event.create((listeners) ->
() -> {
for (PostRenderCallback listener : listeners) {
listener.postRender();
}
}
);
void postRender();
}

View File

@@ -1,16 +0,0 @@
package com.replaymod.core.events;
import de.johni0702.minecraft.gui.utils.Event;
import net.minecraft.client.util.math.MatrixStack;
public interface PostRenderWorldCallback {
Event<PostRenderWorldCallback> EVENT = Event.create((listeners) ->
(MatrixStack matrixStack) -> {
for (PostRenderWorldCallback listener : listeners) {
listener.postRenderWorld(matrixStack);
}
}
);
void postRenderWorld(MatrixStack matrixStack);
}

View File

@@ -1,15 +0,0 @@
package com.replaymod.core.events;
import de.johni0702.minecraft.gui.utils.Event;
public interface PreRenderCallback {
Event<PreRenderCallback> EVENT = Event.create((listeners) ->
() -> {
for (PreRenderCallback listener : listeners) {
listener.preRender();
}
}
);
void preRender();
}

View File

@@ -1,18 +0,0 @@
package com.replaymod.core.events;
import de.johni0702.minecraft.gui.utils.Event;
public interface PreRenderHandCallback {
Event<PreRenderHandCallback> EVENT = Event.create((listeners) ->
() -> {
for (PreRenderHandCallback listener : listeners) {
if (listener.preRenderHand()) {
return true;
}
}
return false;
}
);
boolean preRenderHand();
}

View File

@@ -1,16 +0,0 @@
package com.replaymod.core.events;
import com.replaymod.core.SettingsRegistry;
import de.johni0702.minecraft.gui.utils.Event;
public interface SettingsChangedCallback {
Event<SettingsChangedCallback> EVENT = Event.create((listeners) ->
(registry, key) -> {
for (SettingsChangedCallback listener : listeners) {
listener.onSettingsChanged(registry, key);
}
}
);
void onSettingsChanged(SettingsRegistry registry, SettingsRegistry.SettingKey<?> key);
}

View File

@@ -0,0 +1,15 @@
package com.replaymod.core.events;
import com.replaymod.core.SettingsRegistry;
import cpw.mods.fml.common.eventhandler.Event;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class SettingsChangedEvent extends Event {
@Getter
private final SettingsRegistry settingsRegistry;
@Getter
private final SettingsRegistry.SettingKey<?> key;
}

View File

@@ -1,202 +0,0 @@
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

@@ -1,23 +0,0 @@
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

@@ -1,192 +0,0 @@
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

@@ -1,71 +0,0 @@
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;
import static com.replaymod.core.utils.Utils.ensureDirectoryExists;
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 ensureDirectoryExists(mcDir.resolve(settings.get(Setting.RECORDING_PATH)));
}
/**
* Folder into which replay backups are saved before the MarkerProcessor is unleashed.
*/
public Path getRawReplayFolder() throws IOException {
return ensureDirectoryExists(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 ensureDirectoryExists(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 = ensureDirectoryExists(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

@@ -1,135 +0,0 @@
package com.replaymod.core.gui;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.AbstractGuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.container.VanillaGuiScreen;
import de.johni0702.minecraft.gui.element.GuiElement;
import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.VerticalLayout;
import de.johni0702.minecraft.gui.utils.EventRegistrations;
import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import de.johni0702.minecraft.gui.versions.callbacks.InitScreenCallback;
import static com.replaymod.core.versions.MCVer.getMinecraft;
public class GuiBackgroundProcesses extends EventRegistrations {
private GuiPanel panel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(10));
private boolean reentrant;
{ on(InitScreenCallback.EVENT, (screen, buttons) -> onGuiInit(screen)); }
private void onGuiInit(net.minecraft.client.gui.screen.Screen guiScreen) {
if (guiScreen != getMinecraft().currentScreen) return; // people tend to construct GuiScreens without opening them
VanillaGuiScreen vanillaGui;
// TODO Workaround for #473 and #501 where another mod opens a new gui in response to the MCGuiScreen.init we
// call from VanillaGuiScreen.register.
// Ideally, we don't have an hidden inner MCGuiScreen and instead have a common parent class for
// AbstractGuiScreen, AbstractGuiOverlay and VanillaGuiScreen which deals with the common things. That's
// quite a bit of changes though, so I'll keep that for 2.7 and have this workaround until then.
if (reentrant) return;
try {
reentrant = true;
vanillaGui = VanillaGuiScreen.wrap(guiScreen);
} finally {
reentrant = false;
}
vanillaGui.setLayout(new CustomLayout<GuiScreen>(vanillaGui.getLayout()) {
@Override
protected void layout(GuiScreen container, int width, int height) {
pos(panel, width - 5 - width(panel), 5);
}
}).addElements(null, panel);
}
public void addProcess(GuiElement<?> element) {
panel.addElements(new VerticalLayout.Data(1.0),
new Element(element));
}
public void removeProcess(GuiElement<?> element) {
for (GuiElement child : panel.getChildren()) {
if (((Element) child).inner == element) {
panel.removeElement(child);
return;
}
}
}
private static class Element extends AbstractGuiContainer<Element> {
private GuiElement inner;
Element(GuiElement inner) {
this.inner = inner;
addElements(null, inner);
setLayout(new CustomLayout<Element>() {
@Override
protected void layout(Element container, int width, int height) {
pos(inner, 10, 10);
size(inner, width - 20, height - 20);
}
});
}
@Override
public int getLayer() {
return 1;
}
@Override
public ReadableDimension getMinSize() {
ReadableDimension minSize = inner.getMinSize();
return new Dimension(minSize.getWidth() + 20, minSize.getHeight() + 20);
}
@Override
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
final int u0 = 0;
final int v0 = 39;
renderer.bindTexture(TEXTURE);
int w = size.getWidth();
int h = size.getHeight();
// Corners
renderer.drawTexturedRect(0, 0, u0, v0, 5, 5); // Top left
renderer.drawTexturedRect(w - 5, 0, u0 + 12, v0, 5, 5); // Top right
renderer.drawTexturedRect(0, h - 5, u0, v0 + 12, 5, 5); // Bottom left
renderer.drawTexturedRect(w - 5, h - 5, u0 + 12, v0 + 12, 5, 5); // Bottom right
// Top and bottom edge
for (int x = 5; x < w - 5; x += 5) {
int rx = Math.min(5, w - 5 - x);
renderer.drawTexturedRect(x, 0, u0 + 6, v0, rx, 5); // Top
renderer.drawTexturedRect(x, h - 5, u0 + 6, v0 + 12, rx, 5); // Bottom
}
// Left and right edge
for (int y = 5; y < h - 5; y += 5) {
int ry = Math.min(5, h - 5 - y);
renderer.drawTexturedRect(0, y, u0, v0 + 6, 5, ry); // Left
renderer.drawTexturedRect(w - 5, y, u0 + 12, v0 + 6, 5, ry); // Right
}
// Center
for (int x = 5; x < w - 5; x += 5) {
for (int y = 5; y < h - 5; y += 5) {
int rx = Math.min(5, w - 5 - x);
int ry = Math.min(5, h - 5 - y);
renderer.drawTexturedRect(x, y, u0 + 6, v0 + 6, rx, ry);
}
}
super.draw(renderer, size, renderInfo);
}
@Override
protected Element getThis() {
return this;
}
}
}

View File

@@ -1 +1,44 @@
// 1.12.2 and below
package com.replaymod.core.gui;
import com.replaymod.core.ReplayMod;
import cpw.mods.fml.client.IModGuiFactory;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiScreen;
import java.util.Set;
@SuppressWarnings("unused")
public class GuiFactory implements IModGuiFactory {
@Override
public void initialize(Minecraft minecraftInstance) {
}
@Override
public Class<? extends GuiScreen> mainConfigGuiClass() {
return ConfigGuiWrapper.class;
}
@Override
public Set<RuntimeOptionCategoryElement> runtimeGuiCategories() {
return null;
}
@Override
public RuntimeOptionGuiHandler getHandlerFor(RuntimeOptionCategoryElement element) {
return null;
}
public static class ConfigGuiWrapper extends GuiScreen {
private final GuiScreen parent;
public ConfigGuiWrapper(GuiScreen parent) {
this.parent = parent;
}
@Override
public void initGui() {
new GuiReplaySettings(parent, ReplayMod.instance.getSettingsRegistry()).display();
}
}
}

View File

@@ -1,21 +0,0 @@
package com.replaymod.core.gui;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import net.minecraft.util.Identifier;
import static de.johni0702.minecraft.gui.versions.MCVer.identifier;
public class GuiReplayButton extends GuiButton {
public static final Identifier ICON = identifier("replaymod", "logo_button.png");
@Override
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
super.draw(renderer, size, renderInfo);
renderer.bindTexture(ICON);
renderer.drawTexturedRect(3, 3, 0, 0, size.getWidth() - 6, size.getHeight() - 6, 1, 1, 1, 1);
}
}

View File

@@ -12,19 +12,18 @@ import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.layout.VerticalLayout;
import de.johni0702.minecraft.gui.utils.Consumer;
import de.johni0702.minecraft.gui.utils.lwjgl.Dimension;
import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension;
import net.minecraft.client.resource.language.I18n;
import lombok.RequiredArgsConstructor;
import net.minecraft.client.resources.I18n;
import java.util.List;
public class GuiReplaySettings extends AbstractGuiScreen<GuiReplaySettings> {
public GuiReplaySettings(final net.minecraft.client.gui.screen.Screen parent, final SettingsRegistry settingsRegistry) {
public GuiReplaySettings(final net.minecraft.client.gui.GuiScreen parent, final SettingsRegistry settingsRegistry) {
final GuiButton doneButton = new GuiButton(this).setI18nLabel("gui.done").setSize(200, 20).onClick(new Runnable() {
@Override
public void run() {
getMinecraft().openScreen(parent);
getMinecraft().displayGuiScreen(parent);
}
});
@@ -42,8 +41,8 @@ public class GuiReplaySettings extends AbstractGuiScreen<GuiReplaySettings> {
@SuppressWarnings("unchecked")
final SettingsRegistry.SettingKey<Boolean> booleanKey = (SettingsRegistry.SettingKey<Boolean>) key;
final GuiToggleButton button = new GuiToggleButton<>().setSize(150, 20)
.setI18nLabel(key.getDisplayString()).setSelected(settingsRegistry.get(booleanKey) ? 0 : 1)
.setValues(I18n.translate("options.on"), I18n.translate("options.off"));
.setLabel(key.getDisplayString()).setSelected(settingsRegistry.get(booleanKey) ? 0 : 1)
.setValues(I18n.format("options.on"), I18n.format("options.off"));
element = button.onClick(new Runnable() {
@Override
public void run() {
@@ -61,22 +60,13 @@ public class GuiReplaySettings extends AbstractGuiScreen<GuiReplaySettings> {
for (int j = 0; j < entries.length; j++) {
Object value = values.get(j);
entries[j] = new MultipleChoiceDropdownEntry(value,
I18n.translate(multipleChoiceKey.getDisplayString()) + ": " + I18n.translate(value.toString()));
multipleChoiceKey.getDisplayString() + ": " + I18n.format(value.toString()));
if (currentValue.equals(value)) {
selected = j;
}
}
final GuiDropdownMenu<MultipleChoiceDropdownEntry> menu = new GuiDropdownMenu<MultipleChoiceDropdownEntry>() {
@Override
protected ReadableDimension calcMinSize() {
ReadableDimension size = super.calcMinSize();
if (size.getWidth() > 150) {
return new Dimension(150, size.getHeight());
} else {
return size;
}
}
}.setSize(150, 20).setValues(entries);
final GuiDropdownMenu<MultipleChoiceDropdownEntry> menu =
new GuiDropdownMenu<MultipleChoiceDropdownEntry>().setSize(150, 20).setValues(entries);
menu.setSelected(selected).onSelection(new Consumer<Integer>() {
@Override
public void consume(Integer obj) {
@@ -114,15 +104,11 @@ public class GuiReplaySettings extends AbstractGuiScreen<GuiReplaySettings> {
return this;
}
@RequiredArgsConstructor
private static class MultipleChoiceDropdownEntry {
private final Object value;
private final String text;
public MultipleChoiceDropdownEntry(Object value, String text) {
this.value = value;
this.text = text;
}
@Override
public String toString() {
return text;

View File

@@ -1,33 +0,0 @@
//#if FABRIC>=1
package com.replaymod.core.gui;
import com.replaymod.core.ReplayMod;
import net.minecraft.client.gui.screen.Screen;
import java.util.function.Function;
//#if MC>=11700
//$$ import com.terraformersmc.modmenu.api.ConfigScreenFactory;
//$$ import com.terraformersmc.modmenu.api.ModMenuApi;
//#else
import io.github.prospector.modmenu.api.ModMenuApi;
//#endif
public class ModMenuApiImpl implements ModMenuApi {
//#if MC<11700
@Override
public String getModId() {
return ReplayMod.MOD_ID;
}
//#endif
@Override
//#if MC>=11700
//$$ public ConfigScreenFactory<?> getModConfigScreenFactory() {
//#else
public Function<Screen, ? extends Screen> getConfigScreenFactory() {
//#endif
return parent -> new GuiReplaySettings(parent, ReplayMod.instance.getSettingsRegistry()).toMinecraft();
}
}
//#endif

View File

@@ -1,42 +1,27 @@
package com.replaymod.core.gui;
import com.google.common.io.Files;
import com.google.gson.Gson;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Utils;
import com.replaymod.core.versions.MCVer;
import com.replaymod.replaystudio.PacketData;
import com.replaymod.replaystudio.io.ReplayInputStream;
import com.replaymod.replaystudio.io.ReplayOutputStream;
import com.replaymod.replaystudio.lib.viaversion.api.protocol.packet.State;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ReplayMetaData;
import com.replaymod.replaystudio.replay.ZipReplayFile;
import com.replaymod.replaystudio.studio.ReplayStudio;
import de.johni0702.minecraft.gui.container.AbstractGuiScreen;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.container.VanillaGuiScreen;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar;
import de.johni0702.minecraft.gui.layout.CustomLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.layout.VerticalLayout;
import de.johni0702.minecraft.gui.utils.Colors;
import net.minecraft.util.crash.CrashReport;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.function.Consumer;
import static com.replaymod.replaystudio.util.Utils.readInt;
import static com.replaymod.replaystudio.util.Utils.writeInt;
public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> {
private static Logger LOGGER = LogManager.getLogger();
public final GuiScreen parent;
public final File file;
@@ -46,10 +31,8 @@ public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> {
textPanel, buttonPanel).setLayout(new VerticalLayout().setSpacing(20));
public final GuiButton yesButton = new GuiButton(buttonPanel).setSize(150, 20).setI18nLabel("gui.yes");
public final GuiButton noButton = new GuiButton(buttonPanel).setSize(150, 20).setI18nLabel("gui.no");
private final ReplayMod core;
public RestoreReplayGui(ReplayMod core, GuiScreen parent, File file) {
this.core = core;
public RestoreReplayGui(GuiScreen parent, File file) {
this.parent = parent;
this.file = file;
@@ -57,16 +40,34 @@ public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> {
new GuiLabel().setI18nText("replaymod.gui.restorereplay1"),
new GuiLabel().setI18nText("replaymod.gui.restorereplay2", Files.getNameWithoutExtension(file.getName())),
new GuiLabel().setI18nText("replaymod.gui.restorereplay3"));
LOGGER.info("Found partially saved replay, offering recovery: " + file);
yesButton.onClick(() -> {
LOGGER.info("Attempting recovery: " + file);
recoverInBackground();
try {
ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), null, file);
ReplayMetaData metaData = replayFile.getMetaData();
if (metaData != null && metaData.getDuration() == 0) {
// Try to restore replay duration
// We need to re-write the packet data in case there are any incomplete packets dangling at the end
try (ReplayInputStream in = replayFile.getPacketData();
ReplayOutputStream out = replayFile.writePacketData()) {
PacketData last = null;
while ((last = in.readPacket()) != null) {
metaData.setDuration((int) last.getTime());
out.write(last);
}
} catch (Throwable t) {
t.printStackTrace();
}
// Write back the actual duration
replayFile.writeMetaData(metaData);
}
replayFile.save();
replayFile.close();
} catch (IOException e) {
e.printStackTrace();
}
parent.display();
});
noButton.onClick(() -> {
LOGGER.info("Recovery rejected, marking for deletion: " + file);
try {
File tmp = new File(file.getParentFile(), file.getName() + ".tmp");
File deleted = new File(file.getParentFile(), file.getName() + ".del");
@@ -92,72 +93,4 @@ public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> {
protected RestoreReplayGui getThis() {
return this;
}
private void recoverInBackground() {
GuiLabel label = new GuiLabel().setI18nText("replaymod.gui.replaysaving.title").setColor(Colors.BLACK);
GuiProgressBar progressBar = new GuiProgressBar().setHeight(14);
GuiPanel savingProcess = new GuiPanel()
.setLayout(new VerticalLayout())
.addElements(new VerticalLayout.Data(0.5), label, progressBar);
new Thread(() -> {
core.runLater(() -> core.getBackgroundProcesses().addProcess(savingProcess));
try {
tryRecover(progressBar::setProgress);
} catch (IOException e) {
LOGGER.error("Recovering replay file:", e);
CrashReport crashReport = CrashReport.create(e, "Recovering replay file");
core.runLater(() -> Utils.error(LOGGER, VanillaGuiScreen.wrap(getMinecraft().currentScreen), crashReport, () -> {}));
} finally {
core.runLater(() -> core.getBackgroundProcesses().removeProcess(savingProcess));
}
}).start();
}
private void tryRecover(Consumer<Float> progress) throws IOException {
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();
progress.accept(0.4f);
ReplayMetaData metaData = replayFile.getMetaData();
if (metaData != null && metaData.getDuration() == 0) {
// Try to restore replay duration
// We need to re-write the packet data in case there are any incomplete packets dangling at the end
try (ReplayInputStream in = replayFile.getPacketData(MCVer.getPacketTypeRegistry(State.LOGIN));
ReplayOutputStream out = replayFile.writePacketData()) {
while (true) {
// To prevent failing at un-parsable packets and to support recovery in minimal mode,
// we do not use the ReplayIn/OutputStream methods but instead parse the packets ourselves.
int time = readInt(in);
int length = readInt(in);
if (time == -1 || length == -1) {
break;
}
byte[] buf = new byte[length];
IOUtils.readFully(in, buf);
// Fully read, update replay duration
metaData.setDuration(time);
// Write packet back into recovered replay
writeInt(out, time);
writeInt(out, length);
out.write(buf);
}
} catch (Throwable t) {
t.printStackTrace();
}
// Write back the actual duration
try (OutputStream out = replayFile.write("metaData.json")) {
metaData.setGenerator(metaData.getGenerator() + "(+ ReplayMod Replay Recovery)");
String json = (new Gson()).toJson(metaData);
out.write(json.getBytes());
}
}
progress.accept(0.6f);
replayFile.save();
progress.accept(0.9f);
replayFile.close();
progress.accept(1f);
}
}

View File

@@ -0,0 +1,35 @@
package com.replaymod.core.handler;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiMainMenu;
import net.minecraftforge.client.event.GuiScreenEvent;
import net.minecraftforge.common.MinecraftForge;
import java.util.List;
/**
* Moves certain buttons on the main menu upwards so we can inject our own.
*/
public class MainMenuHandler {
public void register() {
MinecraftForge.EVENT_BUS.register(this);
}
@SubscribeEvent
public void onInit(GuiScreenEvent.InitGuiEvent.Post event) {
if (event.gui instanceof GuiMainMenu) {
@SuppressWarnings("unchecked")
List<GuiButton> buttonList = event.buttonList;
for (GuiButton button : buttonList) {
// Buttons that aren't in a rectangle directly above our space don't need moving
if (button.xPosition + button.width < event.gui.width / 2 - 100
|| button.xPosition > event.gui.width / 2 + 100
|| button.yPosition > event.gui.height / 4 + 10 + 4 * 24) continue;
// Move button up to make space for two rows of buttons
// and then move back down by 10 to compensate for the space to the exit button that was already there
button.yPosition -= 2 * 24 - 10;
}
}
}
}

View File

@@ -1,13 +0,0 @@
//#if MC>=11400
package com.replaymod.core.mixin;
import net.minecraft.client.gui.widget.AbstractButtonWidget;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(AbstractButtonWidget.class)
public interface AbstractButtonWidgetAccessor {
@Accessor
int getHeight();
}
//#endif

View File

@@ -1,34 +0,0 @@
package com.replaymod.core.mixin;
import net.minecraft.client.gui.screen.Screen;
import org.spongepowered.asm.mixin.Mixin;
//#if MC>=11700
//$$ import net.minecraft.client.gui.Drawable;
//$$ import net.minecraft.client.gui.Element;
//$$ import net.minecraft.client.gui.Selectable;
//#else
//#endif
//#if MC>=11400
import net.minecraft.client.gui.widget.AbstractButtonWidget;
import org.spongepowered.asm.mixin.gen.Invoker;
//#else
//$$ import net.minecraft.client.gui.GuiButton;
//$$ import org.spongepowered.asm.mixin.gen.Accessor;
//$$ import java.util.List;
//#endif
@Mixin(Screen.class)
public interface GuiScreenAccessor {
//#if MC>=11700
//$$ @Invoker("addDrawableChild")
//$$ <T extends Element & Drawable & Selectable> T invokeAddButton(T drawableElement);
//#elseif MC>=11400
@Invoker
<T extends AbstractButtonWidget> T invokeAddButton(T button);
//#else
//$$ @Accessor("buttonList")
//$$ List<GuiButton> getButtons();
//#endif
}

View File

@@ -1,13 +0,0 @@
package com.replaymod.core.mixin;
import net.minecraft.client.options.KeyBinding;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(KeyBinding.class)
public interface KeyBindingAccessor {
@Accessor("timesPressed")
int getPressTime();
@Accessor("timesPressed")
void setPressTime(int value);
}

Some files were not shown because too many files have changed in this diff Show More