diff --git a/.drone.yml b/.drone.yml index 5b371337..ecea2c97 100644 --- a/.drone.yml +++ b/.drone.yml @@ -8,8 +8,9 @@ pipeline: commands: - curl --fail --create-dirs -o $HOME/.gradle/init.d/mirror.gradle https://maven.johni0702.de/config/gradle - git config --global user.email "replaymod@drone" && git config --global user.name "Drone" - - REPRODUCIBLE_BUILD=1 ./gradlew :1.7.10:setupCIWorkspace :jGui:1.7.10:setupCIWorkspace - - SIGNED_JAR=$(! git describe --exact 2>&1 > /dev/null; echo $?) REPRODUCIBLE_BUILD=1 ./gradlew $(bash -c "echo :{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}:reobfJar") --parallel + - ./gradlew --old-gradle :1.7.10:setupCIWorkspace :jGui:1.7.10:setupCIWorkspace + - ./gradlew --old-gradle :1.7.10:reobfJar + - ./gradlew $(bash -c "echo :{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.13.2}:reobfJar") --parallel - mkdir -p build/libs && mv versions/*/build/libs/* build/libs/ archive: image: plugins/s3 diff --git a/.drone.yml.sig b/.drone.yml.sig index 3bd52b21..fff644e1 100644 --- a/.drone.yml.sig +++ b/.drone.yml.sig @@ -1 +1 @@ -eyJhbGciOiJIUzI1NiJ9.cGlwZWxpbmU6CiAgY2xvbmU6CiAgICBpbWFnZTogcGx1Z2lucy9naXQKICAgIHRhZ3M6IHRydWUKICAgIHJlY3Vyc2l2ZTogdHJ1ZQogIGJ1aWxkOgogICAgaW1hZ2U6IG1hdmVuOjMuMy45LWpkay04CiAgICBjb21tYW5kczoKICAgICAgLSBjdXJsIC0tZmFpbCAtLWNyZWF0ZS1kaXJzIC1vICRIT01FLy5ncmFkbGUvaW5pdC5kL21pcnJvci5ncmFkbGUgaHR0cHM6Ly9tYXZlbi5qb2huaTA3MDIuZGUvY29uZmlnL2dyYWRsZQogICAgICAtIGdpdCBjb25maWcgLS1nbG9iYWwgdXNlci5lbWFpbCAicmVwbGF5bW9kQGRyb25lIiAmJiBnaXQgY29uZmlnIC0tZ2xvYmFsIHVzZXIubmFtZSAiRHJvbmUiCiAgICAgIC0gUkVQUk9EVUNJQkxFX0JVSUxEPTEgLi9ncmFkbGV3IDoxLjcuMTA6c2V0dXBDSVdvcmtzcGFjZSA6akd1aToxLjcuMTA6c2V0dXBDSVdvcmtzcGFjZQogICAgICAtIFNJR05FRF9KQVI9JCghIGdpdCBkZXNjcmliZSAtLWV4YWN0IDI-JjEgPiAvZGV2L251bGw7IGVjaG8gJD8pIFJFUFJPRFVDSUJMRV9CVUlMRD0xIC4vZ3JhZGxldyAkKGJhc2ggLWMgImVjaG8gOnsxLjcuMTAsMS44LDEuOC45LDEuOS40LDEuMTAuMiwxLjExLDEuMTEuMiwxLjEyLDEuMTIuMSwxLjEyLjJ9OnJlb2JmSmFyIikgLS1wYXJhbGxlbAogICAgICAtIG1rZGlyIC1wIGJ1aWxkL2xpYnMgJiYgbXYgdmVyc2lvbnMvKi9idWlsZC9saWJzLyogYnVpbGQvbGlicy8KICBhcmNoaXZlOgogICAgaW1hZ2U6IHBsdWdpbnMvczMKICAgIGFjbDogcHVibGljLXJlYWQKICAgIHJlZ2lvbjogInVzLWVhc3QtMSIKICAgIGJ1Y2tldDogInJlcGxheW1vZCIKICAgIHBhdGhfc3R5bGU6IHRydWUKICAgIHNvdXJjZTogYnVpbGQvbGlicy8qCiAgICBzdHJpcF9wcmVmaXg6IGJ1aWxkL2xpYnMvCiAgICB0YXJnZXQ6IC8K.2bNe9Pd4FUTIP9VTrFR4YuHJkgCmjIbfBCUu-Wrvap8 \ No newline at end of file +eyJhbGciOiJIUzI1NiJ9.cGlwZWxpbmU6CiAgY2xvbmU6CiAgICBpbWFnZTogcGx1Z2lucy9naXQKICAgIHRhZ3M6IHRydWUKICAgIHJlY3Vyc2l2ZTogdHJ1ZQogIGJ1aWxkOgogICAgaW1hZ2U6IG1hdmVuOjMuMy45LWpkay04CiAgICBjb21tYW5kczoKICAgICAgLSBjdXJsIC0tZmFpbCAtLWNyZWF0ZS1kaXJzIC1vICRIT01FLy5ncmFkbGUvaW5pdC5kL21pcnJvci5ncmFkbGUgaHR0cHM6Ly9tYXZlbi5qb2huaTA3MDIuZGUvY29uZmlnL2dyYWRsZQogICAgICAtIGdpdCBjb25maWcgLS1nbG9iYWwgdXNlci5lbWFpbCAicmVwbGF5bW9kQGRyb25lIiAmJiBnaXQgY29uZmlnIC0tZ2xvYmFsIHVzZXIubmFtZSAiRHJvbmUiCiAgICAgIC0gLi9ncmFkbGV3IC0tb2xkLWdyYWRsZSA6MS43LjEwOnNldHVwQ0lXb3Jrc3BhY2UgOmpHdWk6MS43LjEwOnNldHVwQ0lXb3Jrc3BhY2UKICAgICAgLSAuL2dyYWRsZXcgLS1vbGQtZ3JhZGxlIDoxLjcuMTA6cmVvYmZKYXIKICAgICAgLSAuL2dyYWRsZXcgJChiYXNoIC1jICJlY2hvIDp7MS44LDEuOC45LDEuOS40LDEuMTAuMiwxLjExLDEuMTEuMiwxLjEyLDEuMTIuMSwxLjEyLjIsMS4xMy4yfTpyZW9iZkphciIpIC0tcGFyYWxsZWwKICAgICAgLSBta2RpciAtcCBidWlsZC9saWJzICYmIG12IHZlcnNpb25zLyovYnVpbGQvbGlicy8qIGJ1aWxkL2xpYnMvCiAgYXJjaGl2ZToKICAgIGltYWdlOiBwbHVnaW5zL3MzCiAgICBhY2w6IHB1YmxpYy1yZWFkCiAgICByZWdpb246ICJ1cy1lYXN0LTEiCiAgICBidWNrZXQ6ICJyZXBsYXltb2QiCiAgICBwYXRoX3N0eWxlOiB0cnVlCiAgICBzb3VyY2U6IGJ1aWxkL2xpYnMvKgogICAgc3RyaXBfcHJlZml4OiBidWlsZC9saWJzLwogICAgdGFyZ2V0OiAvCg.xvkvCrkXlWrSXghw1bLwsewIfd0vJZyz-Wk15airj_s \ No newline at end of file diff --git a/.gitignore b/.gitignore index b56d0cb7..cf554856 100644 Binary files a/.gitignore and b/.gitignore differ diff --git a/README.md b/README.md index cc6f2ed1..a3e842e0 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ After the initial clone, 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). +**Note**: ForgeGradle 1.2 (used for 1.7.10) is not compatible with Gradle 4. +As such, you need to use an older version (Gradle 3.x) to build the ReplayMod for 1.7.10. +An easy way to do that is to pass `--old-gradle` as the first argument to the Gradle wrapper shell script: `./gradlew --old-gradle build` + ### No IDE You can build the mod by running `./gradlew build`. 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` (builds the MC 1.8 version). diff --git a/build.gradle b/build.gradle index e4e25ae6..de31bda2 100755 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,6 @@ +apply plugin: 'eclipse' +apply plugin: 'idea' + import groovy.json.JsonOutput def latestVersion = file('version.txt').readLines().first() @@ -13,7 +16,7 @@ if (releaseCommit == currentCommit) { version = "$latestVersion-$diff-g${currentCommit.substring(0, 7)}" } if (gitDescribe().endsWith('*')) { - version = "$version*" + version = "$version-dirty" } group= "com.replaymod" @@ -23,18 +26,6 @@ subprojects { repositories { maven { url 'https://jitpack.io' } } - configurations.classpath.resolutionStrategy.eachDependency { dep -> - if (System.getenv('REPRODUCIBLE_BUILD') == '1') return - def req = dep.target - // Substitute FG 2.0 with custom fork that works with Gradle 3 - if (req.group == 'net.minecraftforge.gradle' && req.name == 'ForgeGradle' && req.version == '2.0-SNAPSHOT') { - dep.useTarget 'com.github.ReplayMod:ForgeGradle:dd521d0' - } - // Substitute FG 2.1 with custom fork that works with Gradle 3 - if (req.group == 'net.minecraftforge.gradle' && req.name == 'ForgeGradle' && req.version == '2.1-SNAPSHOT') { - dep.useTarget 'com.github.ReplayMod:ForgeGradle:9c2f79c' - } - } } // Thanks for wasting my time gradle... diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..b209ec29 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,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=-Xmx3G +org.gradle.daemon=false +org.gradle.parallel= diff --git a/gradle/reprod/checksums.empty.txt b/gradle/reprod/checksums.empty.txt new file mode 100644 index 00000000..e69de29b diff --git a/gradle/reprod/checksums.txt b/gradle/reprod/checksums.txt index 04ec2f70..20344785 100644 --- a/gradle/reprod/checksums.txt +++ b/gradle/reprod/checksums.txt @@ -30,24 +30,24 @@ cddfc1a07301f2fa07a5e1b6897e455d397d6d5c547d45082c267a1ee2c75f70 /com/github/Mod 7191124b8491d36b9fc1eb1a4e0f8ef710a4b98e27dde92854c24d45457fd9d0 /com/github/ModCoderPack/MCInjector/05aa80e/MCInjector-05aa80e.pom ecf57aab63509ec0ce3937c2215bc3a2e3cb6357cd11095978db18bd3251731a /com/github/ModCoderPack/MCInjector/7258466/MCInjector-7258466.jar dc7ed1f4f0ddd8f5ce3b449db944907a5eb2855dbbbc59b08dad1247ebafba33 /com/github/ModCoderPack/MCInjector/7258466/MCInjector-7258466.pom -e1a215146621ab100c5d88c6c65e7b76c91f2a06ab8714ac438738ddec5ea904 /com/github/ReplayMod/ReplayStudio/1.10.2/3680c2e/1.10.2-3680c2e.jar -da0b27f2719bf829f91eb4d6ea7d6e54c71b301a19578bafe6ec05e72ed04a6a /com/github/ReplayMod/ReplayStudio/1.10.2/3680c2e/1.10.2-3680c2e.pom -67e8b400d005235bd2825717864c4a9341567302877c857c3681adc74802b2ba /com/github/ReplayMod/ReplayStudio/1.11.2/3680c2e/1.11.2-3680c2e.jar -ff786e8da1d52bfdca8abe58bc83cf9200e027a85212da633e795811b5c8b1ee /com/github/ReplayMod/ReplayStudio/1.11.2/3680c2e/1.11.2-3680c2e.pom -68da074f5cccd590c9584efc885f6ea842a15f1ce968aad04cbac6af2c8ecf49 /com/github/ReplayMod/ReplayStudio/1.11/3680c2e/1.11-3680c2e.jar -79a6ed9587d6b23f5221b0fdff12f01de4859dc985f3cdbebf081edd4e478a54 /com/github/ReplayMod/ReplayStudio/1.11/3680c2e/1.11-3680c2e.pom -60549740b778011d5e4a8b786805c6de8c518ce8f627cf70369b2c71f0d399c4 /com/github/ReplayMod/ReplayStudio/1.12.1/3680c2e/1.12.1-3680c2e.jar -da248126087579d384175fa36eb654c8755027727aff6c285cd8e4e056ddf424 /com/github/ReplayMod/ReplayStudio/1.12.1/3680c2e/1.12.1-3680c2e.pom -30ecf796e3697cf7d86c21f245695e659b39f55f2aecf40864ec6ac2c0eeae87 /com/github/ReplayMod/ReplayStudio/1.12.2/3680c2e/1.12.2-3680c2e.jar -73c4c53e5f18cffd921db81019510c47afb306eb2aea86377f44fc91e8b45130 /com/github/ReplayMod/ReplayStudio/1.12.2/3680c2e/1.12.2-3680c2e.pom -2f0e27b40a0c3ac298573c6a2fad24829e968731676305b5f1c471a827ac7458 /com/github/ReplayMod/ReplayStudio/1.12/3680c2e/1.12-3680c2e.jar -282e2fa0ef80206ab592f90f8585d5ecf5adfb110dcd1cb5204971257e3fd44e /com/github/ReplayMod/ReplayStudio/1.12/3680c2e/1.12-3680c2e.pom -1aa22b15d5e89258fc58c2a310c915937cf93b42bf27b9fefc04d20b6b07b19c /com/github/ReplayMod/ReplayStudio/1.7.10/3680c2e/1.7.10-3680c2e.jar -fbd4bfe141d845248bb095d59f3aaf273bf20ad849cec89d21594754a69f3f53 /com/github/ReplayMod/ReplayStudio/1.7.10/3680c2e/1.7.10-3680c2e.pom -96b37e76cf8b4aac4f0419d84ba6427da156cd7c4f41f9b9380d149893dd50a7 /com/github/ReplayMod/ReplayStudio/1.8/3680c2e/1.8-3680c2e.jar -7a68b798f4029b98b391076992d39633b3060d06d43edc25422edbbed09914f5 /com/github/ReplayMod/ReplayStudio/1.8/3680c2e/1.8-3680c2e.pom -5e5bea81fd85631a5f80973fab581e991d793d543e2bbbdbf1bebc15b4a279cd /com/github/ReplayMod/ReplayStudio/1.9.4/3680c2e/1.9.4-3680c2e.jar -cb0a152f7c720de75325b2cb857879596edf8c4b0de6f5b39168a1be1aa6709f /com/github/ReplayMod/ReplayStudio/1.9.4/3680c2e/1.9.4-3680c2e.pom +72dd4d393e1103a8719629c2231b7fc4ac307f339c24d0da94c6cc1d050838ec /com/github/ReplayMod/ReplayStudio/1.10.2/2d8d41d/1.10.2-2d8d41d.jar +6849dab3156262b109d07bba0ac56ed996f6581b2f7f4065c88afe0a9d3a620c /com/github/ReplayMod/ReplayStudio/1.10.2/2d8d41d/1.10.2-2d8d41d.pom +868ce8eb3c2cf8628edb952828cbfec26fa9c443a33a6125a912c40548e0a835 /com/github/ReplayMod/ReplayStudio/1.11.2/2d8d41d/1.11.2-2d8d41d.jar +599546e6a69a60df616f657f3bda58d0d4e58103a17de351c65941e0b53bc658 /com/github/ReplayMod/ReplayStudio/1.11.2/2d8d41d/1.11.2-2d8d41d.pom +4ccffcdea734d26004364737b90cc394049a11f3ab0d47c66cdd2f0d5728e316 /com/github/ReplayMod/ReplayStudio/1.11/2d8d41d/1.11-2d8d41d.jar +e8e9bf0ee84eb6f89ed6ec28d5142fc0cdb68eea74a763a6f8ee5f4ee8af00d2 /com/github/ReplayMod/ReplayStudio/1.11/2d8d41d/1.11-2d8d41d.pom +57fef9ef4cbcf766f73122556d05aab6e80fecc3c9b12d0efdfb090500d422cf /com/github/ReplayMod/ReplayStudio/1.12.2/2d8d41d/1.12.2-2d8d41d.jar +55601b8a60329c2b5271dd8a44f7dd82bbf4288d698e840c111edeb9a0b52517 /com/github/ReplayMod/ReplayStudio/1.12.2/2d8d41d/1.12.2-2d8d41d.pom +f175ad66645b7fdf6e472ccac0e01893ea22cfb7812636013d0982b6280b4663 /com/github/ReplayMod/ReplayStudio/1.12.1/2d8d41d/1.12.1-2d8d41d.jar +d3ea142c6ef67c84d1b0f658a8a17612465fc2d89fb8c10fe15def51392a9fc2 /com/github/ReplayMod/ReplayStudio/1.12.1/2d8d41d/1.12.1-2d8d41d.pom +bcf66a214b72615ba0d60a7d4a8be0ae3480baa5a65aa37732616d0d7154fe68 /com/github/ReplayMod/ReplayStudio/1.12/2d8d41d/1.12-2d8d41d.jar +6681d0640cb569a2b2c8c082371e80f96e67b3ba292a4ba56cf188a71a6d915b /com/github/ReplayMod/ReplayStudio/1.12/2d8d41d/1.12-2d8d41d.pom +7f718341faa2b1203b078e8a416b74ae195dc98f98eebb0e08eb462cf98d6cd7 /com/github/ReplayMod/ReplayStudio/1.7.10/2d8d41d/1.7.10-2d8d41d.jar +d482d4a1d6b3b169822512ecb8a296ed43cbdba2f3ae85cc391c9e890526b7e7 /com/github/ReplayMod/ReplayStudio/1.7.10/2d8d41d/1.7.10-2d8d41d.pom +dc19cacb7b2e8dbce84200f78f0799ec8509343777b6d7511614e4b3a48e6f26 /com/github/ReplayMod/ReplayStudio/1.8/2d8d41d/1.8-2d8d41d.pom +6497cc087743beaaf397cda6d2ca76eb5afa37dae9889ea587ef8deae6483edd /com/github/ReplayMod/ReplayStudio/1.8/2d8d41d/1.8-2d8d41d.jar +9134cdee78eaac0c2c4a4728cef40fb8b9c824cbb587efdde5d768f23f295dfc /com/github/ReplayMod/ReplayStudio/1.9.4/2d8d41d/1.9.4-2d8d41d.jar +67faf1d44985182c4056ddbde300db813ca424b3f07818172eedcfc0f0a2c496 /com/github/ReplayMod/ReplayStudio/1.9.4/2d8d41d/1.9.4-2d8d41d.pom bfdcda4cbf23f28392384818a1417c2d6df687e15ce77530ef96381393102f8a /com/github/SpongePowered/MixinGradle/dcfaf61/MixinGradle-dcfaf61.jar 3552e0c04817d91d1b6671a051d0e1d952c8b6a8eec57b54e829de9fa3ddf002 /com/github/SpongePowered/MixinGradle/dcfaf61/MixinGradle-dcfaf61.pom c6ebd74cd6108b8c372c95b842725ee5bd6d3303ba26cb00f18bc92f80b47956 /com/github/tony19/named-regexp/0.2.3/named-regexp-0.2.3.jar diff --git a/gradle/reprod/setup.sh b/gradle/reprod/setup.sh index 46d93973..00c0a8cf 100755 --- a/gradle/reprod/setup.sh +++ b/gradle/reprod/setup.sh @@ -34,7 +34,7 @@ popd java -Dproxywitness.httpUris=http://export.mcpbot.bspk.rs/versions.json \ -Dproxywitness.useCache=false \ -jar deps/proxy-witness/build/libs/proxy-witness.jar \ - "$PROXY_PORT" checksums.txt > proxy.log 2>&1 & + "$PROXY_PORT" checksums.empty.txt > proxy.log 2>&1 & proxy_pid=$! trap "kill $proxy_pid" EXIT diff --git a/gradle/wrapper/gradle-wrapper-old.jar b/gradle/wrapper/gradle-wrapper-old.jar new file mode 100755 index 00000000..5ccda13e Binary files /dev/null and b/gradle/wrapper/gradle-wrapper-old.jar differ diff --git a/gradle/wrapper/gradle-wrapper-old.properties b/gradle/wrapper/gradle-wrapper-old.properties new file mode 100755 index 00000000..ca10d237 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper-old.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5.1-all.zip +distributionSha256Sum=69d99ade952de3727b65a88f3188ee72231ff4e28c2d00aacfc96a42c6d6f303 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ca10d237..7d754af9 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -2,5 +2,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.5.1-all.zip -distributionSha256Sum=69d99ade952de3727b65a88f3188ee72231ff4e28c2d00aacfc96a42c6d6f303 +distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip +distributionSha256Sum=39e2d5803bbd5eaf6c8efe07067b0e5a00235e8c71318642b2ed262920b27721 diff --git a/gradlew b/gradlew index 085750b4..757b1487 100755 --- a/gradlew +++ b/gradlew @@ -65,7 +65,12 @@ cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +if [ "$1" == "--old-gradle" ]; then + shift + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper-old.jar +else + CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +fi # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then diff --git a/jGui b/jGui index 09713104..79b0aaf3 160000 --- a/jGui +++ b/jGui @@ -1 +1 @@ -Subproject commit 097131041b4293245820b6f9b46271b5d5f4a830 +Subproject commit 79b0aaf3e9b5c70739c30a9ce1af9efa8adb26dc diff --git a/settings.gradle b/settings.gradle index f67c539b..1fb85e6d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,12 +1,47 @@ +def f = file('versions/core/mcVersion') +def coreMcVersion = f.exists() ? f.readLines().first() as int : 11302 + +def jGuiVersions = [] +def replayModVersions = [] +if (gradle.gradleVersion.startsWith('3.')) { + if (coreMcVersion <= 10710) { + jGuiVersions += ['core'] + replayModVersions += ['core'] + } + jGuiVersions += [ + '1.7.10', + ] + replayModVersions += [ + '1.7.10', + ] +} else { + if (coreMcVersion > 10710) { + jGuiVersions += ['core'] + replayModVersions += ['core'] + } + jGuiVersions += [ + '1.8', + '1.8.9', + '1.9.4', + '1.12', + '1.13.2' + ] + replayModVersions += [ + '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.13.2' + ] +} + include ':jGui' -[ - 'core', - '1.7.10', - '1.8', - '1.8.9', - '1.9.4', - '1.12' -].each { version -> +jGuiVersions.each { version -> include ":jGui:$version" project(":jGui:$version").with { projectDir = file("jGui/versions/$version") @@ -14,20 +49,7 @@ include ':jGui' } } -def versions = [ - 'core', - '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' -] -versions.each { version -> +replayModVersions.each { version -> include ":$version" project(":$version").with { projectDir = file("versions/$version") diff --git a/src/main/java/com/replaymod/compat/optifine/OptifineReflection.java b/src/main/java/com/replaymod/compat/optifine/OptifineReflection.java index 07aab724..b05205d2 100644 --- a/src/main/java/com/replaymod/compat/optifine/OptifineReflection.java +++ b/src/main/java/com/replaymod/compat/optifine/OptifineReflection.java @@ -27,7 +27,13 @@ public class OptifineReflection { public static void reloadLang() { try { - Class.forName("Lang").getDeclaredMethod("resourcesReloaded").invoke(null); + 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) { diff --git a/src/main/java/com/replaymod/compat/shaders/ShaderReflection.java b/src/main/java/com/replaymod/compat/shaders/ShaderReflection.java index 387874fd..5daac450 100644 --- a/src/main/java/com/replaymod/compat/shaders/ShaderReflection.java +++ b/src/main/java/com/replaymod/compat/shaders/ShaderReflection.java @@ -25,16 +25,19 @@ public class ShaderReflection { static { try { - shaders_frameTimeCounter = Class.forName("shadersmod.client.Shaders") - .getDeclaredField("frameTimeCounter"); + 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 = Class.forName("shadersmod.client.Shaders") - .getDeclaredField("isShadowPass"); + shaders_isShadowPass = shadersClass.getDeclaredField("isShadowPass"); shaders_isShadowPass.setAccessible(true); - shaders_beginRender = Class.forName("shadersmod.client.Shaders") - .getDeclaredMethod("beginRender", Minecraft.class, float.class, long.class); + shaders_beginRender = shadersClass.getDeclaredMethod("beginRender", Minecraft.class, float.class, long.class); shaders_beginRender.setAccessible(true); renderGlobal_chunksToUpdateForced = Class.forName("net.minecraft.client.renderer.RenderGlobal") diff --git a/src/main/java/com/replaymod/compat/shaders/mixin/MixinShadersRender.java b/src/main/java/com/replaymod/compat/shaders/mixin/MixinShadersRender.java index b81d176f..0ba5299f 100644 --- a/src/main/java/com/replaymod/compat/shaders/mixin/MixinShadersRender.java +++ b/src/main/java/com/replaymod/compat/shaders/mixin/MixinShadersRender.java @@ -9,7 +9,10 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Pseudo -@Mixin(targets = "shadersmod/client/ShadersRender", remap = false) +@Mixin(targets = { + "shadersmod/client/ShadersRender", // Pre Optifine 1.12.2 E1 + "net/optifine/shaders/ShadersRender" // Post Optifine 1.12.2 E1 +}, remap = false) public abstract class MixinShadersRender { @Inject(method = "renderHand0", at = @At("HEAD"), cancellable = true) diff --git a/src/main/java/com/replaymod/core/KeyBindingRegistry.java b/src/main/java/com/replaymod/core/KeyBindingRegistry.java index 1f02e0c3..9b2b87a6 100644 --- a/src/main/java/com/replaymod/core/KeyBindingRegistry.java +++ b/src/main/java/com/replaymod/core/KeyBindingRegistry.java @@ -6,12 +6,19 @@ import com.replaymod.core.versions.MCVer; 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 MC>=11300 +//#else +//$$ import org.lwjgl.input.Keyboard; +//#endif //#if MC>=10800 +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif import net.minecraftforge.fml.client.registry.ClientRegistry; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.InputEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; //#else @@ -95,24 +102,25 @@ public class KeyBindingRegistry { } catch (Throwable cause) { CrashReport crashReport = CrashReport.makeCrashReport(cause, "Handling Key Binding"); CrashReportCategory category = crashReport.makeCategory("Key Binding"); - category.addCrashSection("Key Binding", keyBinding); + MCVer.addDetail(category, "Key Binding", keyBinding::toString); MCVer.addDetail(category, "Handler", runnable::toString); - throw new ReportedException(crashReport); + throw newReportedException(crashReport); } } } public void handleRaw() { - int keyCode = Keyboard.getEventKey() == 0 ? Keyboard.getEventCharacter() + 256 : Keyboard.getEventKey(); + //int keyCode = Keyboard.getEventKey() == 0 ? Keyboard.getEventCharacter() + 256 : Keyboard.getEventKey(); + int keyCode = -1; // FIXME for (final Runnable runnable : rawHandlers.get(keyCode)) { try { runnable.run(); } catch (Throwable cause) { CrashReport crashReport = CrashReport.makeCrashReport(cause, "Handling Raw Key Binding"); CrashReportCategory category = crashReport.makeCategory("Key Binding"); - category.addCrashSection("Key Code", keyCode); + MCVer.addDetail(category, "Key Code", () -> "" + keyCode); MCVer.addDetail(category, "Handler", runnable::toString); - throw new ReportedException(crashReport); + throw newReportedException(crashReport); } } } diff --git a/src/main/java/com/replaymod/core/LoadingPlugin.java b/src/main/java/com/replaymod/core/LoadingPlugin.java index c3a7ea35..89b1f086 100755 --- a/src/main/java/com/replaymod/core/LoadingPlugin.java +++ b/src/main/java/com/replaymod/core/LoadingPlugin.java @@ -1,12 +1,13 @@ package com.replaymod.core; -import org.apache.logging.log4j.LogManager; -import org.spongepowered.asm.launch.MixinBootstrap; -import org.spongepowered.asm.mixin.Mixins; - +//#if MC<11300 +//$$ import org.apache.logging.log4j.LogManager; +//$$ import org.spongepowered.asm.launch.MixinBootstrap; +//$$ import org.spongepowered.asm.mixin.Mixins; +//$$ //#if MC>=10800 -import net.minecraftforge.fml.relauncher.CoreModManager; -import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin; +//$$ import net.minecraftforge.fml.relauncher.CoreModManager; +//$$ import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin; //#else //$$ import com.replaymod.core.asm.GLErrorTransformer; //$$ import com.replaymod.core.asm.GLStateTrackerTransformer; @@ -16,57 +17,57 @@ import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin; //$$ import java.util.ArrayList; //$$ import java.util.List; //#endif - -import java.io.File; -import java.net.URISyntaxException; -import java.net.URL; -import java.security.CodeSource; -import java.util.Map; - -@IFMLLoadingPlugin.TransformerExclusions("com.replaymod.core.asm.") -public class LoadingPlugin implements IFMLLoadingPlugin { - - public LoadingPlugin() { - MixinBootstrap.init(); - Mixins.addConfiguration("mixins.recording.replaymod.json"); - Mixins.addConfiguration("mixins.render.replaymod.json"); - Mixins.addConfiguration("mixins.render.blend.replaymod.json"); - Mixins.addConfiguration("mixins.replay.replaymod.json"); - Mixins.addConfiguration("mixins.compat.mapwriter.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? +//$$ +//$$ import java.io.File; +//$$ import java.net.URISyntaxException; +//$$ import java.net.URL; +//$$ import java.security.CodeSource; +//$$ import java.util.Map; +//$$ +//$$ @IFMLLoadingPlugin.TransformerExclusions("com.replaymod.core.asm.") +//$$ public class LoadingPlugin implements IFMLLoadingPlugin { +//$$ +//$$ public LoadingPlugin() { +//$$ MixinBootstrap.init(); +//$$ Mixins.addConfiguration("mixins.recording.replaymod.json"); +//$$ Mixins.addConfiguration("mixins.render.replaymod.json"); +//$$ Mixins.addConfiguration("mixins.render.blend.replaymod.json"); +//$$ Mixins.addConfiguration("mixins.replay.replaymod.json"); +//$$ Mixins.addConfiguration("mixins.compat.mapwriter.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? //#if MC>=10809 - CoreModManager.getIgnoredMods().remove(file.getName()); + //$$ CoreModManager.getIgnoredMods().remove(file.getName()); //#else //$$ CoreModManager.getLoadedCoremods().remove(file.getName()); //#if MC<=10710 //$$ CoreModManager.getReparseableCoremods().add(file.getName()); //#endif //#endif - } - } 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() { +//$$ } +//$$ } 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() { //#if MC>=10800 - return new String[]{ - }; + //$$ return new String[]{ + //$$ }; //#else //$$ List transformers = new ArrayList<>(); //$$ if ("true".equals(System.getProperty("replaymod.glerrors", "false"))) { @@ -75,25 +76,26 @@ public class LoadingPlugin implements IFMLLoadingPlugin { //$$ transformers.add(GLStateTrackerTransformer.class.getName()); //$$ return transformers.stream().toArray(String[]::new); //#endif - } - - @Override - public String getModContainerClass() { - return null; - } - - @Override - public String getSetupClass() { - return null; - } - - @Override - public void injectData(Map data) { - } - - @Override - public String getAccessTransformerClass() { - return null; - } - -} +//$$ } +//$$ +//$$ @Override +//$$ public String getModContainerClass() { +//$$ return null; +//$$ } +//$$ +//$$ @Override +//$$ public String getSetupClass() { +//$$ return null; +//$$ } +//$$ +//$$ @Override +//$$ public void injectData(Map data) { +//$$ } +//$$ +//$$ @Override +//$$ public String getAccessTransformerClass() { +//$$ return null; +//$$ } +//$$ +//$$ } +//#endif diff --git a/src/main/java/com/replaymod/core/Module.java b/src/main/java/com/replaymod/core/Module.java new file mode 100644 index 00000000..321b1613 --- /dev/null +++ b/src/main/java/com/replaymod/core/Module.java @@ -0,0 +1,10 @@ +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) {} +} diff --git a/src/main/java/com/replaymod/core/ReplayMod.java b/src/main/java/com/replaymod/core/ReplayMod.java index 86c43a4b..5efd7fea 100755 --- a/src/main/java/com/replaymod/core/ReplayMod.java +++ b/src/main/java/com/replaymod/core/ReplayMod.java @@ -2,21 +2,42 @@ package com.replaymod.core; import com.google.common.io.Files; import com.google.common.util.concurrent.ListenableFutureTask; +import com.replaymod.core.gui.GuiBackgroundProcesses; import com.replaymod.core.gui.GuiReplaySettings; import com.replaymod.core.gui.RestoreReplayGui; import com.replaymod.core.handler.MainMenuHandler; -import com.replaymod.core.utils.OpenGLUtils; +import com.replaymod.core.versions.MCVer; +import com.replaymod.editor.ReplayModEditor; +import com.replaymod.extras.ReplayModExtras; +import com.replaymod.online.ReplayModOnline; +import com.replaymod.recording.ReplayModRecording; +import com.replaymod.render.ReplayModRender; +import com.replaymod.replay.ReplayModReplay; import com.replaymod.replaystudio.util.I18n; +import com.replaymod.simplepathing.ReplayModSimplePathing; import de.johni0702.minecraft.gui.container.GuiScreen; import lombok.Getter; import net.minecraft.client.Minecraft; -import net.minecraft.client.resources.FolderResourcePack; -import net.minecraft.client.resources.IResourcePack; import net.minecraft.util.ResourceLocation; -import net.minecraftforge.common.config.Configuration; import org.apache.commons.io.Charsets; import org.apache.commons.io.FileUtils; +//#if MC>=11300 +import com.replaymod.core.versions.LangResourcePack; +import net.minecraft.resources.FolderPack; +import net.minecraft.resources.IPackFinder; +import net.minecraft.resources.ResourcePackInfo; +import net.minecraftforge.fml.DeferredWorkQueue; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.versions.mcp.MCPVersion; +//#else +//$$ import net.minecraft.client.resources.FolderResourcePack; +//$$ import net.minecraft.client.resources.IResourcePack; +//$$ import net.minecraftforge.common.config.Configuration; +//#endif + //#if MC>=10904 import net.minecraft.util.text.*; //#else @@ -28,18 +49,23 @@ import net.minecraft.util.text.*; //#endif //#if MC>=10800 -import net.minecraft.client.settings.GameSettings; -import net.minecraftforge.fml.client.FMLClientHandler; -import net.minecraftforge.fml.common.Loader; +//#if MC>=11300 +import net.minecraft.client.GameSettings; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.ModContainer; +import net.minecraftforge.fml.ModList; +//#else +//$$ import net.minecraft.client.settings.GameSettings; +//$$ import net.minecraftforge.fml.client.FMLClientHandler; +//$$ import net.minecraftforge.fml.common.Loader; +//$$ import net.minecraftforge.fml.common.Mod.EventHandler; +//$$ import net.minecraftforge.fml.common.Mod.Instance; +//$$ import net.minecraftforge.fml.common.ModContainer; +//$$ import net.minecraftforge.fml.common.event.FMLInitializationEvent; +//$$ import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.Mod.EventHandler; -import net.minecraftforge.fml.common.Mod.Instance; -import net.minecraftforge.fml.common.ModContainer; -import net.minecraftforge.fml.common.event.FMLInitializationEvent; -import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; -import net.minecraftforge.fml.common.eventhandler.EventBus; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; //#else //$$ import cpw.mods.fml.common.Loader; @@ -48,9 +74,7 @@ import net.minecraftforge.fml.common.gameevent.TickEvent; //$$ 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 com.replaymod.replay.InputReplayTimer; @@ -62,48 +86,108 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Queue; import java.util.concurrent.FutureTask; import static com.replaymod.core.versions.MCVer.*; -@Mod(modid = ReplayMod.MOD_ID, - useMetadata = true, - version = "@MOD_VERSION@", - acceptedMinecraftVersions = "@MC_VERSION@", - acceptableRemoteVersions = "*", +//#if MC>=11300 +@Mod(ReplayMod.MOD_ID) +//#else +//$$ @Mod(modid = ReplayMod.MOD_ID, +//$$ useMetadata = true, +//$$ version = "@MOD_VERSION@", +//$$ acceptedMinecraftVersions = "@MC_VERSION@", +//$$ acceptableRemoteVersions = "*", //#if MC>=10800 - clientSideOnly = true, - updateJSON = "https://raw.githubusercontent.com/ReplayMod/ReplayMod/master/versions.json", + //$$ clientSideOnly = true, + //$$ updateJSON = "https://raw.githubusercontent.com/ReplayMod/ReplayMod/master/versions.json", //#endif - guiFactory = "com.replaymod.core.gui.GuiFactory") -public class ReplayMod { +//$$ guiFactory = "com.replaymod.core.gui.GuiFactory") +//#endif +public class ReplayMod implements Module { public static ModContainer getContainer() { - return Loader.instance().getIndexedModList().get(MOD_ID); + //#if MC>=11300 + return ModList.get().getModContainerById(MOD_ID).get(); + //#else + //$$ return Loader.instance().getIndexedModList().get(MOD_ID); + //#endif } @Getter(lazy = true) - private static final String minecraftVersion = Loader.MC_VERSION; + //#if MC>=11300 + private static final String minecraftVersion = MCPVersion.getMCVersion(); + //#else + //$$ private static final String minecraftVersion = Loader.MC_VERSION; + //#endif public static final String MOD_ID = "replaymod"; public static final ResourceLocation TEXTURE = new ResourceLocation("replaymod", "replay_gui.png"); public static final int TEXTURE_SIZE = 256; - private static final Minecraft mc = Minecraft.getMinecraft(); + private static final Minecraft mc = MCVer.getMinecraft(); - @Deprecated - public static Configuration config; + //#if MC<11300 + //$$ @Deprecated + //$$ public static Configuration config; + //#endif private final KeyBindingRegistry keyBindingRegistry = new KeyBindingRegistry(); private final SettingsRegistry settingsRegistry = new SettingsRegistry(); + { + settingsRegistry.register(Setting.class); + } // The instance of your mod that Forge uses. - @Instance(MOD_ID) + //#if MC>=11300 + { instance = this; } + //#else + //$$ @Instance(MOD_ID) + //#endif public static ReplayMod instance; + private final List modules = new ArrayList<>(); + + private final GuiBackgroundProcesses backgroundProcesses = new GuiBackgroundProcesses(); + + public ReplayMod() { + I18n.setI18n(net.minecraft.client.resources.I18n::format); + + //#if MC>=11300 + DeferredWorkQueue.runLater(() -> MCVer.getMinecraft().resourcePackRepository.addPackFinder(new LangResourcePack.Finder())); + //#endif + + // Register all RM modules + modules.add(this); + modules.add(new ReplayModRecording(this)); + ReplayModReplay replayModule = new ReplayModReplay(this); + modules.add(replayModule); + modules.add(new ReplayModOnline(this, replayModule)); + modules.add(new ReplayModRender(this)); + modules.add(new ReplayModSimplePathing(this)); + modules.add(new ReplayModEditor(this)); + modules.add(new ReplayModExtras(this)); + + //#if MC>=11300 + settingsRegistry.register(); + //#endif + } + + //#if MC<=11300 + //$$ @EventHandler + //$$ public void init(FMLPreInitializationEvent event) { + //$$ config = new Configuration(event.getSuggestedConfigurationFile()); + //$$ config.load(); + //$$ settingsRegistry.setConfiguration(config); + //$$ settingsRegistry.save(); // Save default values to disk + //$$ } + //#endif + public KeyBindingRegistry getKeyBindingRegistry() { return keyBindingRegistry; } @@ -114,33 +198,28 @@ public class ReplayMod { public File getReplayFolder() throws IOException { String path = getSettingsRegistry().get(Setting.RECORDING_PATH); - File folder = new File(path.startsWith("./") ? getMinecraft().mcDataDir : null, path); + File folder = new File(path.startsWith("./") ? mcDataDir(getMinecraft()) : 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); - } - //#ifdef DEV_ENV static { // Note: even preInit is too late and we'd have to issue another resource reload @SuppressWarnings("unchecked") - List defaultResourcePacks = mc.defaultResourcePacks; - FolderResourcePack jGuiResourcePack = new FolderResourcePack(new File("../jGui/src/main/resources")) { + //#if MC>=11300 + FolderPack jGuiResourcePack = new FolderPack(new File("../jGui/src/main/resources")) { @Override - protected InputStream getInputStreamByName(String resourceName) throws IOException { + protected InputStream getInputStream(String resourceName) throws IOException { try { - return super.getInputStreamByName(resourceName); + return super.getInputStream(resourceName); + //#else + //$$ List defaultResourcePacks = mc.defaultResourcePacks; + //$$ FolderResourcePack jGuiResourcePack = new FolderResourcePack(new File("../jGui/src/main/resources")) { + //$$ @Override + //$$ protected InputStream getInputStreamByName(String resourceName) throws IOException { + //$$ try { + //$$ return super.getInputStreamByName(resourceName); + //#endif } catch (IOException e) { if ("pack.mcmeta".equals(resourceName)) { return new ByteArrayInputStream(("{\"pack\": {\"description\": \"dummy pack for jGui resources in dev-env\", \"pack_format\": 1}}").getBytes(Charsets.UTF_8)); @@ -149,7 +228,16 @@ public class ReplayMod { } } }; - defaultResourcePacks.add(jGuiResourcePack); + //#if MC>=11300 + mc.resourcePackRepository.addPackFinder(new IPackFinder() { + @Override + public void addPackInfosToMap(Map map, ResourcePackInfo.IFactory factory) { + map.put("jgui", ResourcePackInfo.func_195793_a("jgui", true, () -> jGuiResourcePack, factory, ResourcePackInfo.Priority.BOTTOM)); + } + }); + //#else + //$$ defaultResourcePacks.add(jGuiResourcePack); + //#endif //#if MC<=10710 //$$ FolderResourcePack mainResourcePack = new FolderResourcePack(new File("../src/main/resources")) { //$$ @Override @@ -169,30 +257,43 @@ public class ReplayMod { } //#endif - @EventHandler - public void init(FMLInitializationEvent event) { - getSettingsRegistry().register(Setting.class); + //#if MC>=11300 + { + FMLJavaModLoadingContext.get().getModEventBus().addListener((FMLCommonSetupEvent event) -> modules.forEach(Module::initCommon)); + FMLJavaModLoadingContext.get().getModEventBus().addListener((FMLClientSetupEvent event) -> modules.forEach(Module::initClient)); + FMLJavaModLoadingContext.get().getModEventBus().addListener((FMLClientSetupEvent event) -> modules.forEach(m -> m.registerKeyBindings(keyBindingRegistry))); + } + //#else + //$$ @EventHandler + //$$ public void init(FMLInitializationEvent event) { + //$$ modules.forEach(Module::initCommon); + //$$ modules.forEach(Module::initClient); + //$$ modules.forEach(m -> m.registerKeyBindings(keyBindingRegistry)); + //$$ } + //#endif + @Override + public void registerKeyBindings(KeyBindingRegistry registry) { + registry.registerKeyBinding("replaymod.input.settings", 0, () -> { + new GuiReplaySettings(null, settingsRegistry).display(); + }); + } + + @Override + public void initClient() { new MainMenuHandler().register(); //#if MC<=10710 //$$ FML_BUS.register(this); // For runLater(Runnable) //#endif + FML_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 + FORGE_BUS.register(backgroundProcesses); // 1.7.10 crashes when render distance > 16 //#if MC>=10800 - if(!FMLClientHandler.instance().hasOptifine()) - GameSettings.Options.RENDER_DISTANCE.setValueMax(64f); + //if(!FMLClientHandler.instance().hasOptifine()) FIXME 1.13 update + GameSettings.Options.RENDER_DISTANCE.setValueMax(64f); //#endif testIfMoeshAndExitMinecraft(); @@ -240,19 +341,18 @@ public class ReplayMod { public void runLater(Runnable runnable) { if (mc.isCallingFromMinecraftThread() && inRunLater) { - EventBus bus = FORGE_BUS; //#if MC>=10800 - bus.register(new Object() { + FORGE_BUS.register(new Object() { @SubscribeEvent public void onRenderTick(TickEvent.RenderTickEvent event) { if (event.phase == TickEvent.Phase.START) { runLater(runnable); - bus.unregister(this); + FORGE_BUS.unregister(this); } } }); //#else - //$$ bus.register(new RunLaterHelper(runnable)); + //$$ FORGE_BUS.register(new RunLaterHelper(runnable)); //#endif return; } @@ -309,7 +409,11 @@ public class ReplayMod { //#endif public String getVersion() { - return getContainer().getVersion(); + //#if MC>=11300 + return getContainer().getModInfo().getVersion().toString(); + //#else + //$$ return getContainer().getVersion(); + //#endif } private void testIfMoeshAndExitMinecraft() { @@ -355,4 +459,8 @@ public class ReplayMod { mc.ingameGUI.getChatGUI().printChatMessage(text); } } + + public GuiBackgroundProcesses getBackgroundProcesses() { + return backgroundProcesses; + } } diff --git a/src/main/java/com/replaymod/core/SettingsRegistry.java b/src/main/java/com/replaymod/core/SettingsRegistry.java index 55be8abc..99885bf7 100644 --- a/src/main/java/com/replaymod/core/SettingsRegistry.java +++ b/src/main/java/com/replaymod/core/SettingsRegistry.java @@ -1,30 +1,67 @@ package com.replaymod.core; import com.replaymod.core.events.SettingsChangedEvent; -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; +//#if MC>=11300 +import net.minecraftforge.common.ForgeConfigSpec; +import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.fml.config.ModConfig; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +//#else +//$$ import net.minecraftforge.common.config.Configuration; +//#endif + import static com.replaymod.core.versions.MCVer.*; public class SettingsRegistry { private static final Object NULL_OBJECT = new Object(); private Map, Object> settings = new ConcurrentHashMap<>(); - private Configuration configuration; + //#if MC>=11300 + private ForgeConfigSpec spec; + private ModConfig config; + //#else + //$$ private Configuration configuration; + //#endif - public void setConfiguration(Configuration configuration) { - this.configuration = configuration; + //#if MC>=11300 + public void register() { + if (spec == null) { + ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); + for (SettingKey key : settings.keySet()) { + builder + .translation(key.getDisplayString()) + .define(key.getCategory() + "." + key.getKey(), key.getDefault()); + } + spec = builder.build(); + } + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::load); + ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, spec); + } - List> keys = new ArrayList<>(settings.keySet()); - settings.clear(); - for (SettingKey key : keys) { - register(key); + private void load(ModConfig.Loading event) { + config = event.getConfig(); + for (Map.Entry, Object> entry : settings.entrySet()) { + SettingKey key = entry.getKey(); + Object value = config.getConfigData().get(key.getCategory() + "." + key.getKey()); + entry.setValue(value == null ? key.getDefault() : value); } } + //#else + //$$ public void setConfiguration(Configuration configuration) { + //$$ this.configuration = configuration; + //$$ + //$$ List> keys = new ArrayList<>(settings.keySet()); + //$$ settings.clear(); + //$$ for (SettingKey key : keys) { + //$$ register(key); + //$$ } + //$$ } + //#endif public void register(Class settingsClass) { for (Field field : settingsClass.getDeclaredFields()) { @@ -40,23 +77,30 @@ public class SettingsRegistry { } public void register(SettingKey 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; + //#if MC>=11300 + if (spec != null) { + throw new IllegalStateException("Cannot register more settings are spec has been built."); } - settings.put(key, value); + settings.put(key, NULL_OBJECT); + //#else + //$$ 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); + //#endif } public Set> getSettings() { @@ -72,23 +116,35 @@ public class SettingsRegistry { } public void set(SettingKey key, T 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."); + //#if MC>=11300 + if (config != null) { + config.getConfigData().set(key.getCategory() + "." + key.getKey(), value); } + //#else + //$$ 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."); + //$$ } + //#endif settings.put(key, value); FML_BUS.post(new SettingsChangedEvent(this, key)); } public void save() { - configuration.save(); + //#if MC>=11300 + if (config != null) { + config.save(); + } + //#else + //$$ configuration.save(); + //#endif } public interface SettingKey { @@ -127,7 +183,7 @@ public class SettingsRegistry { @Override public String getDisplayString() { - return displayString == null ? null : I18n.format(displayString); + return displayString; } @Override diff --git a/src/main/java/com/replaymod/core/events/SettingsChangedEvent.java b/src/main/java/com/replaymod/core/events/SettingsChangedEvent.java index 47105893..1c8f00c5 100644 --- a/src/main/java/com/replaymod/core/events/SettingsChangedEvent.java +++ b/src/main/java/com/replaymod/core/events/SettingsChangedEvent.java @@ -5,7 +5,11 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.Event; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.Event; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.Event; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.Event; //#endif diff --git a/src/main/java/com/replaymod/core/gui/GuiBackgroundProcesses.java b/src/main/java/com/replaymod/core/gui/GuiBackgroundProcesses.java new file mode 100644 index 00000000..d0754278 --- /dev/null +++ b/src/main/java/com/replaymod/core/gui/GuiBackgroundProcesses.java @@ -0,0 +1,135 @@ +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.lwjgl.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; +import net.minecraftforge.client.event.GuiScreenEvent; +import net.minecraftforge.common.MinecraftForge; + +//#if MC>=10800 +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif +//#else +//$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; +//#endif + +import static com.replaymod.core.versions.MCVer.getGui; +import static com.replaymod.core.versions.MCVer.getMinecraft; + +public class GuiBackgroundProcesses { + private GuiPanel panel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(10)); + + public void register() { + MinecraftForge.EVENT_BUS.register(this); + } + + public void unregister() { + MinecraftForge.EVENT_BUS.unregister(this); + } + + @SubscribeEvent + public void onGuiInit(GuiScreenEvent.InitGuiEvent.Post event) { + if (getGui(event) != getMinecraft().currentScreen) return; // people tend to construct GuiScreens without opening them + + VanillaGuiScreen.setup(getGui(event)).setLayout(new CustomLayout() { + @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 { + private GuiElement inner; + + Element(GuiElement inner) { + this.inner = inner; + addElements(null, inner); + + setLayout(new CustomLayout() { + @Override + protected void layout(Element container, int width, int height) { + pos(inner, 10, 10); + size(inner, width - 20, height - 20); + } + }); + } + + @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; + } + } +} diff --git a/src/main/java/com/replaymod/core/gui/GuiReplaySettings.java b/src/main/java/com/replaymod/core/gui/GuiReplaySettings.java index a4621a7c..921ae1f4 100644 --- a/src/main/java/com/replaymod/core/gui/GuiReplaySettings.java +++ b/src/main/java/com/replaymod/core/gui/GuiReplaySettings.java @@ -41,7 +41,7 @@ public class GuiReplaySettings extends AbstractGuiScreen { @SuppressWarnings("unchecked") final SettingsRegistry.SettingKey booleanKey = (SettingsRegistry.SettingKey) key; final GuiToggleButton button = new GuiToggleButton<>().setSize(150, 20) - .setLabel(key.getDisplayString()).setSelected(settingsRegistry.get(booleanKey) ? 0 : 1) + .setI18nLabel(key.getDisplayString()).setSelected(settingsRegistry.get(booleanKey) ? 0 : 1) .setValues(I18n.format("options.on"), I18n.format("options.off")); element = button.onClick(new Runnable() { @Override @@ -60,7 +60,7 @@ public class GuiReplaySettings extends AbstractGuiScreen { for (int j = 0; j < entries.length; j++) { Object value = values.get(j); entries[j] = new MultipleChoiceDropdownEntry(value, - multipleChoiceKey.getDisplayString() + ": " + I18n.format(value.toString())); + I18n.format(multipleChoiceKey.getDisplayString()) + ": " + I18n.format(value.toString())); if (currentValue.equals(value)) { selected = j; } diff --git a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java index d922c1e4..62833344 100644 --- a/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java +++ b/src/main/java/com/replaymod/core/gui/RestoreReplayGui.java @@ -43,6 +43,9 @@ public class RestoreReplayGui extends AbstractGuiScreen { yesButton.onClick(() -> { try { ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), null, file); + // 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(); ReplayMetaData metaData = replayFile.getMetaData(); if (metaData != null && metaData.getDuration() == 0) { // Try to restore replay duration diff --git a/src/main/java/com/replaymod/core/handler/MainMenuHandler.java b/src/main/java/com/replaymod/core/handler/MainMenuHandler.java index 9a46cfc2..42ec9dbb 100644 --- a/src/main/java/com/replaymod/core/handler/MainMenuHandler.java +++ b/src/main/java/com/replaymod/core/handler/MainMenuHandler.java @@ -1,5 +1,6 @@ package com.replaymod.core.handler; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiMainMenu; import net.minecraft.client.gui.GuiScreen; @@ -9,13 +10,15 @@ import net.minecraftforge.common.MinecraftForge; import org.lwjgl.opengl.GL11; //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; //#endif -import java.io.IOException; - import static com.replaymod.core.versions.MCVer.*; /** @@ -37,17 +40,18 @@ public class MainMenuHandler { if (x(button) + button.width < gui.width / 2 - 100 || x(button) > gui.width / 2 + 100 || y(button) > gui.height / 4 + 10 + 4 * 24) continue; - // Move button up to make space for two rows of buttons + // Move button up to make space for one rows of buttons // and then move back down by 10 to compensate for the space to the exit button that was already there - int offset = -2 * 24 + 10; + int offset = -1 * 24 + 10; y(button, y(button) + offset); - //#if MC>=11202 - if (button == gui.realmsButton) { + + //#if MC>=11300 + if (button.id == 14) { realmsOffset = offset; } //#endif } - //#if MC>=11202 + //#if MC>=11300 if (realmsOffset != 0 && gui.realmsNotification instanceof GuiScreenRealmsProxy) { gui.realmsNotification = new RealmsNotificationProxy((GuiScreenRealmsProxy) gui.realmsNotification, realmsOffset); } @@ -55,7 +59,7 @@ public class MainMenuHandler { } } - //#if MC>=11202 + //#if MC>=11300 private static class RealmsNotificationProxy extends GuiScreen { private final GuiScreenRealmsProxy proxy; private final int offset; @@ -66,30 +70,25 @@ public class MainMenuHandler { } @Override - public void setGuiSize(int w, int h) { - proxy.setGuiSize(w, h); + public void setWorldAndResolution(Minecraft mc, int width, int height) { + proxy.setWorldAndResolution(mc, width, height); } @Override - public void initGui() { - proxy.initGui(); + public void tick() { + proxy.tick(); } @Override - public void updateScreen() { - proxy.updateScreen(); - } - - @Override - public void drawScreen(int mouseX, int mouseY, float partialTicks) { + public void render(int mouseX, int mouseY, float partialTicks) { GL11.glTranslated(0, offset, 0); - proxy.drawScreen(mouseX, mouseY - offset, partialTicks); + proxy.render(mouseX, mouseY - offset, partialTicks); GL11.glTranslated(0, -offset, 0); } @Override - public void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { - proxy.mouseClicked(mouseX, mouseY - offset, mouseButton); + public boolean mouseClicked(double mouseX, double mouseY, int mouseButton) { + return proxy.mouseClicked(mouseX, mouseY - offset, mouseButton); } @Override diff --git a/src/main/java/com/replaymod/core/utils/ModCompat.java b/src/main/java/com/replaymod/core/utils/ModCompat.java index 26dcba5b..41813c1c 100644 --- a/src/main/java/com/replaymod/core/utils/ModCompat.java +++ b/src/main/java/com/replaymod/core/utils/ModCompat.java @@ -11,11 +11,16 @@ import net.minecraftforge.registries.RegistryManager; //#else //$$ import cpw.mods.fml.common.registry.GameData; //#endif +//$$ import java.util.stream.Stream; //#endif //#if MC>=10800 -import net.minecraftforge.fml.common.Loader; -import net.minecraftforge.fml.common.ModContainer; +//#if MC>=11300 +import net.minecraftforge.fml.ModList; +//#else +//$$ import net.minecraftforge.fml.common.Loader; +//$$ import net.minecraftforge.fml.common.ModContainer; +//#endif //#else //$$ import cpw.mods.fml.common.Loader; //$$ import cpw.mods.fml.common.ModContainer; @@ -24,22 +29,32 @@ import net.minecraftforge.fml.common.ModContainer; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; public class ModCompat { @SuppressWarnings("unchecked") public static Collection getInstalledNetworkMods() { - Map ignoreCaseMap = Loader.instance().getModList().stream() - .collect(Collectors.toMap(m -> m.getModId().toLowerCase(), Function.identity())); - //#if MC>=11200 + //#if MC>=11300 + Map modInfoMap = ModList.get().getMods().stream() + .map(m -> new ModInfo(m.getModId(), m.getDisplayName(), m.getVersion().toString())) + .collect(Collectors.toMap(ModInfo::getId, Function.identity())); return RegistryManager.ACTIVE.takeSnapshot(false).keySet().stream() .map(RegistryManager.ACTIVE::getRegistry) .map(ForgeRegistry::getKeys).flatMap(Set::stream) - .map(ResourceLocation::getResourceDomain).filter(s -> !s.equals("minecraft")).distinct() - .map(String::toLowerCase).map(ignoreCaseMap::get).filter(mod -> mod != null) - .map(mod -> new ModInfo(mod.getModId(), mod.getName(), mod.getVersion())) + .map(ResourceLocation::getNamespace).filter(s -> !s.equals("minecraft")).distinct() + .map(modInfoMap::get).filter(Objects::nonNull) .collect(Collectors.toList()); //#else + //$$ Map ignoreCaseMap = Loader.instance().getModList().stream() + //$$ .collect(Collectors.toMap(m -> m.getModId().toLowerCase(), Function.identity())); + //#if MC>=11200 + //$$ return RegistryManager.ACTIVE.takeSnapshot(false).keySet().stream() + //$$ .map(RegistryManager.ACTIVE::getRegistry) + //$$ .map(ForgeRegistry::getKeys).flatMap(Set::stream) + //$$ .map(ResourceLocation::getResourceDomain).filter(s -> !s.equals("minecraft")).distinct() + //$$ .map(String::toLowerCase).map(ignoreCaseMap::get).filter(mod -> mod != null) + //$$ .map(mod -> new ModInfo(mod.getModId(), mod.getName(), mod.getVersion())) + //$$ .collect(Collectors.toList()); + //#else //#if MC>=10800 //$$ return Stream.concat( //$$ ((Set) GameData.getBlockRegistry().getKeys()).stream(), @@ -61,6 +76,7 @@ public class ModCompat { //$$ .collect(Collectors.toList()); //#endif //#endif + //#endif } public static final class ModInfoDifference { diff --git a/src/main/java/com/replaymod/core/utils/OpenGLUtils.java b/src/main/java/com/replaymod/core/utils/OpenGLUtils.java deleted file mode 100644 index 4e209865..00000000 --- a/src/main/java/com/replaymod/core/utils/OpenGLUtils.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.replaymod.core.utils; - -import org.lwjgl.BufferUtils; -import org.lwjgl.opengl.GL11; - -import java.nio.IntBuffer; - -public class OpenGLUtils { - public static final int VIEWPORT_MAX_WIDTH; - public static final int VIEWPORT_MAX_HEIGHT; - - static { - IntBuffer buffer = BufferUtils.createIntBuffer(16); - GL11.glGetInteger(GL11.GL_MAX_VIEWPORT_DIMS, buffer); - VIEWPORT_MAX_WIDTH = buffer.get(); - VIEWPORT_MAX_HEIGHT = buffer.get(); - } - - /** - * Magic init method which has to be called from the OpenGL thread so the variables in this class - * can be initialized successfully. - * Does not perform any work on its own. - */ - public static void init() { - } -} diff --git a/src/main/java/com/replaymod/core/utils/Restrictions.java b/src/main/java/com/replaymod/core/utils/Restrictions.java index c198baf6..1deccf7f 100644 --- a/src/main/java/com/replaymod/core/utils/Restrictions.java +++ b/src/main/java/com/replaymod/core/utils/Restrictions.java @@ -3,6 +3,7 @@ package com.replaymod.core.utils; import net.minecraft.network.PacketBuffer; //#if MC>=10904 import net.minecraft.network.play.server.SPacketCustomPayload; +import net.minecraft.util.ResourceLocation; //#else //$$ import net.minecraft.network.play.server.S3FPacketCustomPayload; //#endif @@ -18,7 +19,11 @@ import static com.replaymod.core.versions.MCVer.readString; * @see Replay Restrictions Gist */ public class Restrictions { - public static final String PLUGIN_CHANNEL = "Replay|Restrict"; + //#if MC>=11300 + public static final ResourceLocation PLUGIN_CHANNEL = new ResourceLocation("replaymod", "restrict"); + //#else + //$$ public static final String PLUGIN_CHANNEL = "Replay|Restrict"; + //#endif private boolean noXray; private boolean noNoclip; private boolean onlyFirstPerson; diff --git a/src/main/java/com/replaymod/core/utils/Utils.java b/src/main/java/com/replaymod/core/utils/Utils.java index d173d1c8..94a22dd7 100644 --- a/src/main/java/com/replaymod/core/utils/Utils.java +++ b/src/main/java/com/replaymod/core/utils/Utils.java @@ -18,15 +18,20 @@ import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.popup.GuiInfoPopup; import de.johni0702.minecraft.gui.utils.Colors; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; +import de.johni0702.minecraft.gui.versions.MCVer; import net.minecraft.client.gui.GuiScreen; import net.minecraft.crash.CrashReport; import net.minecraft.util.ResourceLocation; import org.apache.commons.io.Charsets; import org.apache.commons.io.FilenameUtils; import org.apache.logging.log4j.Logger; -import org.lwjgl.input.Keyboard; -import org.lwjgl.util.Dimension; -import org.lwjgl.util.ReadableDimension; + +//#if MC>=11300 +//#else +//$$ import org.lwjgl.input.Keyboard; +//#endif //#if MC>=10800 import net.minecraft.client.network.NetworkPlayerInfo; @@ -64,16 +69,30 @@ import java.util.Date; import java.util.UUID; import java.util.function.Consumer; -import static net.minecraft.client.Minecraft.getMinecraft; +import static com.replaymod.core.versions.MCVer.Minecraft_mcDataDir; +import static com.replaymod.core.versions.MCVer.getMinecraft; public class Utils { + private static InputStream getResourceAsStream(String path) { + // FIXME this seems broken in 1.13, hence the workaround. probably want to open an issue with modlauncher (or forge?) + //#ifdef DEV_ENV + try { + return new java.io.FileInputStream(new File("../src/main/resources" + path)); + } catch (java.io.FileNotFoundException e) { + throw new RuntimeException(e); + } + //#else + //$$ return Utils.class.getResourceAsStream(path); + //#endif + } + public static final BufferedImage DEFAULT_THUMBNAIL; static { BufferedImage thumbnail; try { - thumbnail = ImageIO.read(Utils.class.getClassLoader().getResourceAsStream("default_thumb.jpg")); + thumbnail = ImageIO.read(getResourceAsStream("/default_thumb.jpg")); } catch (Exception e) { thumbnail = new BufferedImage(1, 1, BufferedImage.TYPE_3BYTE_BGR); e.printStackTrace(); @@ -98,7 +117,7 @@ public class Utils { static { // Largely from https://community.letsencrypt.org/t/134/37 - try (InputStream in = Utils.class.getResourceAsStream("/dst_root_ca_x3.pem")){ + try (InputStream in = getResourceAsStream("/dst_root_ca_x3.pem")){ Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(in); KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); @@ -189,7 +208,11 @@ public class Utils { } public static boolean isCtrlDown() { - return Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); + //#if MC>=11300 + return GuiScreen.isCtrlKeyDown(); + //#else + //$$ return Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); + //#endif } public static void addCallback(ListenableFuture future, Consumer onSuccess, Consumer onFailure) { @@ -216,7 +239,7 @@ public class Utils { // Try to save the crash report if (crashReport.getFile() == null) { try { - File folder = new File(getMinecraft().mcDataDir, "crash-reports"); + File folder = new File(Minecraft_mcDataDir(getMinecraft()), "crash-reports"); File file = new File(folder, "crash-" + (new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss")).format(new Date()) + "-client.txt"); logger.debug("Saving crash report to file: {}", file); crashReport.saveToFile(file); @@ -263,7 +286,7 @@ public class Utils { // Replace close button with panel containing close and copy buttons GuiButton copyToClipboardButton = new GuiButton().setI18nLabel("chat.copy").onClick(() -> - GuiScreen.setClipboardString(crashReport)).setSize(150, 20); + MCVer.setClipboardString(crashReport)).setSize(150, 20); GuiButton closeButton = getCloseButton(); popup.removeElement(closeButton); popup.addElements(new VerticalLayout.Data(1), @@ -281,4 +304,17 @@ public class Utils { super.draw(renderer, size, renderInfo); } } + + public static void throwIfInstanceOf(Throwable t, Class cls) throws T { + if (cls.isInstance(t)) { + throw cls.cast(t); + } + } + public static void throwIfUnchecked(Throwable t) { + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } else if (t instanceof Error) { + throw (Error) t; + } + } } diff --git a/src/main/java/com/replaymod/core/utils/WrappedTimer.java b/src/main/java/com/replaymod/core/utils/WrappedTimer.java index 8063d24c..929f0097 100644 --- a/src/main/java/com/replaymod/core/utils/WrappedTimer.java +++ b/src/main/java/com/replaymod/core/utils/WrappedTimer.java @@ -8,15 +8,27 @@ public class WrappedTimer extends Timer { protected final Timer wrapped; public WrappedTimer(Timer wrapped) { - super(0); + //#if MC>=11300 + super(0, 0); + //#else + //$$ super(0); + //#endif this.wrapped = wrapped; copy(wrapped, this); } @Override - public void updateTimer() { + public void updateTimer( + //#if MC>=11300 + long sysClock + //#endif + ) { copy(this, wrapped); - wrapped.updateTimer(); + wrapped.updateTimer( + //#if MC>=11300 + sysClock + //#endif + ); copy(wrapped, this); } diff --git a/src/main/java/com/replaymod/core/versions/LangResourcePack.java b/src/main/java/com/replaymod/core/versions/LangResourcePack.java new file mode 100644 index 00000000..61038bb5 --- /dev/null +++ b/src/main/java/com/replaymod/core/versions/LangResourcePack.java @@ -0,0 +1,146 @@ +//#if MC>=11300 +package com.replaymod.core.versions; + +import com.google.gson.Gson; +import com.replaymod.core.ReplayMod; +import net.minecraft.resources.AbstractResourcePack; +import net.minecraft.resources.IPackFinder; +import net.minecraft.resources.IResourcePack; +import net.minecraft.resources.ResourcePackFileNotFoundException; +import net.minecraft.resources.ResourcePackInfo; +import net.minecraft.resources.ResourcePackType; +import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.loading.moddiscovery.ModFile; +import net.minecraftforge.fml.packs.ModFileResourcePack; +import net.minecraftforge.fml.packs.ResourcePackLoader; +import org.apache.commons.io.IOUtils; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringBufferInputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Resource pack which on-the-fly converts pre-1.13 language files into 1.13 json format. + */ +public class LangResourcePack extends AbstractResourcePack { + private static final Gson GSON = new Gson(); + private static final String NAME = "replaymod_lang"; + private static final Pattern JSON_FILE_PATTERN = Pattern.compile("^assets/" + ReplayMod.MOD_ID + "/lang/([a-z][a-z])_([a-z][a-z]).json$"); + private static final Pattern LANG_FILE_NAME_PATTERN = Pattern.compile("^([a-z][a-z])_([a-z][a-z]).lang$"); + + LangResourcePack() { + super(new File(NAME)); + } + + private ModFileResourcePack getParent() { + return ResourcePackLoader.getResourcePackFor(ReplayMod.MOD_ID).orElseThrow(() -> new RuntimeException("Failed to get ReplayMod resource pack!")); + } + + private String langName(String path) { + Matcher matcher = JSON_FILE_PATTERN.matcher(path); + if (!matcher.matches()) return null; + return String.format("%s_%s.lang", matcher.group(1), matcher.group(2).toUpperCase()); + } + + private Path langPath(String path) { + ModFileResourcePack parent = getParent(); + if (parent == null) return null; + ModFile modFile = parent.getModFile(); + + String langName = langName(path); + if (langName == null) return null; + return modFile.getLocator().findPath(modFile, "assets", ReplayMod.MOD_ID, "lang", langName); + } + + private String convertValue(String value) { + return value; + } + + @Override + protected InputStream getInputStream(String path) throws IOException { + if ("pack.mcmeta".equals(path)) { + return new StringBufferInputStream("{\"pack\": {\"description\": \"ReplayMod language files\", \"pack_format\": 4}}"); + } + + Path langPath = langPath(path); + if (langPath == null) throw new ResourcePackFileNotFoundException(file, path); + + String langFile; + try (InputStream in = Files.newInputStream(langPath)) { + langFile = IOUtils.toString(in); + } + + Map properties = new HashMap<>(); + for (String line : langFile.split("\n")) { + if (line.trim().isEmpty() || line.trim().startsWith("#")) continue; + int i = line.indexOf('='); + String key = line.substring(0, i); + String value = line.substring(i + 1); + value = convertValue(value); + properties.put(key, value); + } + + return new ByteArrayInputStream(GSON.toJson(properties).getBytes(StandardCharsets.UTF_8)); + } + + @Override + protected boolean resourceExists(String path) { + Path langPath = langPath(path); + return langPath != null && Files.exists(langPath); + } + + @Override + public Collection getAllResourceLocations(ResourcePackType resourcePackType, String path, int maxDepth, Predicate filter) { + if (resourcePackType == ResourcePackType.CLIENT_RESOURCES && "lang".equals(path)) { + IResourcePack parent = getParent(); + if (parent == null) return Collections.emptyList(); + return parent.getAllResourceLocations(resourcePackType, path, maxDepth, name -> { + Matcher matcher = LANG_FILE_NAME_PATTERN.matcher(name); + if (matcher.matches()) { + return filter.test(String.format("%s_%s.json", matcher.group(1), matcher.group(1))); + } else { + return false; + } + }).stream().map(resourceLocation -> { + String p = resourceLocation.getPath().substring(0, "assets/lang/xx_XX".length()) + ".json"; + return new ResourceLocation(resourceLocation.getNamespace(), p); + }).collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } + } + + @Override + public Set getResourceNamespaces(ResourcePackType resourcePackType) { + if (resourcePackType == ResourcePackType.CLIENT_RESOURCES) { + return Collections.singleton("replaymod"); + } else { + return Collections.emptySet(); + } + } + + @Override + public void close() {} + + public static class Finder implements IPackFinder { + @Override + public void addPackInfosToMap(Map packList, ResourcePackInfo.IFactory factory) { + packList.put(NAME, ResourcePackInfo.func_195793_a(NAME, true, LangResourcePack::new, factory, ResourcePackInfo.Priority.BOTTOM)); + } + } +} +//#endif diff --git a/src/main/java/com/replaymod/core/versions/MCVer.java b/src/main/java/com/replaymod/core/versions/MCVer.java index d5b05e7f..95bc43b6 100644 --- a/src/main/java/com/replaymod/core/versions/MCVer.java +++ b/src/main/java/com/replaymod/core/versions/MCVer.java @@ -6,9 +6,6 @@ import net.minecraft.client.entity.EntityPlayerSP; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.gui.ScaledResolution; -import net.minecraft.client.model.ModelBox; -import net.minecraft.client.model.ModelRenderer; import net.minecraft.client.multiplayer.ServerData; import net.minecraft.client.multiplayer.ServerList; import net.minecraft.client.multiplayer.WorldClient; @@ -16,8 +13,8 @@ import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.entity.RenderManager; import net.minecraft.client.renderer.texture.TextureMap; -import net.minecraft.client.resources.ResourcePackRepository; import net.minecraft.client.settings.KeyBinding; +import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReportCategory; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; @@ -25,6 +22,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.network.Packet; import net.minecraft.network.PacketBuffer; import net.minecraft.util.ResourceLocation; +import net.minecraft.util.Util; import net.minecraft.world.World; import net.minecraft.world.chunk.Chunk; import net.minecraftforge.client.event.GuiScreenEvent; @@ -32,6 +30,25 @@ import net.minecraftforge.client.event.RenderGameOverlayEvent; import net.minecraftforge.client.event.RenderLivingEvent; import net.minecraftforge.common.MinecraftForge; +//#if MC>=11300 +import net.minecraft.client.MainWindow; +import net.minecraft.client.renderer.entity.model.ModelBox; +import net.minecraft.client.renderer.entity.model.ModelRenderer; +import net.minecraft.client.util.InputMappings; +import net.minecraft.crash.ReportedException; +import org.lwjgl.glfw.GLFW; +//#else +//$$ import net.minecraft.client.gui.ScaledResolution; +//$$ import net.minecraft.client.model.ModelBox; +//$$ import net.minecraft.client.model.ModelRenderer; +//$$ import net.minecraft.client.resources.ResourcePackRepository; +//$$ import net.minecraft.util.ReportedException; +//$$ import org.apache.logging.log4j.LogManager; +//$$ import org.lwjgl.Sys; +//$$ import java.awt.Desktop; +//#endif + + //#if MC>=10904 //#if MC>=11200 import net.minecraft.client.renderer.BufferBuilder; @@ -63,7 +80,11 @@ import net.minecraft.client.renderer.GlStateManager.BooleanState; import net.minecraft.client.renderer.vertex.VertexFormat; import net.minecraft.client.renderer.vertex.VertexFormatElement; import net.minecraft.world.WorldType; -import net.minecraftforge.fml.common.eventhandler.EventBus; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.IEventBus; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.EventBus; +//#endif //#else //$$ import com.google.common.util.concurrent.Futures; //$$ import com.replaymod.render.hooks.GLStateTracker; @@ -85,12 +106,17 @@ import java.util.concurrent.Callable; * Abstraction over things that have changed between different MC versions. */ public class MCVer { - public static EventBus FORGE_BUS = MinecraftForge.EVENT_BUS; + //#if MC>=11300 + public static IEventBus FORGE_BUS = MinecraftForge.EVENT_BUS; + public static IEventBus FML_BUS = FORGE_BUS; + //#else + //$$ public static EventBus FORGE_BUS = MinecraftForge.EVENT_BUS; //#if MC>=10809 - public static EventBus FML_BUS = FORGE_BUS; + //$$ public static EventBus FML_BUS = FORGE_BUS; //#else //$$ public static EventBus FML_BUS = FMLCommonHandler.instance().bus(); //#endif + //#endif public static void addDetail(CrashReportCategory category, String name, Callable callable) { //#if MC>=10904 @@ -104,6 +130,22 @@ public class MCVer { //#endif } + public static void addButton(GuiScreenEvent.InitGuiEvent event, GuiButton button) { + //#if MC>=11300 + event.addButton(button); + //#else + //$$ getButtonList(event).add(button); + //#endif + } + + public static void removeButton(GuiScreenEvent.InitGuiEvent event, GuiButton button) { + //#if MC>=11300 + event.removeButton(button); + //#else + //$$ getButtonList(event).remove(button); + //#endif + } + @SuppressWarnings("unchecked") public static List getButtonList(GuiScreenEvent.InitGuiEvent event) { //#if MC>=10904 @@ -226,6 +268,14 @@ public class MCVer { //#endif //#endif + public static File mcDataDir(Minecraft mc) { + //#if MC>=11300 + return mc.gameDir; + //#else + //$$ return mc.mcDataDir; + //#endif + } + //#if MC>=10800 public static Entity getRenderViewEntity(Minecraft mc) { return mc.getRenderViewEntity(); @@ -318,8 +368,11 @@ public class MCVer { } public static BooleanState fog() { + //#if MC>=11300 + return GlStateManager.FOG.fog; + //#else //#if MC>=10809 - return GlStateManager.fogState.fog; + //$$ return GlStateManager.fogState.fog; //#else //#if MC>=10800 //$$ return GlStateManager.fogState.field_179049_a; @@ -327,11 +380,15 @@ public class MCVer { //$$ return GLStateTracker.getInstance().fog; //#endif //#endif + //#endif } public static void fog(BooleanState fog) { + //#if MC>=11300 + GlStateManager.FOG.fog = fog; + //#else //#if MC>=10809 - GlStateManager.fogState.fog = fog; + //$$ GlStateManager.fogState.fog = fog; //#else //#if MC>=10800 //$$ GlStateManager.fogState.field_179049_a = fog; @@ -339,22 +396,31 @@ public class MCVer { //$$ GLStateTracker.getInstance().fog = fog; //#endif //#endif + //#endif } public static BooleanState texture2DState(int index) { + //#if MC>=11300 + return GlStateManager.TEXTURES[index].texture2DState; + //#else //#if MC>=10800 - return GlStateManager.textureState[index].texture2DState; + //$$ return GlStateManager.textureState[index].texture2DState; //#else //$$ return GLStateTracker.getInstance().texture[index]; //#endif + //#endif } public static void texture2DState(int index, BooleanState texture2DState) { + //#if MC>=11300 + GlStateManager.TEXTURES[index].texture2DState = texture2DState; + //#else //#if MC>=10800 - GlStateManager.textureState[index].texture2DState = texture2DState; + //$$ GlStateManager.textureState[index].texture2DState = texture2DState; //#else //$$ GLStateTracker.getInstance().texture[index] = texture2DState; //#endif + //#endif } public static void ServerList_saveSingleServer(ServerData serverData) { @@ -373,18 +439,29 @@ public class MCVer { //#endif } - public static ScaledResolution newScaledResolution(Minecraft mc) { - //#if MC>=10809 - return new ScaledResolution(mc); - //#else - //$$ return new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); - //#endif - } - public static ListenableFuture setServerResourcePack(ResourcePackRepository repo, File file) { + //#if MC>=11300 + public static MainWindow newScaledResolution(Minecraft mc) { + return mc.mainWindow; + } + //#else + //$$ public static ScaledResolution newScaledResolution(Minecraft mc) { + //#if MC>=10809 + //$$ return new ScaledResolution(mc); + //#else + //$$ return new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + //#endif + //$$ } + //#endif + + public static ListenableFuture setServerResourcePack(File file) { + //#if MC>=11300 + return getMinecraft().getPackFinder().func_195741_a(file); + //#else + //$$ ResourcePackRepository repo = getMinecraft().getResourcePackRepository(); //#if MC>=10809 //#if MC>=11200 - return repo.setServerResourcePack(file); + //$$ return repo.setServerResourcePack(file); //#else //$$ return repo.setResourcePackInstance(file); //#endif @@ -398,6 +475,7 @@ public class MCVer { //$$ return Futures.immediateFuture(null); //#endif //#endif + //#endif } public static boolean isKeyDown(KeyBinding keyBinding) { @@ -566,12 +644,36 @@ public class MCVer { public static RenderManager getRenderManager() { //#if MC>=10800 - return Minecraft.getMinecraft().getRenderManager(); + return getMinecraft().getRenderManager(); //#else //$$ return RenderManager.instance; //#endif } + public static Minecraft getMinecraft() { + //#if MC>=11300 + return Minecraft.getInstance(); + //#else + //$$ return Minecraft.getMinecraft(); + //#endif + } + + public static long milliTime() { + //#if MC>=11300 + return Util.milliTime(); + //#else + //$$ return Minecraft.getSystemTime(); + //#endif + } + + public static File Minecraft_mcDataDir(Minecraft mc) { + //#if MC>=11300 + return mc.gameDir; + //#else + //$$ return mc.mcDataDir; + //#endif + } + public static int floor(double val) { //#if MC>=11102 return MathHelper.floor(val); @@ -580,6 +682,14 @@ public class MCVer { //#endif } + public static void bindTexture(ResourceLocation texture) { + //#if MC>=11300 + getMinecraft().getTextureManager().bindTexture(texture); + //#else + //$$ getMinecraft().renderEngine.bindTexture(texture); + //#endif + } + public static float cos(float val) { return MathHelper.cos(val); } @@ -612,6 +722,40 @@ public class MCVer { //#endif } + public static ReportedException newReportedException(CrashReport crashReport) { + return new ReportedException(crashReport); + } + + public static void openFile(File file) { + //#if MC>=11300 + Util.getOSType().openFile(file); + //#else + //$$ String path = file.getAbsolutePath(); + //$$ + //$$ // First try OS specific methods + //$$ try { + //$$ switch (Util.getOSType()) { + //$$ case WINDOWS: + //$$ Runtime.getRuntime().exec(String.format("cmd.exe /C start \"Open file\" \"%s\"", path)); + //$$ return; + //$$ case OSX: + //$$ Runtime.getRuntime().exec(new String[]{"/usr/bin/open", path}); + //$$ return; + //$$ } + //$$ } catch (IOException e) { + //$$ LogManager.getLogger().error("Cannot open file", e); + //$$ } + //$$ + //$$ // Otherwise try to java way + //$$ try { + //$$ Desktop.getDesktop().browse(file.toURI()); + //$$ } catch (Throwable throwable) { + //$$ // And if all fails, lwjgl + //$$ Sys.openURL("file://" + path); + //$$ } + //#endif + } + //#if MC<=10710 //$$ public static class GlStateManager { //$$ public static void resetColor() { /* nop */ } @@ -628,4 +772,59 @@ public class MCVer { //$$ public static void rotate(float angle, float x, float y, float z) { glRotatef(angle, x, y, z); } //$$ } //#endif + + //#if MC>=11300 + public static void color(float r, float g, float b, float a) { GlStateManager.color4f(r, g, b, a); } + public static void enableAlpha() { GlStateManager.enableAlphaTest(); } + public static void disableAlpha() { GlStateManager.disableAlphaTest(); } + public static void tryBlendFuncSeparate(int l, int r, int vl, int vr) { GlStateManager.blendFuncSeparate(l, r, vl, vr); } + public static void colorLogicOp(int op) { GlStateManager.logicOp(op); } + + public static abstract class Keyboard { + public static final int KEY_LCONTROL = GLFW.GLFW_KEY_LEFT_CONTROL; + public static final int KEY_LSHIFT = GLFW.GLFW_KEY_LEFT_SHIFT; + public static final int KEY_ESCAPE = GLFW.GLFW_KEY_ESCAPE; + public static final int KEY_HOME = GLFW.GLFW_KEY_HOME; + public static final int KEY_END = GLFW.GLFW_KEY_END; + public static final int KEY_UP = GLFW.GLFW_KEY_UP; + public static final int KEY_DOWN = GLFW.GLFW_KEY_DOWN; + public static final int KEY_LEFT = GLFW.GLFW_KEY_LEFT; + public static final int KEY_RIGHT = GLFW.GLFW_KEY_RIGHT; + public static final int KEY_BACK = GLFW.GLFW_KEY_BACKSPACE; + public static final int KEY_DELETE = GLFW.GLFW_KEY_DELETE; + public static final int KEY_RETURN = GLFW.GLFW_KEY_ENTER; + public static final int KEY_TAB = GLFW.GLFW_KEY_TAB; + public static final int KEY_F1 = GLFW.GLFW_KEY_F1; + public static final int KEY_A = GLFW.GLFW_KEY_A; + public static final int KEY_B = GLFW.GLFW_KEY_B; + public static final int KEY_C = GLFW.GLFW_KEY_C; + public static final int KEY_D = GLFW.GLFW_KEY_D; + public static final int KEY_E = GLFW.GLFW_KEY_E; + public static final int KEY_F = GLFW.GLFW_KEY_F; + public static final int KEY_G = GLFW.GLFW_KEY_G; + public static final int KEY_H = GLFW.GLFW_KEY_H; + public static final int KEY_I = GLFW.GLFW_KEY_I; + public static final int KEY_J = GLFW.GLFW_KEY_J; + public static final int KEY_K = GLFW.GLFW_KEY_K; + public static final int KEY_L = GLFW.GLFW_KEY_L; + public static final int KEY_M = GLFW.GLFW_KEY_M; + public static final int KEY_N = GLFW.GLFW_KEY_N; + public static final int KEY_O = GLFW.GLFW_KEY_O; + public static final int KEY_P = GLFW.GLFW_KEY_P; + public static final int KEY_Q = GLFW.GLFW_KEY_Q; + public static final int KEY_R = GLFW.GLFW_KEY_R; + public static final int KEY_S = GLFW.GLFW_KEY_S; + public static final int KEY_T = GLFW.GLFW_KEY_T; + public static final int KEY_U = GLFW.GLFW_KEY_U; + public static final int KEY_V = GLFW.GLFW_KEY_V; + public static final int KEY_W = GLFW.GLFW_KEY_W; + public static final int KEY_X = GLFW.GLFW_KEY_X; + public static final int KEY_Y = GLFW.GLFW_KEY_Y; + public static final int KEY_Z = GLFW.GLFW_KEY_Z; + + public static boolean isKeyDown(int keyCode) { + return InputMappings.isKeyDown(keyCode); + } + } + //#endif } diff --git a/src/main/java/com/replaymod/editor/ReplayModEditor.java b/src/main/java/com/replaymod/editor/ReplayModEditor.java index 7756dbd6..6e5b0304 100644 --- a/src/main/java/com/replaymod/editor/ReplayModEditor.java +++ b/src/main/java/com/replaymod/editor/ReplayModEditor.java @@ -1,49 +1,29 @@ package com.replaymod.editor; +import com.replaymod.core.Module; import com.replaymod.core.ReplayMod; import com.replaymod.editor.handler.GuiHandler; import com.replaymod.online.Setting; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -//#if MC>=10800 -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.event.FMLInitializationEvent; -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; -//#else -//$$ import cpw.mods.fml.common.Mod; -//$$ import cpw.mods.fml.common.event.FMLInitializationEvent; -//$$ import cpw.mods.fml.common.event.FMLPreInitializationEvent; -//#endif - -@Mod(modid = ReplayModEditor.MOD_ID, - version = "@MOD_VERSION@", - acceptedMinecraftVersions = "@MC_VERSION@", - acceptableRemoteVersions = "*", - //#if MC>=10800 - clientSideOnly = true, - //#endif - useMetadata = true) -public class ReplayModEditor { - public static final String MOD_ID = "replaymod-editor"; - - @Mod.Instance(MOD_ID) +public class ReplayModEditor implements Module { + { instance = this; } public static ReplayModEditor instance; private ReplayMod core; - public static Logger LOGGER; + public static Logger LOGGER = LogManager.getLogger(); - @Mod.EventHandler - public void preInit(FMLPreInitializationEvent event) { - ReplayModEditor.LOGGER = event.getModLog(); - core = ReplayMod.instance; + public ReplayModEditor(ReplayMod core) { + this.core = core; core.getSettingsRegistry().register(Setting.class); } - @Mod.EventHandler - public void init(FMLInitializationEvent event) { - new GuiHandler(this).register(); + @Override + public void initClient() { + new GuiHandler().register(); } public ReplayMod getCore() { diff --git a/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java b/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java new file mode 100644 index 00000000..9045f32c --- /dev/null +++ b/src/main/java/com/replaymod/editor/gui/GuiEditReplay.java @@ -0,0 +1,262 @@ +package com.replaymod.editor.gui; + +import com.replaymod.core.ReplayMod; +import com.replaymod.core.utils.Utils; +import com.replaymod.editor.ReplayModEditor; +import com.replaymod.replay.gui.overlay.GuiMarkerTimeline; +import com.replaymod.replaystudio.data.Marker; +import com.replaymod.replaystudio.replay.ZipReplayFile; +import com.replaymod.replaystudio.studio.ReplayStudio; +import de.johni0702.minecraft.gui.GuiRenderer; +import de.johni0702.minecraft.gui.container.GuiContainer; +import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.element.GuiButton; +import de.johni0702.minecraft.gui.element.GuiHorizontalScrollbar; +import de.johni0702.minecraft.gui.element.GuiTexturedButton; +import de.johni0702.minecraft.gui.element.GuiTooltip; +import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar; +import de.johni0702.minecraft.gui.element.advanced.GuiTimelineTime; +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.popup.AbstractGuiPopup; +import de.johni0702.minecraft.gui.utils.lwjgl.Color; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; +import net.minecraft.crash.CrashReport; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class GuiEditReplay extends AbstractGuiPopup { + private final Path inputPath; + + private final EditTimeline timeline; + + private final GuiTimelineTime timelineTime = new GuiTimelineTime<>(); + + private final GuiHorizontalScrollbar scrollbar = new GuiHorizontalScrollbar().setSize(300, 9); + + + private final GuiTexturedButton zoomInButton = new GuiTexturedButton().setSize(9, 9) + .onClick(() -> zoomTimeline(2d / 3d)) + .setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 20) + .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.zoomin")); + + private final GuiTexturedButton zoomOutButton = new GuiTexturedButton().setSize(9, 9) + .onClick(() -> zoomTimeline(3d / 2d)) + .setTexture(ReplayMod.TEXTURE, ReplayMod.TEXTURE_SIZE).setTexturePosH(40, 30) + .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.ingame.menu.zoomout")); + + private final GuiPanel zoomButtonPanel = new GuiPanel() + .setLayout(new VerticalLayout(VerticalLayout.Alignment.CENTER).setSpacing(2)) + .addElements(null, zoomInButton, zoomOutButton); + + private Set markers; + + protected GuiEditReplay(GuiContainer container, Path inputPath) throws IOException { + super(container); + this.inputPath = inputPath; + + try (ZipReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), inputPath.toFile())) { + markers = replayFile.getMarkers().or(HashSet::new); + timeline = new EditTimeline(new HashSet<>(markers), markers -> this.markers = markers); + timeline.setSize(300, 20) + .setMarkers() + .setLength(replayFile.getMetaData().getDuration()) + .onClick(timeline::setCursorPosition); + } + + timelineTime.setTimeline(timeline); + + scrollbar.onValueChanged(() -> { + timeline.setOffset((int) (scrollbar.getPosition() * timeline.getLength())); + timeline.setZoom(scrollbar.getZoom()); + }).setZoom(1); + + GuiPanel timelinePanel = new GuiPanel() + .setSize(300, 40) + .setLayout(new CustomLayout() { + @Override + protected void layout(GuiPanel container, int width, int height) { + pos(zoomButtonPanel, width - width(zoomButtonPanel), 10); + pos(timelineTime, 0, 2); + size(timelineTime, x(zoomButtonPanel), 8); + pos(timeline, 0, y(timelineTime) + height(timelineTime)); + size(timeline, x(zoomButtonPanel) - 2, 20); + pos(scrollbar, 0, y(timeline) + height(timeline) + 1); + size(scrollbar, x(zoomButtonPanel) - 2, 9); + } + }) + .addElements(null, timelineTime, timeline, scrollbar, zoomButtonPanel); + + GuiButton buttonAddSplit = new GuiButton() + .setSize(100, 20) + .setI18nLabel("replaymod.gui.edit.split") + .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.edit.split.tooltip")) + .onClick(() -> { + Marker marker = new Marker(); + marker.setTime(timeline.getCursorPosition()); + marker.setName(MarkerProcessor.MARKER_NAME_SPLIT); + timeline.addMarker(marker); + }); + + GuiButton buttonInsertCut = new GuiButton() + .setSize(100, 20) + .setI18nLabel("replaymod.gui.edit.cut.start") + .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.edit.cut.tooltip")) + .onClick(() -> { + Marker marker = new Marker(); + marker.setTime(timeline.getCursorPosition()); + marker.setName(MarkerProcessor.MARKER_NAME_START_CUT); + timeline.addMarker(marker); + }); + + GuiButton buttonEndCut = new GuiButton() + .setSize(100, 20) + .setI18nLabel("replaymod.gui.edit.cut.end") + .setTooltip(new GuiTooltip().setI18nText("replaymod.gui.edit.cut.tooltip")) + .onClick(() -> { + Marker marker = new Marker(); + marker.setTime(timeline.getCursorPosition()); + marker.setName(MarkerProcessor.MARKER_NAME_END_CUT); + timeline.addMarker(marker); + }); + + GuiPanel controlPanel = new GuiPanel() + .setLayout(new HorizontalLayout().setSpacing(4)) + .addElements(null, buttonAddSplit, buttonInsertCut, buttonEndCut); + + GuiButton applyButton = new GuiButton().setI18nLabel("replaymod.gui.edit.apply").setSize(150, 20).onClick(this::apply); + GuiButton closeButton = new GuiButton().setI18nLabel("replaymod.gui.close").setSize(150, 20).onClick(this::close); + GuiPanel buttonPanel = new GuiPanel() + .setLayout(new HorizontalLayout().setSpacing(8)) + .addElements(null, applyButton, closeButton); + + popup.setLayout(new VerticalLayout(VerticalLayout.Alignment.TOP).setSpacing(10)); + popup.addElements(new VerticalLayout.Data(0.5), timelinePanel, controlPanel, buttonPanel); + } + + private void zoomTimeline(double factor) { + scrollbar.setZoom(scrollbar.getZoom() * factor); + } + + private void apply() { + ProgressPopup progressPopup = new ProgressPopup(this); + + new Thread(() -> { + try (ZipReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), inputPath.toFile())) { + replayFile.writeMarkers(markers); + replayFile.save(); + } catch (IOException e) { + Utils.error(ReplayModEditor.LOGGER, this, CrashReport.makeCrashReport(e, "Writing markers"), this::close); + } + + try { + MarkerProcessor.apply(inputPath, progressPopup.progressBar::setProgress); + } catch (IOException e) { + Utils.error(ReplayModEditor.LOGGER, this, CrashReport.makeCrashReport(e, "Running marker processor"), this::close); + } + + ReplayMod.instance.runLater(() -> { + progressPopup.close(); + close(); + }); + }).start(); + } + + @Override + public void open() { + super.open(); + } + + @Override + protected GuiEditReplay getThis() { + return this; + } + + private class ProgressPopup extends AbstractGuiPopup { + private final GuiProgressBar progressBar = new GuiProgressBar(popup).setSize(300, 20); + + ProgressPopup(GuiContainer container) { + super(container); + open(); + } + + @Override + public void close() { + super.close(); + } + + @Override + protected ProgressPopup getThis() { + return this; + } + } + + private class EditTimeline extends GuiMarkerTimeline { + EditTimeline(Set markers, Consumer> saveMarkers) { + super(markers, saveMarkers); + } + + @Override + protected void drawMarkers(GuiRenderer renderer, ReadableDimension size) { + drawCutQuads(renderer, size); + super.drawMarkers(renderer, size); + } + + @Override + protected void drawMarker(GuiRenderer renderer, ReadableDimension size, Marker marker, int markerX) { + if (MarkerProcessor.MARKER_NAME_SPLIT.equals(marker.getName())) { + int height = size.getHeight() - BORDER_BOTTOM - BORDER_TOP - MARKER_SIZE + 1; + for (int y = 0; y < height; y += 3) { + renderer.drawRect(markerX, BORDER_TOP + y, 1, 2, Color.WHITE); + } + } + + super.drawMarker(renderer, size, marker, markerX); + } + + private void drawCutQuads(GuiRenderer renderer, ReadableDimension size) { + boolean inCut = false; + int startTime = 0; + for (Marker marker : markers.stream().sorted(Comparator.comparing(Marker::getTime)).collect(Collectors.toList())) { + if (MarkerProcessor.MARKER_NAME_START_CUT.equals(marker.getName()) && !inCut) { + inCut = true; + startTime = marker.getTime(); + } else if (MarkerProcessor.MARKER_NAME_END_CUT.equals(marker.getName()) && inCut) { + drawCutQuad(renderer, size, startTime, marker.getTime()); + inCut = false; + } + } + + if (inCut) { + drawCutQuad(renderer, size, startTime, getLength()); + } + } + + private void drawCutQuad(GuiRenderer renderer, ReadableDimension size, int startFrameTime, int endFrameTime) { + int visibleWidth = size.getWidth() - BORDER_LEFT - BORDER_RIGHT; + int startTime = getOffset(); + int visibleTime = (int) (getZoom() * getLength()); + int endTime = getOffset() + visibleTime; + + if (startFrameTime >= endTime || endFrameTime <= startTime) { + return; // Segment out of display range + } + + double relativeStart = startFrameTime - startTime; + double relativeEnd = endFrameTime - startTime; + int startX = BORDER_LEFT + Math.max(0, (int) (relativeStart / visibleTime * visibleWidth) + MARKER_SIZE / 2 + 1); + int endX = BORDER_LEFT + Math.min(visibleWidth, (int) (relativeEnd / visibleTime * visibleWidth) - MARKER_SIZE / 2); + if (startX < endX) { + renderer.drawRect(startX + 1, size.getHeight() - BORDER_BOTTOM - MARKER_SIZE, + endX - startX - 2, MARKER_SIZE - 2, Color.RED); + } + } + } +} diff --git a/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java b/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java index 359c3164..d9d5c7ae 100644 --- a/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java +++ b/src/main/java/com/replaymod/editor/gui/GuiReplayEditor.java @@ -34,10 +34,10 @@ import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.popup.GuiInfoPopup; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import net.minecraft.crash.CrashReport; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.tuple.Pair; -import org.lwjgl.util.ReadableDimension; import javax.annotation.Nullable; import java.io.File; diff --git a/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java b/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java index 3947214d..975124ab 100644 --- a/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java +++ b/src/main/java/com/replaymod/editor/gui/GuiTrimPanel.java @@ -1,7 +1,10 @@ package com.replaymod.editor.gui; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; import com.google.gson.JsonObject; import com.replaymod.core.utils.Utils; +import com.replaymod.replay.gui.screen.GuiReplayViewer; import com.replaymod.replaystudio.data.Marker; import com.replaymod.replaystudio.filter.ChangeTimestampFilter; import com.replaymod.replaystudio.filter.RemoveFilter; @@ -12,21 +15,20 @@ import com.replaymod.replaystudio.replay.ZipReplayFile; import com.replaymod.replaystudio.stream.PacketStream; import com.replaymod.replaystudio.studio.ReplayStudio; import de.johni0702.minecraft.gui.container.GuiPanel; +import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiNumberField; import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu; import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; import net.minecraft.client.resources.I18n; import net.minecraft.crash.CrashReport; -import org.apache.commons.io.IOCase; -import org.apache.commons.io.filefilter.SuffixFileFilter; import org.apache.commons.lang3.tuple.Pair; -import org.lwjgl.util.Dimension; +import javax.annotation.Nullable; import java.io.File; -import java.io.FileFilter; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -50,9 +52,10 @@ public class GuiTrimPanel extends GuiPanel { private final GuiReplayEditor gui; - public final GuiDropdownMenu inputReplays = new GuiDropdownMenu(this) - .setMinSize(new Dimension(200, 20)).onSelection(i -> updateSelectedReplay()) - .setToString(f -> f == NO_REPLAY ? "" : Utils.fileNameToReplayName(f.getName())); + private final GuiButton inputReplayButton = new GuiButton(this) + .setMinSize(new Dimension(200, 20)) + .setMaxSize(new Dimension(350, 20)); + private File inputReplay = NO_REPLAY; public final GuiNumberField startHour = newGuiNumberField(); public final GuiNumberField startMin = newGuiNumberField(); @@ -104,24 +107,31 @@ public class GuiTrimPanel extends GuiPanel { endMarker.setToString(toString).setMinSize(new Dimension(100, 20)).onSelection(i -> onSelectedMarkerChanged(endMarker, endHour, endMin, endSec, endMilli)); - File[] files = null; - try { - File folder = gui.getMod().getReplayFolder(); - files = folder.listFiles((FileFilter) new SuffixFileFilter(".mcpr", IOCase.INSENSITIVE)); - } catch (IOException e) { - Utils.error(LOGGER, gui, CrashReport.makeCrashReport(e, "Listing available replays"), gui.backButton::onClick); - } - if (files == null) { - LOGGER.warn("Replay file list is null, has the replay folder been deleted?"); - files = new File[0]; - } else { - LOGGER.debug("Found {} replays in the replay folder", files.length); - } - if (files.length == 0) { - inputReplays.setValues(NO_REPLAY); - } else { - inputReplays.setValues(files); - } + inputReplayButton.onClick(() -> { + File folder; + try { + folder = gui.getMod().getReplayFolder(); + } catch (IOException e) { + Utils.error(LOGGER, gui, CrashReport.makeCrashReport(e, "Getting replay folder"), gui.backButton::onClick); + return; + } + GuiReplayViewer.GuiSelectReplayPopup popup = GuiReplayViewer.GuiSelectReplayPopup.openGui(gui, folder); + Futures.addCallback(popup.getFuture(), new FutureCallback() { + @Override + public void onSuccess(@Nullable File result) { + if (result != null) { + inputReplay = result; + updateSelectedReplay(); + updateReadyState(); + } + } + + @Override + public void onFailure(Throwable t) { + Utils.error(LOGGER, gui, CrashReport.makeCrashReport(t, "Selecting replay"), gui.backButton::onClick); + } + }); + }); updateSelectedReplay(); updateReadyState(); @@ -135,7 +145,7 @@ public class GuiTrimPanel extends GuiPanel { JsonObject config = new JsonObject(); config.addProperty("offset", -start); // Pass filters to save dialog - gui.save(inputReplays.getSelectedValue(), + gui.save(inputReplay, Pair.of(new PacketStream.FilterInfo(new SquashFilter(), -1, start), new JsonObject()), Pair.of(new PacketStream.FilterInfo(ctf, start, end), config), Pair.of(new PacketStream.FilterInfo(new RemoveFilter(), end, -1), new JsonObject()) @@ -161,7 +171,11 @@ public class GuiTrimPanel extends GuiPanel { } private void updateSelectedReplay() { - File file = inputReplays.getSelectedValue(); + File file = inputReplay; + + // Update input button label + inputReplayButton.setLabel(file == NO_REPLAY ? I18n.format("gui.none") : Utils.fileNameToReplayName(file.getName())); + // Load markers and meta data from replay file ReplayMetaData metaData; Set markers; @@ -196,6 +210,6 @@ public class GuiTrimPanel extends GuiPanel { } private void updateReadyState() { - gui.saveButton.setEnabled(inputReplays.getSelectedValue() != NO_REPLAY); + gui.saveButton.setEnabled(inputReplay != NO_REPLAY); } } diff --git a/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java b/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java new file mode 100644 index 00000000..12efe758 --- /dev/null +++ b/src/main/java/com/replaymod/editor/gui/MarkerProcessor.java @@ -0,0 +1,180 @@ +package com.replaymod.editor.gui; + +import com.replaymod.replaystudio.PacketData; +import com.replaymod.replaystudio.Studio; +import com.replaymod.replaystudio.data.Marker; +import com.replaymod.replaystudio.filter.SquashFilter; +import com.replaymod.replaystudio.filter.StreamFilter; +import com.replaymod.replaystudio.io.ReplayInputStream; +import com.replaymod.replaystudio.io.ReplayOutputStream; +import com.replaymod.replaystudio.replay.ReplayMetaData; +import com.replaymod.replaystudio.replay.ZipReplayFile; +import com.replaymod.replaystudio.stream.IteratorStream; +import com.replaymod.replaystudio.studio.ReplayStudio; +import com.replaymod.replaystudio.util.Utils; +import org.apache.commons.io.FilenameUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class MarkerProcessor { + /** + * Signals the start of a section which is to be cut from the replay. + */ + public static final String MARKER_NAME_START_CUT = "_RM_START_CUT"; + /** + * Signals the end of a section which is to be cut from the replay. + */ + public static final String MARKER_NAME_END_CUT = "_RM_END_CUT"; + /** + * Signals the point at which a replay is to be split into multiple replays. + * If it is found in a section which is to be cut, that section will be cut from both replays. + */ + public static final String MARKER_NAME_SPLIT = "_RM_SPLIT"; + + private static boolean hasWork(Path path) throws IOException { + try (ZipReplayFile inputReplayFile = new ZipReplayFile(new ReplayStudio(), path.toFile())) { + return inputReplayFile.getMarkers().or(HashSet::new).stream().anyMatch(m -> m.getName().startsWith("_RM_")); + } + } + + public static void apply(Path path, Consumer progress) throws IOException { + if (!hasWork(path)) { + return; + } + + String replayName = FilenameUtils.getBaseName(path.getFileName().toString()); + int splitCounter = 0; + + Studio studio = new ReplayStudio(); + SquashFilter squashFilter = new SquashFilter(); + squashFilter.init(studio, null); + + Path inputPath = path.resolveSibling("raw").resolve(path.getFileName()); + Files.createDirectories(inputPath.getParent()); + Files.move(path, inputPath); + + try (ZipReplayFile inputReplayFile = new ZipReplayFile(studio, inputPath.toFile())) { + List markers = inputReplayFile.getMarkers().or(HashSet::new) + .stream().sorted(Comparator.comparing(Marker::getTime)).collect(Collectors.toList()); + Iterator markerIterator = markers.iterator(); + boolean anySplit = markers.stream().anyMatch(m -> m.getName().equals(MARKER_NAME_SPLIT)); + + int inputDuration = inputReplayFile.getMetaData().getDuration(); + ReplayInputStream replayInputStream = inputReplayFile.getPacketData(studio, true); + int timeOffset = 0; + SquashFilter cutFilter = null; + int startCutOffset = 0; + PacketData nextPacket = replayInputStream.readPacket(); + Marker nextMarker = markerIterator.next(); + + while (nextPacket != null) { + try (ZipReplayFile outputReplayFile = new ZipReplayFile(studio, null, + (anySplit ? path.resolveSibling(replayName + "_" + splitCounter + ".mcpr") : path).toFile())) { + long duration = 0; + Set outputMarkers = new HashSet<>(); + ReplayMetaData metaData = inputReplayFile.getMetaData(); + metaData.setDate(metaData.getDate() + timeOffset); + + try (ReplayOutputStream replayOutputStream = outputReplayFile.writePacketData(true)) { + if (cutFilter != null) { + cutFilter = squashFilter; + } else if (splitCounter > 0) { + List packets = new ArrayList<>(); + squashFilter.onEnd( + new IteratorStream(packets.listIterator(), (StreamFilter) null), + timeOffset + ); + for (PacketData packet : packets) { + replayOutputStream.write(0, packet.getPacket()); + } + } + + while (nextPacket != null) { + if (nextMarker != null && nextPacket.getTime() > nextMarker.getTime()) { + if (MARKER_NAME_START_CUT.equals(nextMarker.getName())) { + startCutOffset = nextMarker.getTime(); + cutFilter = new SquashFilter(); + } else if (MARKER_NAME_END_CUT.equals(nextMarker.getName())) { + timeOffset += nextMarker.getTime() - startCutOffset; + if (cutFilter != null) { + List packets = new ArrayList<>(); + cutFilter.onEnd( + new IteratorStream(packets.listIterator(), (StreamFilter) null), + nextMarker.getTime() + ); + for (PacketData packet : packets) { + replayOutputStream.write(nextMarker.getTime() - timeOffset, packet.getPacket()); + } + cutFilter = null; + } + } else if (MARKER_NAME_SPLIT.equals(nextMarker.getName())) { + splitCounter++; + timeOffset = nextMarker.getTime(); + startCutOffset = timeOffset; + nextMarker = markerIterator.hasNext() ? markerIterator.next() : null; + break; + } else { + nextMarker.setTime(nextMarker.getTime() - timeOffset); + outputMarkers.add(nextMarker); + } + nextMarker = markerIterator.hasNext() ? markerIterator.next() : null; + continue; + } + + squashFilter.onPacket(null, nextPacket); + if (cutFilter != null) { + if (cutFilter != squashFilter) { + cutFilter.onPacket(null, nextPacket); + } + } else { + replayOutputStream.write(nextPacket.getTime() - timeOffset, nextPacket.getPacket()); + duration = nextPacket.getTime() - timeOffset; + } + nextPacket = replayInputStream.readPacket(); + if (nextPacket != null) { + progress.accept((float) nextPacket.getTime() / (float) inputDuration); + } else { + progress.accept(1f); + } + } + } + + metaData.setDuration((int) duration); + outputReplayFile.writeMetaData(metaData); + + outputReplayFile.writeMarkers(outputMarkers); + + outputReplayFile.writeModInfo(inputReplayFile.getModInfo()); + + // We could filter these but ReplayStudio doesn't yet provide a nice method for determining + // which ones are used. + Map resourcePackIndex = inputReplayFile.getResourcePackIndex(); + if (resourcePackIndex != null) { + outputReplayFile.writeResourcePackIndex(resourcePackIndex); + for (String hash : resourcePackIndex.values()) { + try (InputStream in = inputReplayFile.getResourcePack(hash).get(); + OutputStream out = outputReplayFile.writeResourcePack(hash)) { + Utils.copy(in, out); + } + } + } + + outputReplayFile.save(); + } + } + } + } +} diff --git a/src/main/java/com/replaymod/editor/handler/GuiHandler.java b/src/main/java/com/replaymod/editor/handler/GuiHandler.java index f0fab8f1..88aa69cc 100644 --- a/src/main/java/com/replaymod/editor/handler/GuiHandler.java +++ b/src/main/java/com/replaymod/editor/handler/GuiHandler.java @@ -1,55 +1,55 @@ package com.replaymod.editor.handler; +import com.replaymod.core.utils.Utils; import com.replaymod.editor.ReplayModEditor; -import com.replaymod.editor.gui.GuiReplayEditor; +import com.replaymod.editor.gui.GuiEditReplay; +import com.replaymod.replay.gui.screen.GuiReplayViewer; +import de.johni0702.minecraft.gui.container.AbstractGuiScreen; import de.johni0702.minecraft.gui.container.GuiScreen; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.gui.GuiMainMenu; -import net.minecraft.client.resources.I18n; +import de.johni0702.minecraft.gui.element.GuiButton; +import net.minecraft.crash.CrashReport; import net.minecraftforge.client.event.GuiScreenEvent; //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; //#endif +import java.io.IOException; + import static com.replaymod.core.versions.MCVer.*; public class GuiHandler { - private static final int BUTTON_REPLAY_EDITOR = 17890237; - - private final ReplayModEditor mod; - - public GuiHandler(ReplayModEditor mod) { - this.mod = mod; - } - public void register() { FML_BUS.register(this); FORGE_BUS.register(this); } @SubscribeEvent - public void injectIntoMainMenu(GuiScreenEvent.InitGuiEvent event) { - if (!(getGui(event) instanceof GuiMainMenu)) { + public void injectIntoReplayViewer(GuiScreenEvent.InitGuiEvent.Post event) { + AbstractGuiScreen guiScreen = GuiScreen.from(getGui(event)); + if (!(guiScreen instanceof GuiReplayViewer)) { return; } - - GuiButton button = new GuiButton(BUTTON_REPLAY_EDITOR, getGui(event).width / 2 + 2, - getGui(event).height / 4 + 10 + 3 * 24, I18n.format("replaymod.gui.replayeditor")); - button.width = button.width / 2 - 2; - getButtonList(event).add(button); - } - - @SubscribeEvent - public void onButton(GuiScreenEvent.ActionPerformedEvent.Pre event) { - if(!getButton(event).enabled) return; - - if (getGui(event) instanceof GuiMainMenu) { - if (getButton(event).id == BUTTON_REPLAY_EDITOR) { - new GuiReplayEditor(GuiScreen.wrap(getGui(event)), mod.getCore()).display(); + final GuiReplayViewer replayViewer = (GuiReplayViewer) guiScreen; + // Inject Edit button + replayViewer.replaySpecificButtons.add(new GuiButton(replayViewer.editorButton).onClick(() -> { + try { + new GuiEditReplay(replayViewer, replayViewer.list.getSelected().file.toPath()) { + @Override + protected void close() { + super.close(); + replayViewer.list.load(); + } + }.open(); + } catch (IOException e) { + Utils.error(ReplayModEditor.LOGGER, replayViewer, CrashReport.makeCrashReport(e, "Opening replay editor"), () -> {}); } - } + }).setSize(73, 20).setI18nLabel("replaymod.gui.edit").setDisabled()); } } diff --git a/src/main/java/com/replaymod/extras/FullBrightness.java b/src/main/java/com/replaymod/extras/FullBrightness.java index a1610370..b06a8aa7 100644 --- a/src/main/java/com/replaymod/extras/FullBrightness.java +++ b/src/main/java/com/replaymod/extras/FullBrightness.java @@ -8,11 +8,21 @@ import com.replaymod.replay.gui.overlay.GuiReplayOverlay; import de.johni0702.minecraft.gui.element.GuiImage; import de.johni0702.minecraft.gui.element.IGuiImage; import de.johni0702.minecraft.gui.layout.HorizontalLayout; -import net.minecraft.client.settings.GameSettings; -import org.lwjgl.input.Keyboard; + +//#if MC>=11300 +import com.replaymod.core.versions.MCVer.Keyboard; +import net.minecraft.client.GameSettings; +//#else +//$$ import net.minecraft.client.settings.GameSettings; +//$$ import org.lwjgl.input.Keyboard; +//#endif //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif import net.minecraftforge.fml.common.gameevent.TickEvent; //#else //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; @@ -28,7 +38,11 @@ public class FullBrightness implements Extra { private GameSettings gameSettings; private boolean active; - private float originalGamma; + //#if MC>=11300 + private double originalGamma; + //#else + //$$ private float originalGamma; + //#endif @Override public void register(final ReplayMod mod) throws Exception { @@ -39,7 +53,11 @@ public class FullBrightness implements Extra { @Override public void run() { active = !active; - mod.getMinecraft().entityRenderer.lightmapUpdateNeeded = true; + //#if MC>=11300 + mod.getMinecraft().entityRenderer.tick(); // need to tick once to update lightmap when replay is paused + //#else + //$$ mod.getMinecraft().entityRenderer.lightmapUpdateNeeded = true; + //#endif ReplayHandler replayHandler = module.getReplayHandler(); if (replayHandler != null) { updateIndicator(replayHandler.getOverlay()); diff --git a/src/main/java/com/replaymod/extras/HotkeyButtons.java b/src/main/java/com/replaymod/extras/HotkeyButtons.java index 4f24c558..2275854e 100644 --- a/src/main/java/com/replaymod/extras/HotkeyButtons.java +++ b/src/main/java/com/replaymod/extras/HotkeyButtons.java @@ -16,13 +16,21 @@ import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.LayoutData; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import net.minecraft.client.resources.I18n; -import org.lwjgl.input.Keyboard; -import org.lwjgl.util.Dimension; -import org.lwjgl.util.ReadableDimension; + +//#if MC>=11300 +//#else +//$$ import org.lwjgl.input.Keyboard; +//#endif //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; //#endif @@ -89,7 +97,11 @@ public class HotkeyButtons implements Extra { // There doesn't seem to be an KeyBindingUpdate event, so we'll just update it every time String keyName = "???"; try { - keyName = Keyboard.getKeyName(keyBinding.getKeyCode()); + //#if MC>=11300 + keyName = keyBinding.func_197978_k(); + //#else + //$$ keyName = Keyboard.getKeyName(keyBinding.getKeyCode()); + //#endif } catch (ArrayIndexOutOfBoundsException e) { // Apparently windows likes to press strange keys, see https://www.replaymod.com/forum/thread/55 } diff --git a/src/main/java/com/replaymod/extras/LocalizationExtra.java b/src/main/java/com/replaymod/extras/LocalizationExtra.java deleted file mode 100644 index 13f4d1d2..00000000 --- a/src/main/java/com/replaymod/extras/LocalizationExtra.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.replaymod.extras; - -import com.google.common.collect.ImmutableSet; -import com.replaymod.compat.optifine.OptifineReflection; -import com.replaymod.core.ReplayMod; -import net.minecraft.client.Minecraft; -import net.minecraft.client.resources.IResourcePack; -import net.minecraft.client.resources.data.IMetadataSection; -import net.minecraft.util.ResourceLocation; -import org.apache.commons.io.IOUtils; - -//#if MC>=10904 -import net.minecraft.client.resources.data.MetadataSerializer; -//#else -//$$ import net.minecraft.client.resources.data.IMetadataSerializer; -//#endif - -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -import static com.replaymod.extras.ReplayModExtras.LOGGER; - -public class LocalizationExtra implements Extra { - private static final String ZIP_FILE_URL = "https://github.com/ReplayMod/Translations/archive/master.zip"; - private static final String LANG_PREFIX = "Translations-master/"; - - @Override - public void register(ReplayMod mod) throws Exception { - final Minecraft mc = mod.getMinecraft(); - if (Boolean.parseBoolean(System.getProperty("replaymod.offline", "false"))) { - return; - } - Thread localizedResourcePackLoader = new Thread(() -> { - try { - // Download zip of lang files - LOGGER.debug("Downloading languages from {}", ZIP_FILE_URL); - Map languages = new HashMap<>(); - try (InputStream urlIn = new URL(ZIP_FILE_URL).openStream(); - ZipInputStream in = new ZipInputStream(urlIn)) { - ZipEntry entry; - while ((entry = in.getNextEntry()) != null) { - String name = entry.getName(); - if (!name.startsWith(LANG_PREFIX) || !name.endsWith(".lang")) { - continue; - } - name = name.substring(LANG_PREFIX.length()); - languages.put(name, IOUtils.toByteArray(in)); - LOGGER.debug("Added language file {}", name); - } - } - LOGGER.debug("Downloaded {} languages", languages.size()); - - // Add lang files as resource pack - mc.addScheduledTask(() -> { - @SuppressWarnings("unchecked") - List defaultResourcePacks = mc.defaultResourcePacks; - defaultResourcePacks.add(new LocalizedResourcePack(languages)); - mc.getLanguageManager().onResourceManagerReload(mc.getResourceManager()); - // Optifine does its own, additional language data loading - OptifineReflection.reloadLang(); - LOGGER.debug("Added language files to resource packs and reloaded LanguageManager"); - }); - } catch (Throwable t) { - LOGGER.error("Loading localized resource pack:", t); - } - }, "localizedResourcePackLoader"); - localizedResourcePackLoader.setDaemon(true); - localizedResourcePackLoader.start(); - } - - public static class LocalizedResourcePack implements IResourcePack { - private final Pattern LANG_PATTERN = Pattern.compile("^lang/([.+].lang)$"); - private final Map languages; - - public LocalizedResourcePack(Map languages) { - this.languages = languages; - } - - @Override - public InputStream getInputStream(ResourceLocation loc) { - if (!"replaymod".equals(loc.getResourceDomain())) return null; - Matcher matcher = LANG_PATTERN.matcher(loc.getResourcePath()); - if (matcher.matches()) { - byte[] bytes = languages.get(matcher.group()); - if (bytes != null) { - return new ByteArrayInputStream(bytes); - } - } - return null; - } - - @Override - public boolean resourceExists(ResourceLocation loc) { - // Assumes that getInputStream returns a ByteArrayInputStream that doesn't need to be closed - return getInputStream(loc) != null; - } - - @Override - public Set getResourceDomains() { - return ImmutableSet.of("replaymod"); - } - - //#if MC>=10904 - @Override - public T getPackMetadata(MetadataSerializer metadataSerializer, String metadataSectionName) throws IOException { - //#else - //$$ @Override - //$$ public IMetadataSection getPackMetadata(IMetadataSerializer metadataSerializer, String metadataSectionName) throws IOException { - //#endif - return null; - } - - @Override - public BufferedImage getPackImage() throws IOException { - return null; - } - - @Override - public String getPackName() { - return "ReplayModLocalizationResourcePack"; - } - } -} diff --git a/src/main/java/com/replaymod/extras/OpenEyeExtra.java b/src/main/java/com/replaymod/extras/OpenEyeExtra.java index 55776d80..4d88d726 100644 --- a/src/main/java/com/replaymod/extras/OpenEyeExtra.java +++ b/src/main/java/com/replaymod/extras/OpenEyeExtra.java @@ -1,7 +1,7 @@ package com.replaymod.extras; import com.replaymod.core.ReplayMod; -import com.replaymod.core.Setting; +import com.replaymod.core.versions.MCVer; import de.johni0702.minecraft.gui.container.AbstractGuiScreen; import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiPanel; @@ -16,11 +16,15 @@ import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.utils.Colors; import org.apache.commons.io.FileUtils; +//#if MC>=11300 +import net.minecraftforge.fml.ModList; +//#else //#if MC>=10800 -import net.minecraftforge.fml.common.Loader; +//$$ import net.minecraftforge.fml.common.Loader; //#else //$$ import cpw.mods.fml.common.Loader; //#endif +//#endif import javax.net.ssl.HttpsURLConnection; import java.io.File; @@ -34,17 +38,20 @@ import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY; import static com.replaymod.extras.ReplayModExtras.LOGGER; public class OpenEyeExtra implements Extra { - private static final String DOWNLOAD_URL = "https://www.replaymod.com/dl/openeye/" + Loader.MC_VERSION; - private static final Setting ASK_FOR_OPEN_EYE = new Setting<>("advanced", "askForOpenEye", null, true); + private static final String DOWNLOAD_URL = "https://www.replaymod.com/dl/openeye/" + ReplayMod.getMinecraftVersion(); private ReplayMod mod; @Override public void register(ReplayMod mod) throws Exception { this.mod = mod; - mod.getSettingsRegistry().register(ASK_FOR_OPEN_EYE); - if (!Loader.isModLoaded("OpenEye") && mod.getSettingsRegistry().get(ASK_FOR_OPEN_EYE)) { + //#if MC>=11300 + boolean isOpenEyeLoaded = ModList.get().isLoaded("openeye"); + //#else + //$$ boolean isOpenEyeLoaded = Loader.isModLoaded("OpenEye"); + //#endif + if (!isOpenEyeLoaded && mod.getSettingsRegistry().get(Setting.ASK_FOR_OPEN_EYE)) { new Thread(() -> { try { LOGGER.trace("Checking for OpenEye availability"); @@ -93,7 +100,7 @@ public class OpenEyeExtra implements Extra { GuiPopup popup = new GuiPopup(OfferGui.this); new Thread(() -> { try { - File targetFile = new File(mod.getMinecraft().mcDataDir, "mods/" + Loader.MC_VERSION + "/OpenEye.jar"); + File targetFile = new File(MCVer.mcDataDir(mod.getMinecraft()), "mods/" + ReplayMod.getMinecraftVersion() + "/OpenEye.jar"); FileUtils.forceMkdir(targetFile.getParentFile()); HttpsURLConnection connection = (HttpsURLConnection) new URL(DOWNLOAD_URL).openConnection(); @@ -112,7 +119,7 @@ public class OpenEyeExtra implements Extra { }).start(); }); noButton.onClick(() -> { - mod.getSettingsRegistry().set(ASK_FOR_OPEN_EYE, false); + mod.getSettingsRegistry().set(Setting.ASK_FOR_OPEN_EYE, false); mod.getSettingsRegistry().save(); parent.display(); }); diff --git a/src/main/java/com/replaymod/extras/ReplayModExtras.java b/src/main/java/com/replaymod/extras/ReplayModExtras.java index 768afb44..4c6f5666 100644 --- a/src/main/java/com/replaymod/extras/ReplayModExtras.java +++ b/src/main/java/com/replaymod/extras/ReplayModExtras.java @@ -1,40 +1,22 @@ package com.replaymod.extras; +import com.replaymod.core.Module; import com.replaymod.core.ReplayMod; import com.replaymod.extras.advancedscreenshots.AdvancedScreenshots; import com.replaymod.extras.playeroverview.PlayerOverview; import com.replaymod.extras.urischeme.UriSchemeExtra; import com.replaymod.extras.youtube.YoutubeUpload; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -//#if MC>=10800 -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.event.FMLInitializationEvent; -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; -//#else -//$$ import cpw.mods.fml.common.Mod; -//$$ import cpw.mods.fml.common.event.FMLInitializationEvent; -//$$ import cpw.mods.fml.common.event.FMLPreInitializationEvent; -//#endif - import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -@Mod(modid = ReplayModExtras.MOD_ID, - version = "@MOD_VERSION@", - acceptedMinecraftVersions = "@MC_VERSION@", - acceptableRemoteVersions = "*", - //#if MC>=10800 - clientSideOnly = true, - //#endif - useMetadata = true) -public class ReplayModExtras { - public static final String MOD_ID = "replaymod-extras"; - - @Mod.Instance(MOD_ID) +public class ReplayModExtras implements Module { + { instance = this; } public static ReplayModExtras instance; private static final List> builtin = Arrays.asList( @@ -44,22 +26,19 @@ public class ReplayModExtras { YoutubeUpload.class, FullBrightness.class, HotkeyButtons.class, - LocalizationExtra.class, OpenEyeExtra.class ); private final Map, Extra> instances = new HashMap<>(); - public static Logger LOGGER; + public static Logger LOGGER = LogManager.getLogger(); - @Mod.EventHandler - public void preInit(FMLPreInitializationEvent event) { - LOGGER = event.getModLog(); - ReplayMod.instance.getSettingsRegistry().register(Setting.class); + public ReplayModExtras(ReplayMod core) { + core.getSettingsRegistry().register(Setting.class); } - @Mod.EventHandler - public void init(FMLInitializationEvent event) { + @Override + public void initClient() { for (Class cls : builtin) { try { Extra extra = cls.newInstance(); diff --git a/src/main/java/com/replaymod/extras/Setting.java b/src/main/java/com/replaymod/extras/Setting.java index 2f69e4c1..ae6a359c 100644 --- a/src/main/java/com/replaymod/extras/Setting.java +++ b/src/main/java/com/replaymod/extras/Setting.java @@ -3,6 +3,8 @@ package com.replaymod.extras; import com.replaymod.core.SettingsRegistry; public final class Setting { + public static final SettingsRegistry.SettingKey ASK_FOR_OPEN_EYE = + new SettingsRegistry.SettingKeys<>("advanced", "askForOpenEye", null, true); public static final SettingsRegistry.SettingKey SKIP_POST_SCREENSHOT_GUI = new SettingsRegistry.SettingKeys<>("advanced", "skipPostScreenshotGui", null, false); } diff --git a/src/main/java/com/replaymod/extras/advancedscreenshots/AdvancedScreenshots.java b/src/main/java/com/replaymod/extras/advancedscreenshots/AdvancedScreenshots.java index b7e4d18a..49d7e2c8 100644 --- a/src/main/java/com/replaymod/extras/advancedscreenshots/AdvancedScreenshots.java +++ b/src/main/java/com/replaymod/extras/advancedscreenshots/AdvancedScreenshots.java @@ -2,14 +2,22 @@ package com.replaymod.extras.advancedscreenshots; import com.replaymod.core.ReplayMod; import com.replaymod.extras.Extra; -import com.replaymod.replay.events.ReplayDispatchKeypressesEvent; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiControls; import net.minecraftforge.common.MinecraftForge; -import org.lwjgl.input.Keyboard; + +//#if MC<11300 +//$$ import com.replaymod.core.versions.MCVer; +//$$ import com.replaymod.replay.events.ReplayDispatchKeypressesEvent; +//$$ import net.minecraft.client.Minecraft; +//$$ import net.minecraft.client.gui.GuiControls; +//$$ import net.minecraftforge.client.event.GuiScreenEvent; +//$$ import org.lwjgl.input.Keyboard; +//#endif //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; //#endif @@ -18,29 +26,36 @@ public class AdvancedScreenshots implements Extra { private ReplayMod mod; - private final Minecraft mc = Minecraft.getMinecraft(); - @Override - public void register(ReplayMod mod) throws Exception { + public void register(ReplayMod mod) { this.mod = mod; MinecraftForge.EVENT_BUS.register(this); } - @SubscribeEvent - public void onDispatchKeypresses(ReplayDispatchKeypressesEvent.Pre event) { - int keyCode = Keyboard.getEventKey() == 0 ? Keyboard.getEventCharacter() : Keyboard.getEventKey(); - - // all the conditions required to trigger a screenshot condensed in a single if statement - if (keyCode != 0 && !Keyboard.isRepeatEvent() - && (!(mc.currentScreen instanceof GuiControls) || ((GuiControls) mc.currentScreen).time <= mc.getSystemTime() - 20L) - && Keyboard.getEventKeyState() - && keyCode == mc.gameSettings.keyBindScreenshot.getKeyCode()) { - - ReplayMod.instance.runLater(() -> { - new GuiCreateScreenshot(mod).display(); - }); - - event.setCanceled(true); + //#if MC>=11300 + private static AdvancedScreenshots instance; { instance = this; } + public static void take() { + if (instance != null) { + instance.takeScreenshot(); } } + //#else + //$$ @SubscribeEvent + //$$ public void onDispatchKeypresses(ReplayDispatchKeypressesEvent.Pre event) { + //$$ Minecraft mc = MCVer.getMinecraft(); + //$$ if (mc.currentScreen instanceof GuiControls) return; + //$$ if (!Keyboard.getEventKeyState()) return; + //$$ if (Keyboard.isRepeatEvent()) return; + //$$ int keyCode = Keyboard.getEventKey() == 0 ? Keyboard.getEventCharacter() : Keyboard.getEventKey(); + //$$ if (keyCode == 0 || keyCode != mc.gameSettings.keyBindScreenshot.getKeyCode()) return; + //$$ + //$$ takeScreenshot(); + //$$ + //$$ event.setCanceled(true); + //$$ } + //#endif + + private void takeScreenshot() { + ReplayMod.instance.runLater(() -> new GuiCreateScreenshot(mod).display()); + } } diff --git a/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java b/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java index 3ef09234..a5422804 100644 --- a/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java +++ b/src/main/java/com/replaymod/extras/advancedscreenshots/GuiCreateScreenshot.java @@ -1,6 +1,7 @@ package com.replaymod.extras.advancedscreenshots; import com.replaymod.core.ReplayMod; +import com.replaymod.core.versions.MCVer; import com.replaymod.render.RenderSettings; import com.replaymod.render.gui.GuiRenderSettings; import com.replaymod.replay.ReplayModReplay; @@ -12,10 +13,9 @@ import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; import net.minecraft.crash.CrashReport; import net.minecraft.util.ScreenShotHelper; -import net.minecraftforge.common.config.Configuration; -import net.minecraftforge.common.config.Property; import java.io.File; +import java.nio.file.Path; import java.util.ArrayList; import static com.replaymod.core.utils.Utils.error; @@ -82,13 +82,12 @@ public class GuiCreateScreenshot extends GuiRenderSettings implements Loadable { @Override protected File generateOutputFile(RenderSettings.EncodingPreset encodingPreset) { - File screenshotFolder = new File(getMinecraft().mcDataDir, "screenshots"); + File screenshotFolder = new File(MCVer.mcDataDir(getMinecraft()), "screenshots"); return ScreenShotHelper.getTimestampedPNGFileForDirectory(screenshotFolder); } @Override - protected Property getConfigProperty(Configuration configuration) { - return configuration.get("screenshotsettings", "settings", "{}", - "Last state of the screenshot settings GUI. Internal use only."); + protected Path getSettingsPath() { + return MCVer.mcDataDir(getMinecraft()).toPath().resolve("config/replaymod-screenshotsettings.json"); } } \ No newline at end of file diff --git a/src/main/java/com/replaymod/extras/advancedscreenshots/GuiUploadScreenshot.java b/src/main/java/com/replaymod/extras/advancedscreenshots/GuiUploadScreenshot.java index 4c2cf396..593f20b9 100644 --- a/src/main/java/com/replaymod/extras/advancedscreenshots/GuiUploadScreenshot.java +++ b/src/main/java/com/replaymod/extras/advancedscreenshots/GuiUploadScreenshot.java @@ -12,7 +12,7 @@ import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; -import org.lwjgl.util.ReadableColor; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableColor; import java.awt.*; import java.io.IOException; diff --git a/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotRenderer.java b/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotRenderer.java index 9f7bfd25..c7013125 100644 --- a/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotRenderer.java +++ b/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotRenderer.java @@ -1,14 +1,15 @@ package com.replaymod.extras.advancedscreenshots; +import com.replaymod.core.versions.MCVer; import com.replaymod.render.RenderSettings; import com.replaymod.render.blend.BlendState; import com.replaymod.render.capturer.RenderInfo; import com.replaymod.render.rendering.Pipelines; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import lombok.RequiredArgsConstructor; import net.minecraft.client.Minecraft; import net.minecraft.crash.CrashReport; -import org.lwjgl.util.Dimension; -import org.lwjgl.util.ReadableDimension; //#if MC>=10800 import com.replaymod.render.hooks.ChunkLoadingRenderGlobal; @@ -17,7 +18,7 @@ import com.replaymod.render.hooks.ChunkLoadingRenderGlobal; @RequiredArgsConstructor public class ScreenshotRenderer implements RenderInfo { - private final Minecraft mc = Minecraft.getMinecraft(); + private final Minecraft mc = MCVer.getMinecraft(); private final RenderSettings settings; @@ -25,8 +26,13 @@ public class ScreenshotRenderer implements RenderInfo { public boolean renderScreenshot() throws Throwable { try { - int displayWidthBefore = mc.displayWidth; - int displayHeightBefore = mc.displayHeight; + //#if MC>=11300 + int displayWidthBefore = mc.mainWindow.framebufferWidth; + int displayHeightBefore = mc.mainWindow.framebufferHeight; + //#else + //$$ int displayWidthBefore = mc.displayWidth; + //$$ int displayHeightBefore = mc.displayHeight; + //#endif boolean hideGUIBefore = mc.gameSettings.hideGUI; mc.gameSettings.hideGUI = true; @@ -48,12 +54,18 @@ public class ScreenshotRenderer implements RenderInfo { //#endif mc.gameSettings.hideGUI = hideGUIBefore; - mc.resize(displayWidthBefore, displayHeightBefore); + //#if MC>=11300 + mc.mainWindow.framebufferWidth = displayWidthBefore; + mc.mainWindow.framebufferHeight = displayHeightBefore; + mc.getFramebuffer().createBindFramebuffer(displayWidthBefore, displayHeightBefore); + //#else + //$$ mc.resize(displayWidthBefore, displayHeightBefore); + //#endif return true; } catch (OutOfMemoryError e) { e.printStackTrace(); CrashReport report = CrashReport.makeCrashReport(e, "Creating Equirectangular Screenshot"); - Minecraft.getMinecraft().crashed(report); + MCVer.getMinecraft().crashed(report); } return false; } diff --git a/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotWriter.java b/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotWriter.java index fcabe95c..dc9c1236 100644 --- a/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotWriter.java +++ b/src/main/java/com/replaymod/extras/advancedscreenshots/ScreenshotWriter.java @@ -2,13 +2,13 @@ package com.replaymod.extras.advancedscreenshots; import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.Utils; +import com.replaymod.core.versions.MCVer; import com.replaymod.extras.ReplayModExtras; import com.replaymod.render.frame.RGBFrame; import com.replaymod.render.rendering.FrameConsumer; import com.replaymod.replay.ReplayModReplay; -import net.minecraft.client.Minecraft; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import net.minecraft.crash.CrashReport; -import org.lwjgl.util.ReadableDimension; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; @@ -48,7 +48,7 @@ public class ScreenshotWriter implements FrameConsumer { } catch (OutOfMemoryError e) { e.printStackTrace(); CrashReport report = CrashReport.makeCrashReport(e, "Exporting frame"); - Minecraft.getMinecraft().crashed(report); + MCVer.getMinecraft().crashed(report); } catch (Throwable t) { CrashReport report = CrashReport.makeCrashReport(t, "Exporting frame"); diff --git a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java index 29e64631..1e16ecc9 100644 --- a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java +++ b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverview.java @@ -11,11 +11,19 @@ import com.replaymod.replay.events.ReplayOpenEvent; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; import net.minecraftforge.client.event.RenderHandEvent; -import org.lwjgl.input.Keyboard; + +//#if MC>=11300 +//#else +//$$ import org.lwjgl.input.Keyboard; +//#endif //#if MC>=10800 import com.google.common.base.Predicate; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.EventPriority; //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; diff --git a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java index 0659410c..af40d4d2 100644 --- a/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java +++ b/src/main/java/com/replaymod/extras/playeroverview/PlayerOverviewGui.java @@ -18,15 +18,13 @@ import de.johni0702.minecraft.gui.function.Closeable; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.utils.Colors; -import net.minecraft.client.audio.PositionedSoundRecord; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.ResourceLocation; -import org.lwjgl.util.Dimension; -import org.lwjgl.util.ReadableDimension; //#if MC>=10904 import net.minecraft.init.MobEffects; -import net.minecraft.init.SoundEvents; //#else //$$ import net.minecraft.potion.Potion; //#endif @@ -55,30 +53,12 @@ public class PlayerOverviewGui extends GuiScreen implements Closeable { public final GuiCheckbox checkAll = new GuiCheckbox(contentPanel){ @Override public void onClick() { - //#if MC>=10904 - getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(SoundEvents.UI_BUTTON_CLICK, 1.0F)); - //#else - //#if MC>=10800 - //$$ getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.create(BUTTON_SOUND, 1.0F)); - //#else - //$$ getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.createPositionedSoundRecord(BUTTON_SOUND, 1.0F)); - //#endif - //#endif playersScrollable.forEach(IGuiCheckbox.class).setChecked(true); } }.setLabel("").setChecked(true).setTooltip(new GuiTooltip().setI18nText("replaymod.gui.playeroverview.showall")); public final GuiCheckbox uncheckAll = new GuiCheckbox(contentPanel){ @Override public void onClick() { - //#if MC>=10904 - getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(SoundEvents.UI_BUTTON_CLICK, 1.0F)); - //#else - //#if MC>=10800 - //$$ getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.create(BUTTON_SOUND, 1.0F)); - //#else - //$$ getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.createPositionedSoundRecord(BUTTON_SOUND, 1.0F)); - //#endif - //#endif playersScrollable.forEach(IGuiCheckbox.class).setChecked(false); } }.setLabel("").setChecked(false).setTooltip(new GuiTooltip().setI18nText("replaymod.gui.playeroverview.hideall")); @@ -135,11 +115,15 @@ public class PlayerOverviewGui extends GuiScreen implements Closeable { } }.setSize(16, 16), new GuiLabel().setText( + //#if MC>=11300 + p.getName().getFormattedText() + //#else //#if MC>=10800 - p.getName() + //$$ p.getName() //#else //$$ p.getDisplayName() //#endif + //#endif ).setColor(isSpectator(p) ? Colors.DKGREY : Colors.WHITE) ).onClick(new Runnable() { @Override @@ -196,11 +180,15 @@ public class PlayerOverviewGui extends GuiScreen implements Closeable { public int compare(EntityPlayer o1, EntityPlayer o2) { if (isSpectator(o1) && !isSpectator(o2)) return 1; if (isSpectator(o2) && !isSpectator(o1)) return -1; + //#if MC>=11300 + return o1.getName().getFormattedText().compareToIgnoreCase(o2.getName().getFormattedText()); + //#else //#if MC>=10800 - return o1.getName().compareToIgnoreCase(o2.getName()); + //$$ return o1.getName().compareToIgnoreCase(o2.getName()); //#else //$$ return o1.getDisplayName().compareToIgnoreCase(o2.getDisplayName()); //#endif + //#endif } } } diff --git a/src/main/java/com/replaymod/extras/urischeme/LinuxUriScheme.java b/src/main/java/com/replaymod/extras/urischeme/LinuxUriScheme.java index 2a4db226..9b19158a 100644 --- a/src/main/java/com/replaymod/extras/urischeme/LinuxUriScheme.java +++ b/src/main/java/com/replaymod/extras/urischeme/LinuxUriScheme.java @@ -1,6 +1,5 @@ package com.replaymod.extras.urischeme; -import com.replaymod.core.ReplayMod; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.StringBuilderWriter; @@ -17,7 +16,7 @@ public class LinuxUriScheme extends UriScheme { public void install() throws URISyntaxException, IOException { File file = new File("replaymod.desktop"); File iconFile = new File("replaymod-icon.jpg"); - String path = ReplayMod.getContainer().getSource().getAbsolutePath().replace("\\", "\\\\").replace("\"", "\\\""); + String path = findJarFile().getAbsolutePath().replace("\\", "\\\\").replace("\"", "\\\""); String content = "[Desktop Entry]\n" + "Name=ReplayMod\n" + diff --git a/src/main/java/com/replaymod/extras/urischeme/UriScheme.java b/src/main/java/com/replaymod/extras/urischeme/UriScheme.java index 2af92bcc..17b873a0 100644 --- a/src/main/java/com/replaymod/extras/urischeme/UriScheme.java +++ b/src/main/java/com/replaymod/extras/urischeme/UriScheme.java @@ -6,6 +6,9 @@ import javax.swing.*; import java.io.*; import java.net.InetAddress; import java.net.Socket; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.CodeSource; import java.util.Arrays; public abstract class UriScheme { @@ -82,4 +85,19 @@ public abstract class UriScheme { } public abstract void install() throws Exception; + + File findJarFile() throws IOException { + CodeSource codeSource = UriScheme.class.getProtectionDomain().getCodeSource(); + if (codeSource != null) { + URL location = codeSource.getLocation(); + if (location != null) { + try { + return new File(location.toURI()); + } catch (URISyntaxException e) { + throw new IOException(e); + } + } + } + throw new IOException("No jar file found. Is this a development environment?"); + } } diff --git a/src/main/java/com/replaymod/extras/urischeme/WindowsUriScheme.java b/src/main/java/com/replaymod/extras/urischeme/WindowsUriScheme.java index 748db63a..64bde10d 100644 --- a/src/main/java/com/replaymod/extras/urischeme/WindowsUriScheme.java +++ b/src/main/java/com/replaymod/extras/urischeme/WindowsUriScheme.java @@ -1,6 +1,5 @@ package com.replaymod.extras.urischeme; -import com.replaymod.core.ReplayMod; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.StringBuilderWriter; @@ -9,7 +8,7 @@ import java.io.IOException; public class WindowsUriScheme extends UriScheme { @Override public void install() throws IOException, InterruptedException { - String path = ReplayMod.getContainer().getSource().getAbsolutePath().replace("\\", "\\\\").replace("\"", "\\\""); + String path = findJarFile().getAbsolutePath().replace("\\", "\\\\").replace("\"", "\\\""); regAdd("\\replaymod /f /ve /d \"URL:replaymod Protocol\""); regAdd("\\replaymod /f /v \"URL Protocol\" /d \"\""); regAdd("\\replaymod\\shell\\open\\command /f /ve /d \"java -cp \\\"" + path + "\\\" " + UriScheme.class.getName() + " \\\"%1\\\"\""); diff --git a/src/main/java/com/replaymod/extras/youtube/GuiYoutubeUpload.java b/src/main/java/com/replaymod/extras/youtube/GuiYoutubeUpload.java index d3af732e..3122ab43 100644 --- a/src/main/java/com/replaymod/extras/youtube/GuiYoutubeUpload.java +++ b/src/main/java/com/replaymod/extras/youtube/GuiYoutubeUpload.java @@ -18,12 +18,17 @@ import de.johni0702.minecraft.gui.element.advanced.GuiTextArea; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.popup.GuiFileChooserPopup; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import joptsimple.internal.Strings; import lombok.RequiredArgsConstructor; import net.minecraft.client.resources.I18n; import org.apache.commons.io.IOUtils; -import org.lwjgl.Sys; -import org.lwjgl.util.ReadableDimension; + +//#if MC>=11300 +import net.minecraft.util.Util; +//#else +//$$ import org.lwjgl.Sys; +//#endif import javax.annotation.Nullable; import javax.imageio.ImageIO; @@ -213,7 +218,11 @@ public class GuiYoutubeUpload extends GuiScreen { try { Desktop.getDesktop().browse(new URL(url).toURI()); } catch(Throwable throwable) { - Sys.openURL(url); + //#if MC>=11300 + Util.getOSType().openURI(url); + //#else + //$$ Sys.openURL(url); + //#endif } upload = null; progressBar.setLabel(I18n.format("replaymod.gui.ytuploadprogress.done", url)); diff --git a/src/main/java/com/replaymod/extras/youtube/YoutubeUpload.java b/src/main/java/com/replaymod/extras/youtube/YoutubeUpload.java index 9527665b..d171cd87 100644 --- a/src/main/java/com/replaymod/extras/youtube/YoutubeUpload.java +++ b/src/main/java/com/replaymod/extras/youtube/YoutubeUpload.java @@ -9,7 +9,11 @@ import net.minecraftforge.client.event.GuiScreenEvent; import net.minecraftforge.common.MinecraftForge; //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; //#endif diff --git a/src/main/java/com/replaymod/extras/youtube/YoutubeUploader.java b/src/main/java/com/replaymod/extras/youtube/YoutubeUploader.java index 79a53302..e2f37a32 100644 --- a/src/main/java/com/replaymod/extras/youtube/YoutubeUploader.java +++ b/src/main/java/com/replaymod/extras/youtube/YoutubeUploader.java @@ -21,8 +21,10 @@ import com.google.api.services.youtube.model.VideoSnippet; import com.google.api.services.youtube.model.VideoStatus; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; +import com.google.common.io.Files; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; +import com.replaymod.core.versions.MCVer; import com.replaymod.render.RenderSettings; import com.replaymod.render.metadata.MetadataInjector; import lombok.Getter; @@ -41,8 +43,7 @@ import static com.replaymod.extras.ReplayModExtras.LOGGER; public class YoutubeUploader { private static final String CLIENT_ID = "743126594724-mfe7pj1k7e47uu5pk4503c8st9vj9ibu.apps.googleusercontent.com"; private static final String CLIENT_SECRET = "gMwcy3mRYCRamCIjJIYP7rqc"; - private static final String FFMPEG_ODS = - "-i %s -vf scale=iw:iw*9/16,setdar=16:9 -c:v libx264 -preset slow -crf 16 %s"; + private static final String FFMPEG_MP4 = "-i %s -c:v libx264 -preset slow -crf 16 %s"; private static final JsonFactory JSON_FACTORY = new GsonFactory(); private final NetHttpTransport httpTransport; private final DataStoreFactory dataStoreFactory; @@ -78,7 +79,7 @@ public class YoutubeUploader { this.videoVisibility = videoVisibility; this.videoSnippet = videoSnippet; this.httpTransport = GoogleNetHttpTransport.newTrustedTransport(); - this.dataStoreFactory = new FileDataStoreFactory(minecraft.mcDataDir); + this.dataStoreFactory = new FileDataStoreFactory(MCVer.mcDataDir(minecraft)); } @@ -131,68 +132,73 @@ public class YoutubeUploader { } private File preUpload() throws InterruptedException, IOException { - if (settings.getRenderMethod() == RenderSettings.RenderMethod.ODS) { - File tmpFile = new File(videoFile.getParentFile(), System.currentTimeMillis() + ".mp4"); - tmpFile.deleteOnExit(); + File outputFile = videoFile; - String args = String.format(FFMPEG_ODS, videoFile.getName(), tmpFile.getName()); + if (settings.getRenderMethod().isSpherical()) { + // inject spherical metadata for YouTube unless already done - CommandLine commandLine = new CommandLine(settings.getExportCommand()); - commandLine.addArguments(args); - LOGGER.info("Re-encoding for ODS with {} {}", settings.getExportCommand(), args); - Process process = new ProcessBuilder(commandLine.toStrings()).directory(videoFile.getParentFile()).start(); + boolean isMp4 = Files.getFileExtension(outputFile.getName()).equalsIgnoreCase("mp4"); - final AtomicBoolean active = new AtomicBoolean(true); - final InputStream in = process.getErrorStream(); - new Thread(() -> { - try { - StringBuilder sb = new StringBuilder(); - while (active.get()) { - char c = (char) in.read(); - if (c == '\r') { - String str = sb.toString(); - LOGGER.debug("[FFmpeg] {}", str); - if (str.startsWith("frame=")) { - str = str.substring(6).trim(); - str = str.substring(0, str.indexOf(' ')); - double frame = Integer.parseInt(str); - progress = Suppliers.ofInstance(frame / videoFrames); + if (!isMp4) { + // convert non-mp4 videos into mp4 format to be able to inject metadata + outputFile = new File(outputFile.getParentFile(), System.currentTimeMillis() + ".mp4"); + outputFile.deleteOnExit(); + + String args = String.format(FFMPEG_MP4, videoFile.getName(), outputFile.getName()); + + CommandLine commandLine = new CommandLine(settings.getExportCommandOrDefault()); + commandLine.addArguments(args); + LOGGER.info("Re-encoding for metadata injection with {} {}", commandLine.getExecutable(), args); + Process process = new ProcessBuilder(commandLine.toStrings()).directory(outputFile.getParentFile()).start(); + + final AtomicBoolean active = new AtomicBoolean(true); + final InputStream in = process.getErrorStream(); + new Thread(() -> { + try { + StringBuilder sb = new StringBuilder(); + while (active.get()) { + char c = (char) in.read(); + if (c == '\r') { + String str = sb.toString(); + LOGGER.debug("[FFmpeg] {}", str); + if (str.startsWith("frame=")) { + str = str.substring(6).trim(); + str = str.substring(0, str.indexOf(' ')); + double frame = Integer.parseInt(str); + progress = Suppliers.ofInstance(frame / videoFrames); + } + sb = new StringBuilder(); + } else { + sb.append(c); } - sb = new StringBuilder(); - } else { - sb.append(c); } + } catch (IOException e) { + e.printStackTrace(); } - } catch (IOException e) { - e.printStackTrace(); + }).start(); + + int result; + try { + result = process.waitFor(); + } catch (InterruptedException e) { + process.destroy(); + throw e; + } finally { + active.set(false); + } + if (result != 0) { + throw new IOException("FFmpeg returned: " + result); } - }).start(); - - int result; - try { - result = process.waitFor(); - } catch (InterruptedException e) { - process.destroy(); - throw e; - } finally { - active.set(false); - } - if (result != 0) { - throw new IOException("FFmpeg returned: " + result); } - MetadataInjector.injectODSMetadata(tmpFile); - - return tmpFile; - - } else if (settings.getRenderMethod() == RenderSettings.RenderMethod.EQUIRECTANGULAR) { - // if metadata hasn't been injected before, inject it before the upload - if (!settings.isInject360Metadata()) { - MetadataInjector.inject360Metadata(videoFile); + if (!settings.isInjectSphericalMetadata() || !isMp4) { + MetadataInjector.injectMetadata(settings.getRenderMethod(), outputFile, + settings.getTargetVideoWidth(), settings.getTargetVideoHeight(), + settings.getSphericalFovX(), settings.getSphericalFovY()); } } - return videoFile; + return outputFile; } private Video doUpload(YouTube youTube, File processedFile) throws IOException { diff --git a/src/main/java/com/replaymod/online/AuthenticationHash.java b/src/main/java/com/replaymod/online/AuthenticationHash.java index 0b212ef7..1aac201f 100644 --- a/src/main/java/com/replaymod/online/AuthenticationHash.java +++ b/src/main/java/com/replaymod/online/AuthenticationHash.java @@ -1,6 +1,6 @@ package com.replaymod.online; -import net.minecraft.client.Minecraft; +import com.replaymod.core.versions.MCVer; import java.util.Random; @@ -9,7 +9,7 @@ public class AuthenticationHash { private static final Random random = new Random(); public AuthenticationHash() { - username = Minecraft.getMinecraft().getSession().getUsername(); + username = MCVer.getMinecraft().getSession().getUsername(); currentTime = System.currentTimeMillis(); randomLong = random.nextLong(); hash = getAuthenticationHash(); diff --git a/src/main/java/com/replaymod/online/ConfigurationAuthData.java b/src/main/java/com/replaymod/online/ConfigurationAuthData.java index d13d6cab..d866b972 100644 --- a/src/main/java/com/replaymod/online/ConfigurationAuthData.java +++ b/src/main/java/com/replaymod/online/ConfigurationAuthData.java @@ -1,63 +1,65 @@ -package com.replaymod.online; - -import com.replaymod.online.api.ApiClient; -import com.replaymod.online.api.AuthData; -import com.replaymod.online.api.replay.holders.AuthConfirmation; -import net.minecraftforge.common.config.Configuration; -import net.minecraftforge.common.config.Property; - -/** - * Auth data stored in a {@link Configuration}. - */ -public class ConfigurationAuthData implements AuthData { - - private final Configuration config; - private String userName; - private String authKey; - - public ConfigurationAuthData(Configuration config) { - this.config = config; - } - - /** - * Loads the data from the configuration and checks for its validity. - * If the data is invalid, it is removed from the config. - * @param apiClient Api client used for validating the auth data - */ - public void load(ApiClient apiClient) { - Property property = config.get("authkey", "authkey", (String) null); - if (property != null) { - String authKey = property.getString(); - AuthConfirmation result = apiClient.checkAuthkey(authKey); - if (result != null) { - this.authKey = authKey; - this.userName = result.getUsername(); - } else { - setData(null, null); - } - } - } - - @Override - public String getUserName() { - return userName; - } - - @Override - public String getAuthKey() { - return authKey; - } - - @Override - public void setData(String userName, String authKey) { - this.userName = userName; - this.authKey = authKey; - - config.removeCategory(config.getCategory("authkey")); - if (authKey != null) { - // Note: .get() actually creates the entry with the default value if it doesn't exist - config.get("authkey", "authkey", authKey); - } - config.save(); - } -} +//#if MC<11300 +//$$ package com.replaymod.online; +//$$ +//$$ import com.replaymod.online.api.ApiClient; +//$$ import com.replaymod.online.api.AuthData; +//$$ import com.replaymod.online.api.replay.holders.AuthConfirmation; +//$$ import net.minecraftforge.common.config.Configuration; +//$$ import net.minecraftforge.common.config.Property; +//$$ +//$$ /** +//$$ * Auth data stored in a {@link Configuration}. +//$$ */ +//$$ public class ConfigurationAuthData implements AuthData { +//$$ +//$$ private final Configuration config; +//$$ private String userName; +//$$ private String authKey; +//$$ +//$$ public ConfigurationAuthData(Configuration config) { +//$$ this.config = config; +//$$ } +//$$ +//$$ /** +//$$ * Loads the data from the configuration and checks for its validity. +//$$ * If the data is invalid, it is removed from the config. +//$$ * @param apiClient Api client used for validating the auth data +//$$ */ +//$$ public void load(ApiClient apiClient) { +//$$ Property property = config.get("authkey", "authkey", (String) null); +//$$ if (property != null) { +//$$ String authKey = property.getString(); +//$$ AuthConfirmation result = apiClient.checkAuthkey(authKey); +//$$ if (result != null) { +//$$ this.authKey = authKey; +//$$ this.userName = result.getUsername(); +//$$ } else { +//$$ setData(null, null); +//$$ } +//$$ } +//$$ } +//$$ +//$$ @Override +//$$ public String getUserName() { +//$$ return userName; +//$$ } +//$$ +//$$ @Override +//$$ public String getAuthKey() { +//$$ return authKey; +//$$ } +//$$ +//$$ @Override +//$$ public void setData(String userName, String authKey) { +//$$ this.userName = userName; +//$$ this.authKey = authKey; +//$$ +//$$ config.removeCategory(config.getCategory("authkey")); +//$$ if (authKey != null) { +//$$ // Note: .get() actually creates the entry with the default value if it doesn't exist +//$$ config.get("authkey", "authkey", authKey); +//$$ } +//$$ config.save(); +//$$ } +//$$ } +//#endif diff --git a/src/main/java/com/replaymod/online/PlainFileAuthData.java b/src/main/java/com/replaymod/online/PlainFileAuthData.java new file mode 100644 index 00000000..4ff020ae --- /dev/null +++ b/src/main/java/com/replaymod/online/PlainFileAuthData.java @@ -0,0 +1,69 @@ +package com.replaymod.online; + +import com.replaymod.online.api.ApiClient; +import com.replaymod.online.api.AuthData; +import com.replaymod.online.api.replay.holders.AuthConfirmation; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +/** + * Auth data stored in a plain text file. + */ +public class PlainFileAuthData implements AuthData { + private final Path path; + private String userName; + private String authKey; + + public PlainFileAuthData(Path path) { + this.path = path; + } + + /** + * Loads the data from the JSON file and checks for its validity. + * If the data is invalid, the JSON file is removed. + * @param apiClient Api client used for validating the auth data + */ + public void load(ApiClient apiClient) throws IOException { + String authKey; + try { + authKey = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + } catch (NoSuchFileException | FileNotFoundException ignored) { + return; + } + AuthConfirmation result = apiClient.checkAuthkey(authKey); + if (result != null) { + this.authKey = authKey; + this.userName = result.getUsername(); + } else { + Files.deleteIfExists(path); + } + } + + @Override + public String getUserName() { + return userName; + } + + @Override + public String getAuthKey() { + return authKey; + } + + @Override + public void setData(String userName, String authKey) { + this.userName = userName; + this.authKey = authKey; + + try { + Files.write(path, authKey.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE_NEW); + } catch (IOException e) { + ReplayModOnline.LOGGER.error("Saving auth data:", e); + } + } +} diff --git a/src/main/java/com/replaymod/online/ReplayModOnline.java b/src/main/java/com/replaymod/online/ReplayModOnline.java index badc3a46..9a87a68a 100644 --- a/src/main/java/com/replaymod/online/ReplayModOnline.java +++ b/src/main/java/com/replaymod/online/ReplayModOnline.java @@ -1,7 +1,10 @@ package com.replaymod.online; +import com.replaymod.core.Module; import com.replaymod.core.ReplayMod; +import com.replaymod.core.versions.MCVer; import com.replaymod.online.api.ApiClient; +import com.replaymod.online.api.AuthData; import com.replaymod.online.gui.GuiLoginPrompt; import com.replaymod.online.gui.GuiReplayDownloading; import com.replaymod.online.gui.GuiSaveModifiedReplay; @@ -12,48 +15,36 @@ 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.minecraftforge.common.config.Configuration; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -//#if MC>=10800 -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.event.FMLInitializationEvent; -import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.common.config.Configuration; +//#if MC>=10800 +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; //#else -//$$ import cpw.mods.fml.common.Mod; -//$$ 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.SubscribeEvent; //#endif +//#endif import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import static com.replaymod.core.versions.MCVer.*; -import static net.minecraft.client.Minecraft.getMinecraft; -@Mod(modid = ReplayModOnline.MOD_ID, - version = "@MOD_VERSION@", - acceptedMinecraftVersions = "@MC_VERSION@", - acceptableRemoteVersions = "*", - //#if MC>=10800 - clientSideOnly = true, - //#endif - useMetadata = true) -public class ReplayModOnline { - public static final String MOD_ID = "replaymod-online"; - - @Mod.Instance(MOD_ID) +public class ReplayModOnline implements Module { + { instance = this; } public static ReplayModOnline instance; private ReplayMod core; private ReplayModReplay replayModule; - public static Logger LOGGER; + public static Logger LOGGER = LogManager.getLogger(); private ApiClient apiClient; @@ -64,22 +55,31 @@ public class ReplayModOnline { */ private File currentReplayOutputFile; - @Mod.EventHandler - public void preInit(FMLPreInitializationEvent event) { - LOGGER = event.getModLog(); - core = ReplayMod.instance; - replayModule = ReplayModReplay.instance; + public ReplayModOnline(ReplayMod core, ReplayModReplay replayModule) { + this.core = core; + this.replayModule = replayModule; core.getSettingsRegistry().register(Setting.class); - - Configuration config = new Configuration(event.getSuggestedConfigurationFile()); - ConfigurationAuthData authData = new ConfigurationAuthData(config); - apiClient = new ApiClient(authData); - authData.load(apiClient); } - @Mod.EventHandler - public void init(FMLInitializationEvent event) { + @Override + public void initClient() { + Path path = MCVer.mcDataDir(MCVer.getMinecraft()).toPath().resolve("config/replaymod-online.token"); + PlainFileAuthData authData = new PlainFileAuthData(path); + apiClient = new ApiClient(authData); + if (Files.notExists(path)) { + AuthData oldAuthData = loadOldAuthData(); + if (oldAuthData != null) { + authData.setData(oldAuthData.getUserName(), oldAuthData.getAuthKey()); + } + } else { + try { + authData.load(apiClient); + } catch (IOException e) { + ReplayModOnline.LOGGER.error("Loading auth data:", e); + } + } + if (!getDownloadsFolder().exists()){ if (!getDownloadsFolder().mkdirs()) { LOGGER.warn("Failed to create downloads folder: " + getDownloadsFolder()); @@ -88,10 +88,7 @@ public class ReplayModOnline { new GuiHandler(this).register(); FML_BUS.register(this); - } - @Mod.EventHandler - public void postInit(FMLPostInitializationEvent event) { // Initial login prompt if (!core.getSettingsRegistry().get(Setting.SKIP_LOGIN_PROMPT)) { if (!isLoggedIn()) { @@ -103,6 +100,23 @@ public class ReplayModOnline { } } + /** + * Loads old auth data from the legacy Configuration system and stores it in the new json file. + * Always returns null on 1.13+ where the Configuration system has been removed. + */ + private AuthData loadOldAuthData() { + //#if MC<11300 + //$$ Path path = MCVer.mcDataDir(MCVer.getMinecraft()).toPath().resolve("config/replaymod-online.cfg"); + //$$ Configuration config = new Configuration(path.toFile()); + //$$ ConfigurationAuthData authData = new ConfigurationAuthData(config); + //$$ authData.load(new ApiClient(authData)); + //$$ if (authData.getAuthKey() != null) { + //$$ return authData; + //$$ } + //#endif + return null; + } + public ReplayMod getCore() { return core; } @@ -125,7 +139,7 @@ public class ReplayModOnline { public File getDownloadsFolder() { String path = core.getSettingsRegistry().get(Setting.DOWNLOAD_PATH); - return new File(path.startsWith("./") ? getMinecraft().mcDataDir : null, path); + return new File(path.startsWith("./") ? mcDataDir(getMinecraft()) : null, path); } public File getDownloadedFile(int id) { diff --git a/src/main/java/com/replaymod/online/api/ApiClient.java b/src/main/java/com/replaymod/online/api/ApiClient.java index a12cd822..0a351c37 100755 --- a/src/main/java/com/replaymod/online/api/ApiClient.java +++ b/src/main/java/com/replaymod/online/api/ApiClient.java @@ -6,6 +6,7 @@ import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import com.mojang.authlib.exceptions.AuthenticationException; import com.replaymod.core.ReplayMod; +import com.replaymod.core.versions.MCVer; import com.replaymod.online.AuthenticationHash; import com.replaymod.online.api.replay.ReplayModApiMethods; import com.replaymod.online.api.replay.SearchQuery; @@ -30,7 +31,7 @@ import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY; public class ApiClient { - private static final Minecraft mc = Minecraft.getMinecraft(); + private static final Minecraft mc = MCVer.getMinecraft(); private static final Gson gson = new Gson(); private static final JsonParser jsonParser = new JsonParser(); diff --git a/src/main/java/com/replaymod/online/api/replay/holders/MinecraftVersion.java b/src/main/java/com/replaymod/online/api/replay/holders/MinecraftVersion.java index 7320efb2..2a3216eb 100644 --- a/src/main/java/com/replaymod/online/api/replay/holders/MinecraftVersion.java +++ b/src/main/java/com/replaymod/online/api/replay/holders/MinecraftVersion.java @@ -9,7 +9,8 @@ public enum MinecraftVersion { MC_1_11_2("Minecraft 1.11.2", "1.11.2"), MC_1_12("Minecraft 1.12", "1.12"), MC_1_12_1("Minecraft 1.12.1", "1.12.1"), - MC_1_12_2("Minecraft 1.12.2", "1.12.2"); + MC_1_12_2("Minecraft 1.12.2", "1.12.2"), + MC_1_13_2("Minecraft 1.13.2", "1.13.2"); private String niceName, apiName; diff --git a/src/main/java/com/replaymod/online/gui/GuiRegister.java b/src/main/java/com/replaymod/online/gui/GuiRegister.java index 81e4bcc4..610f6f8d 100644 --- a/src/main/java/com/replaymod/online/gui/GuiRegister.java +++ b/src/main/java/com/replaymod/online/gui/GuiRegister.java @@ -1,7 +1,6 @@ package com.replaymod.online.gui; import com.mojang.authlib.exceptions.AuthenticationException; -import com.replaymod.core.versions.MCVer; import com.replaymod.online.api.ApiClient; import com.replaymod.online.api.ApiException; import de.johni0702.minecraft.gui.container.AbstractGuiScreen; @@ -13,9 +12,9 @@ import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.utils.Consumers; import de.johni0702.minecraft.gui.utils.Utils; import com.replaymod.core.utils.Patterns; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableColor; import net.minecraft.client.gui.FontRenderer; -import org.lwjgl.util.Dimension; -import org.lwjgl.util.ReadableColor; import static com.replaymod.core.versions.MCVer.*; diff --git a/src/main/java/com/replaymod/online/gui/GuiReplayCenter.java b/src/main/java/com/replaymod/online/gui/GuiReplayCenter.java index fe26f083..29999215 100644 --- a/src/main/java/com/replaymod/online/gui/GuiReplayCenter.java +++ b/src/main/java/com/replaymod/online/gui/GuiReplayCenter.java @@ -37,9 +37,9 @@ import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; import de.johni0702.minecraft.gui.utils.Consumer; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import org.apache.commons.lang3.StringUtils; -import org.lwjgl.util.Dimension; -import org.lwjgl.util.ReadableDimension; import java.awt.image.BufferedImage; import java.io.IOException; @@ -462,7 +462,7 @@ public class GuiReplayCenter extends GuiScreen { @Override public ReadableDimension calcMinSize(GuiContainer container) { - return new org.lwjgl.util.Dimension(300, thumbnail.getMinSize().getHeight()); + return new Dimension(300, thumbnail.getMinSize().getHeight()); } }); } diff --git a/src/main/java/com/replaymod/online/gui/GuiReplayCenterSearch.java b/src/main/java/com/replaymod/online/gui/GuiReplayCenterSearch.java index 9a39e54d..00f6b017 100644 --- a/src/main/java/com/replaymod/online/gui/GuiReplayCenterSearch.java +++ b/src/main/java/com/replaymod/online/gui/GuiReplayCenterSearch.java @@ -15,9 +15,14 @@ import de.johni0702.minecraft.gui.function.Typeable; import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.utils.Colors; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint; import net.minecraft.client.resources.I18n; -import org.lwjgl.input.Keyboard; -import org.lwjgl.util.ReadablePoint; + +//#if MC>=11300 +import com.replaymod.core.versions.MCVer.Keyboard; +//#else +//$$ import org.lwjgl.input.Keyboard; +//#endif import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/replaymod/online/gui/GuiUploadReplay.java b/src/main/java/com/replaymod/online/gui/GuiUploadReplay.java index 3ceb46d6..d95b2d30 100644 --- a/src/main/java/com/replaymod/online/gui/GuiUploadReplay.java +++ b/src/main/java/com/replaymod/online/gui/GuiUploadReplay.java @@ -33,13 +33,16 @@ import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; import net.minecraft.client.resources.I18n; -import net.minecraft.client.settings.GameSettings; import net.minecraft.client.settings.KeyBinding; import net.minecraft.crash.CrashReport; -import net.minecraft.util.ReportedException; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; +//#if MC>=11300 +//#else +//$$ import net.minecraft.client.settings.GameSettings; +//#endif + import javax.annotation.Nullable; import java.awt.image.BufferedImage; import java.io.File; @@ -47,6 +50,7 @@ import java.io.IOException; import java.util.Set; import java.util.stream.Collectors; +import static com.replaymod.core.versions.MCVer.newReportedException; import static java.util.Arrays.stream; public class GuiUploadReplay extends GuiScreen { @@ -130,7 +134,7 @@ public class GuiUploadReplay extends GuiScreen { metaData = replayFile.getMetaData(); optThumbnail = replayFile.getThumb(); } catch (IOException e) { - throw new ReportedException(CrashReport.makeCrashReport(e, "Read replay file " + file.getName())); + throw newReportedException(CrashReport.makeCrashReport(e, "Read replay file " + file.getName())); } // Apply to gui @@ -147,7 +151,11 @@ public class GuiUploadReplay extends GuiScreen { // Get name of key used for thumbnail creation KeyBindingRegistry registry = mod.getCore().getKeyBindingRegistry(); KeyBinding keyBinding = registry.getKeyBindings().get("replaymod.input.thumbnail"); - String keyName = keyBinding == null ? "???" : GameSettings.getKeyDisplayString(keyBinding.getKeyCode()); + //#if MC>=11300 + String keyName = keyBinding == null ? "???" : keyBinding.func_197978_k(); + //#else + //$$ String keyName = keyBinding == null ? "???" : GameSettings.getKeyDisplayString(keyBinding.getKeyCode()); + //#endif // No thumbnail, show default thumbnail and hint on how to create one thumbnail.setTexture(Utils.DEFAULT_THUMBNAIL); diff --git a/src/main/java/com/replaymod/online/handler/GuiHandler.java b/src/main/java/com/replaymod/online/handler/GuiHandler.java index 34363760..165a8bbf 100644 --- a/src/main/java/com/replaymod/online/handler/GuiHandler.java +++ b/src/main/java/com/replaymod/online/handler/GuiHandler.java @@ -15,7 +15,12 @@ import net.minecraft.client.resources.I18n; import net.minecraftforge.client.event.GuiScreenEvent; //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +import java.util.ArrayList; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; //#endif @@ -44,9 +49,21 @@ public class GuiHandler { return; } - GuiButton button = new GuiButton(BUTTON_REPLAY_CENTER, getGui(event).width / 2 - 100, - getGui(event).height / 4 + 10 + 4 * 24, I18n.format("replaymod.gui.replaycenter")); - getButtonList(event).add(button); + GuiButton button = new GuiButton( + BUTTON_REPLAY_CENTER, + getGui(event).width / 2 + 2, + getGui(event).height / 4 + 10 + 4 * 24, + I18n.format("replaymod.gui.replaycenter") + ) { + //#if MC>=11300 + @Override + public void onClick(double mouseX, double mouseY) { + onButton(new GuiScreenEvent.ActionPerformedEvent.Pre(getGui(event), this, new ArrayList<>())); + } + //#endif + }; + button.width = button.width / 2 - 2; + addButton(event, button); } @SubscribeEvent @@ -57,22 +74,15 @@ public class GuiHandler { } final GuiReplayViewer replayViewer = (GuiReplayViewer) guiScreen; // Inject Upload button - for (GuiElement element : replayViewer.replayButtonPanel.getChildren()) { - if (element instanceof GuiPanel && (((GuiPanel) element).getChildren().isEmpty())) { - new de.johni0702.minecraft.gui.element.GuiButton((GuiPanel) element).onClick(new Runnable() { - @Override - public void run() { - File replayFile = replayViewer.list.getSelected().file; - GuiUploadReplay uploadGui = new GuiUploadReplay(replayViewer, mod, replayFile); - if (mod.isLoggedIn()) { - uploadGui.display(); - } else { - new GuiLoginPrompt(mod.getApiClient(), replayViewer, uploadGui, true); - } - } - }).setSize(73, 20).setI18nLabel("replaymod.gui.upload").setDisabled(); + replayViewer.replaySpecificButtons.add(new de.johni0702.minecraft.gui.element.GuiButton(replayViewer.uploadButton).onClick(() -> { + File replayFile = replayViewer.list.getSelected().file; + GuiUploadReplay uploadGui = new GuiUploadReplay(replayViewer, mod, replayFile); + if (mod.isLoggedIn()) { + uploadGui.display(); + } else { + new GuiLoginPrompt(mod.getApiClient(), replayViewer, uploadGui, true); } - } + }).setSize(73, 20).setI18nLabel("replaymod.gui.upload").setDisabled()); } @SubscribeEvent diff --git a/src/main/java/com/replaymod/pathing/gui/GuiKeyframeRepository.java b/src/main/java/com/replaymod/pathing/gui/GuiKeyframeRepository.java index 42a0032c..c6c761a4 100644 --- a/src/main/java/com/replaymod/pathing/gui/GuiKeyframeRepository.java +++ b/src/main/java/com/replaymod/pathing/gui/GuiKeyframeRepository.java @@ -28,8 +28,8 @@ import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; import de.johni0702.minecraft.gui.utils.Consumer; -import org.lwjgl.util.Dimension; -import org.lwjgl.util.ReadableDimension; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import java.io.IOException; import java.util.LinkedHashMap; @@ -229,6 +229,7 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable { timelines.putAll(replayFile.getTimelines(registry)); for (Map.Entry entry : timelines.entrySet()) { + if (entry.getKey().isEmpty()) continue; // don't show auto-save slot list.getListPanel().addElements(null, new Entry(entry.getKey())); } } diff --git a/src/main/java/com/replaymod/pathing/player/AbstractTimelinePlayer.java b/src/main/java/com/replaymod/pathing/player/AbstractTimelinePlayer.java index 4d586132..98a8a92f 100644 --- a/src/main/java/com/replaymod/pathing/player/AbstractTimelinePlayer.java +++ b/src/main/java/com/replaymod/pathing/player/AbstractTimelinePlayer.java @@ -14,7 +14,11 @@ import com.replaymod.replaystudio.pathing.path.Timeline; import net.minecraft.client.Minecraft; //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; //#endif @@ -28,7 +32,7 @@ import static com.replaymod.core.versions.MCVer.*; * Plays a timeline. */ public abstract class AbstractTimelinePlayer { - private final Minecraft mc = Minecraft.getMinecraft(); + private final Minecraft mc = getMinecraft(); private final ReplayHandler replayHandler; private Timeline timeline; protected long startOffset; diff --git a/src/main/java/com/replaymod/pathing/player/ReplayTimer.java b/src/main/java/com/replaymod/pathing/player/ReplayTimer.java index a2bdc331..597fe805 100644 --- a/src/main/java/com/replaymod/pathing/player/ReplayTimer.java +++ b/src/main/java/com/replaymod/pathing/player/ReplayTimer.java @@ -4,7 +4,11 @@ import com.replaymod.core.utils.WrappedTimer; import net.minecraft.util.Timer; //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.Event; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.Event; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.Event; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.Event; //#endif @@ -15,16 +19,28 @@ import static com.replaymod.core.versions.MCVer.*; * Wrapper around the current timer that prevents the timer from advancing by itself. */ public class ReplayTimer extends WrappedTimer { - private final Timer state = new Timer(0); + //#if MC>=11300 + private final Timer state = new Timer(0, 0); + //#else + //$$ private final Timer state = new Timer(0); + //#endif public ReplayTimer(Timer wrapped) { super(wrapped); } @Override - public void updateTimer() { + public void updateTimer( + //#if MC>=11300 + long sysClock + //#endif + ) { copy(this, state); // Save our current state - super.updateTimer(); // Update current state + wrapped.updateTimer( + //#if MC>=11300 + sysClock + //#endif + ); // Update current state copy(state, this); // Restore our old state FML_BUS.post(new UpdatedEvent()); } diff --git a/src/main/java/com/replaymod/recording/ReplayModRecording.java b/src/main/java/com/replaymod/recording/ReplayModRecording.java index fef2207e..f8137d65 100644 --- a/src/main/java/com/replaymod/recording/ReplayModRecording.java +++ b/src/main/java/com/replaymod/recording/ReplayModRecording.java @@ -1,85 +1,99 @@ package com.replaymod.recording; +import com.replaymod.core.KeyBindingRegistry; +import com.replaymod.core.Module; import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.Restrictions; import com.replaymod.recording.handler.ConnectionEventHandler; import com.replaymod.recording.handler.GuiHandler; import com.replaymod.recording.packet.PacketListener; +import io.netty.channel.Channel; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandler; +import io.netty.util.AttributeKey; import net.minecraft.network.NetworkManager; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.lwjgl.input.Keyboard; + +//#if MC>=11300 +import com.replaymod.core.versions.MCVer.Keyboard; +//#else +//$$ import org.lwjgl.input.Keyboard; +//#endif //#if MC>=10800 -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.event.FMLInitializationEvent; -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; -import net.minecraftforge.fml.common.eventhandler.EventBus; -import net.minecraftforge.fml.common.network.NetworkRegistry; +//#if MC>=11300 +import net.minecraftforge.fml.network.NetworkRegistry; +//#else +//$$ import net.minecraftforge.fml.common.network.NetworkRegistry; +//#endif //#else -//$$ import cpw.mods.fml.common.Mod; -//$$ import cpw.mods.fml.common.event.FMLInitializationEvent; -//$$ import cpw.mods.fml.common.event.FMLPreInitializationEvent; -//$$ import cpw.mods.fml.common.eventhandler.EventBus; //$$ import cpw.mods.fml.common.network.NetworkRegistry; //#endif import static com.replaymod.core.versions.MCVer.*; -@Mod(modid = ReplayModRecording.MOD_ID, - version = "@MOD_VERSION@", - acceptedMinecraftVersions = "@MC_VERSION@", - acceptableRemoteVersions = "*", - //#if MC>=10800 - clientSideOnly = true, - //#endif - useMetadata = true) -public class ReplayModRecording { - public static final String MOD_ID = "replaymod-recording"; +public class ReplayModRecording implements Module { - @Mod.Instance(MOD_ID) + private static final Logger LOGGER = LogManager.getLogger(); + //#if MC>=11300 + private static final AttributeKey ATTR_CHECKED = AttributeKey.newInstance("ReplayModRecording_checked"); + //#endif + + { instance = this; } public static ReplayModRecording instance; private ReplayMod core; - private Logger logger; - private ConnectionEventHandler connectionEventHandler; - @Mod.EventHandler - public void preInit(FMLPreInitializationEvent event) { - logger = event.getModLog(); - core = ReplayMod.instance; + public ReplayModRecording(ReplayMod mod) { + core = mod; core.getSettingsRegistry().register(Setting.class); + } - core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.marker", Keyboard.KEY_M, new Runnable() { + @Override + public void registerKeyBindings(KeyBindingRegistry registry) { + registry.registerKeyBinding("replaymod.input.marker", Keyboard.KEY_M, new Runnable() { @Override public void run() { PacketListener packetListener = connectionEventHandler.getPacketListener(); if (packetListener != null) { - packetListener.addMarker(); + packetListener.addMarker(null); core.printInfoToChat("replaymod.chat.addedmarker"); } } }); } - @Mod.EventHandler - public void init(FMLInitializationEvent event) { - EventBus bus = FML_BUS; - bus.register(connectionEventHandler = new ConnectionEventHandler(logger, core)); + @Override + public void initClient() { + FML_BUS.register(connectionEventHandler = new ConnectionEventHandler(LOGGER, core)); new GuiHandler(core).register(); - NetworkRegistry.INSTANCE.newChannel(Restrictions.PLUGIN_CHANNEL, new RestrictionsChannelHandler()); + //#if MC>=11300 + NetworkRegistry.newEventChannel(Restrictions.PLUGIN_CHANNEL, () -> "0", any -> true, any -> true); + //#else + //$$ NetworkRegistry.INSTANCE.newChannel(Restrictions.PLUGIN_CHANNEL, new RestrictionsChannelHandler()); + //#endif } @ChannelHandler.Sharable private static class RestrictionsChannelHandler extends ChannelDuplexHandler {} public void initiateRecording(NetworkManager networkManager) { + Channel channel = networkManager.channel(); + if (channel.pipeline().get("ReplayModReplay_replaySender") != null) return; + //#if MC>=11300 + if (channel.hasAttr(ATTR_CHECKED)) return; + channel.attr(ATTR_CHECKED).set(null); + //#endif connectionEventHandler.onConnectedToServerEvent(networkManager); } + + public ConnectionEventHandler getConnectionEventHandler() { + return connectionEventHandler; + } } diff --git a/src/main/java/com/replaymod/recording/Setting.java b/src/main/java/com/replaymod/recording/Setting.java index e3742d8b..27d2d69c 100644 --- a/src/main/java/com/replaymod/recording/Setting.java +++ b/src/main/java/com/replaymod/recording/Setting.java @@ -6,12 +6,14 @@ public final class Setting extends SettingsRegistry.SettingKeys { public static final Setting RECORD_SINGLEPLAYER = make("recordSingleplayer", "recordsingleplayer", true); public static final Setting RECORD_SERVER = make("recordServer", "recordserver", true); public static final Setting INDICATOR = make("indicator", "indicator", true); + public static final Setting AUTO_START_RECORDING = make("autoStartRecording", "autostartrecording", true); + public static final Setting AUTO_POST_PROCESS = make("autoPostProcess", null, true); private static Setting make(String key, String displayName, T defaultValue) { return new Setting<>(key, displayName, defaultValue); } public Setting(String key, String displayString, T defaultValue) { - super("recording", key, "replaymod.gui.settings." + displayString, defaultValue); + super("recording", key, displayString == null ? null : "replaymod.gui.settings." + displayString, defaultValue); } } diff --git a/src/main/java/com/replaymod/recording/gui/GuiRecordingControls.java b/src/main/java/com/replaymod/recording/gui/GuiRecordingControls.java new file mode 100644 index 00000000..8eb81b62 --- /dev/null +++ b/src/main/java/com/replaymod/recording/gui/GuiRecordingControls.java @@ -0,0 +1,110 @@ +package com.replaymod.recording.gui; + +import com.replaymod.core.ReplayMod; +import com.replaymod.editor.gui.MarkerProcessor; +import com.replaymod.recording.Setting; +import com.replaymod.recording.packet.PacketListener; +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.layout.CustomLayout; +import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import net.minecraft.client.gui.GuiIngameMenu; +import net.minecraftforge.client.event.GuiScreenEvent; +import net.minecraftforge.common.MinecraftForge; + +//#if MC>=10800 +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif +//#else +//$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; +//#endif + +import static com.replaymod.core.versions.MCVer.getGui; + +public class GuiRecordingControls { + private ReplayMod core; + private PacketListener packetListener; + private boolean paused; + private boolean stopped; + + private GuiPanel panel = new GuiPanel().setLayout(new HorizontalLayout().setSpacing(4)); + + private GuiButton buttonPauseResume = new GuiButton(panel).onClick(() -> { + if (paused) { + packetListener.addMarker(MarkerProcessor.MARKER_NAME_END_CUT); + } else { + packetListener.addMarker(MarkerProcessor.MARKER_NAME_START_CUT); + } + paused = !paused; + updateState(); + }).setSize(98, 20); + + private GuiButton buttonStartStop = new GuiButton(panel).onClick(() -> { + if (stopped) { + paused = false; + packetListener.addMarker(MarkerProcessor.MARKER_NAME_END_CUT); + core.printInfoToChat("replaymod.chat.recordingstarted"); + } else { + int timestamp = (int) packetListener.getCurrentDuration(); + if (!paused) { + packetListener.addMarker(MarkerProcessor.MARKER_NAME_START_CUT, timestamp); + } + packetListener.addMarker(MarkerProcessor.MARKER_NAME_SPLIT, timestamp + 1); + } + stopped = !stopped; + updateState(); + }).setSize(98, 20); + + public GuiRecordingControls(ReplayMod core, PacketListener packetListener) { + this.core = core; + this.packetListener = packetListener; + + paused = stopped = !core.getSettingsRegistry().get(Setting.AUTO_START_RECORDING); + + updateState(); + } + + public void register() { + MinecraftForge.EVENT_BUS.register(this); + } + + public void unregister() { + MinecraftForge.EVENT_BUS.unregister(this); + } + + @SubscribeEvent + public void onGuiInit(GuiScreenEvent.InitGuiEvent.Post event) { + if (getGui(event) instanceof GuiIngameMenu) { + show((GuiIngameMenu) getGui(event)); + } + } + + private void updateState() { + buttonPauseResume.setI18nLabel("replaymod.gui.recording." + (paused ? "resume" : "pause")); + buttonStartStop.setI18nLabel("replaymod.gui.recording." + (stopped ? "start" : "stop")); + + buttonPauseResume.setEnabled(!stopped); + } + + public void show(GuiIngameMenu gui) { + VanillaGuiScreen.setup(gui).setLayout(new CustomLayout() { + @Override + protected void layout(GuiScreen container, int width, int height) { + pos(panel, width / 2 - 100, height / 4 + 128); + } + }).addElements(null, panel); + } + + public boolean isPaused() { + return paused; + } + + public boolean isStopped() { + return stopped; + } +} diff --git a/src/main/java/com/replaymod/recording/gui/GuiRecordingOverlay.java b/src/main/java/com/replaymod/recording/gui/GuiRecordingOverlay.java index 467abfcc..a0237d75 100644 --- a/src/main/java/com/replaymod/recording/gui/GuiRecordingOverlay.java +++ b/src/main/java/com/replaymod/recording/gui/GuiRecordingOverlay.java @@ -10,10 +10,15 @@ import net.minecraftforge.client.event.RenderGameOverlayEvent; import net.minecraftforge.common.MinecraftForge; //#if MC>=10800 -import net.minecraft.client.renderer.GlStateManager; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif +import static net.minecraft.client.renderer.GlStateManager.*; //#else //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; +//$$ import static com.replaymod.core.versions.MCVer.GlStateManager.*; //#endif import static com.replaymod.core.ReplayMod.TEXTURE; @@ -26,10 +31,12 @@ import static com.replaymod.core.versions.MCVer.*; public class GuiRecordingOverlay { private final Minecraft mc; private final SettingsRegistry settingsRegistry; + private final GuiRecordingControls guiControls; - public GuiRecordingOverlay(Minecraft mc, SettingsRegistry settingsRegistry) { + public GuiRecordingOverlay(Minecraft mc, SettingsRegistry settingsRegistry, GuiRecordingControls guiControls) { this.mc = mc; this.settingsRegistry = settingsRegistry; + this.guiControls = guiControls; } public void register() { @@ -47,12 +54,14 @@ public class GuiRecordingOverlay { @SubscribeEvent public void renderRecordingIndicator(RenderGameOverlayEvent.Post event) { if (getType(event) != RenderGameOverlayEvent.ElementType.ALL) return; + if (guiControls.isStopped()) return; if (settingsRegistry.get(Setting.INDICATOR)) { FontRenderer fontRenderer = getFontRenderer(mc); - fontRenderer.drawString(I18n.format("replaymod.gui.recording").toUpperCase(), 30, 18 - (fontRenderer.FONT_HEIGHT / 2), 0xffffffff); - mc.renderEngine.bindTexture(TEXTURE); - GlStateManager.resetColor(); - GlStateManager.enableAlpha(); + String text = guiControls.isPaused() ? I18n.format("replaymod.gui.paused") : I18n.format("replaymod.gui.recording"); + fontRenderer.drawString(text.toUpperCase(), 30, 18 - (fontRenderer.FONT_HEIGHT / 2), 0xffffffff); + bindTexture(TEXTURE); + resetColor(); + enableAlpha(); Gui.drawModalRectWithCustomSizedTexture(10, 10, 58, 20, 16, 16, TEXTURE_SIZE, TEXTURE_SIZE); } } diff --git a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java index 1daef60b..cd1cd4e9 100755 --- a/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/ConnectionEventHandler.java @@ -3,7 +3,9 @@ package com.replaymod.recording.handler; import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.ModCompat; import com.replaymod.core.utils.Utils; +import com.replaymod.editor.gui.MarkerProcessor; import com.replaymod.recording.Setting; +import com.replaymod.recording.gui.GuiRecordingControls; import com.replaymod.recording.gui.GuiRecordingOverlay; import com.replaymod.recording.packet.PacketListener; import com.replaymod.replaystudio.replay.ReplayFile; @@ -15,19 +17,19 @@ import net.minecraft.network.NetworkManager; import org.apache.logging.log4j.Logger; //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; -import net.minecraftforge.fml.common.network.FMLNetworkEvent.ClientDisconnectionFromServerEvent; +//#if MC>=11300 +import net.minecraft.world.dimension.DimensionType; +//#endif import static com.replaymod.core.versions.MCVer.WorldType_DEBUG_ALL_BLOCK_STATES; -//#else -//$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; -//$$ import cpw.mods.fml.common.network.FMLNetworkEvent.ClientDisconnectionFromServerEvent; //#endif import java.io.File; import java.text.SimpleDateFormat; import java.util.Calendar; +import static com.replaymod.core.versions.MCVer.getMinecraft; + /** * Handles connection events and initiates recording if enabled. */ @@ -36,7 +38,7 @@ public class ConnectionEventHandler { private static final String packetHandlerKey = "packet_handler"; private static final String DATE_FORMAT = "yyyy_MM_dd_HH_mm_ss"; private static final SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT); - private static final Minecraft mc = Minecraft.getMinecraft(); + private static final Minecraft mc = getMinecraft(); private final Logger logger; private final ReplayMod core; @@ -44,6 +46,7 @@ public class ConnectionEventHandler { private RecordingEventHandler recordingEventHandler; private PacketListener packetListener; private GuiRecordingOverlay guiOverlay; + private GuiRecordingControls guiControls; public ConnectionEventHandler(Logger logger, ReplayMod core) { this.logger = logger; @@ -55,7 +58,11 @@ public class ConnectionEventHandler { boolean local = networkManager.isLocalChannel(); if (local) { //#if MC>=10800 - if (mc.getIntegratedServer().getEntityWorld().getWorldType() == WorldType_DEBUG_ALL_BLOCK_STATES) { + //#if MC>=11300 + if (mc.getIntegratedServer().getWorld(DimensionType.OVERWORLD).getWorldType() == WorldType_DEBUG_ALL_BLOCK_STATES) { + //#else + //$$ if (mc.getIntegratedServer().getEntityWorld().getWorldType() == WorldType_DEBUG_ALL_BLOCK_STATES) { + //#endif logger.info("Debug World recording is not supported."); return; } @@ -74,10 +81,10 @@ public class ConnectionEventHandler { String worldName; if (local) { worldName = mc.getIntegratedServer().getWorldName(); - } else if (Minecraft.getMinecraft().getCurrentServerData() != null) { - worldName = Minecraft.getMinecraft().getCurrentServerData().serverIP; + } else if (mc.getCurrentServerData() != null) { + worldName = mc.getCurrentServerData().serverIP; //#if MC>=11100 - } else if (Minecraft.getMinecraft().isConnectedToRealms()) { + } else if (mc.isConnectedToRealms()) { // we can't access the server name without tapping too deep in the Realms Library worldName = "A Realms Server"; //#endif @@ -97,28 +104,36 @@ public class ConnectionEventHandler { ReplayMetaData metaData = new ReplayMetaData(); metaData.setSingleplayer(local); metaData.setServerName(worldName); - metaData.setGenerator("ReplayMod v" + ReplayMod.getContainer().getVersion()); + metaData.setGenerator("ReplayMod v" + ReplayMod.instance.getVersion()); metaData.setDate(System.currentTimeMillis()); metaData.setMcVersion(ReplayMod.getMinecraftVersion()); - packetListener = new PacketListener(replayFile, metaData); + packetListener = new PacketListener(core, currentFile.toPath(), replayFile, metaData); networkManager.channel().pipeline().addBefore(packetHandlerKey, "replay_recorder", packetListener); recordingEventHandler = new RecordingEventHandler(packetListener); recordingEventHandler.register(); - guiOverlay = new GuiRecordingOverlay(mc, core.getSettingsRegistry()); + guiControls = new GuiRecordingControls(core, packetListener); + guiControls.register(); + + guiOverlay = new GuiRecordingOverlay(mc, core.getSettingsRegistry(), guiControls); guiOverlay.register(); - core.printInfoToChat("replaymod.chat.recordingstarted"); + if (core.getSettingsRegistry().get(Setting.AUTO_START_RECORDING)) { + core.printInfoToChat("replaymod.chat.recordingstarted"); + } else { + packetListener.addMarker(MarkerProcessor.MARKER_NAME_START_CUT, 0); + } } catch (Throwable e) { e.printStackTrace(); core.printWarningToChat("replaymod.chat.recordingfailed"); } } - @SubscribeEvent - public void onDisconnectedFromServerEvent(ClientDisconnectionFromServerEvent event) { + public void reset() { if (packetListener != null) { + guiControls.unregister(); + guiControls = null; guiOverlay.unregister(); guiOverlay = null; recordingEventHandler.unregister(); diff --git a/src/main/java/com/replaymod/recording/handler/FMLHandshakeFilter.java b/src/main/java/com/replaymod/recording/handler/FMLHandshakeFilter.java index 036be75f..d58262de 100644 --- a/src/main/java/com/replaymod/recording/handler/FMLHandshakeFilter.java +++ b/src/main/java/com/replaymod/recording/handler/FMLHandshakeFilter.java @@ -1,34 +1,36 @@ -package com.replaymod.recording.handler; - -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.SimpleChannelInboundHandler; - +//#if MC<11300 +//$$ package com.replaymod.recording.handler; +//$$ +//$$ import io.netty.channel.ChannelHandlerContext; +//$$ import io.netty.channel.SimpleChannelInboundHandler; +//$$ //#if MC>=10800 -import net.minecraftforge.fml.common.network.handshake.FMLHandshakeMessage; +//$$ import net.minecraftforge.fml.common.network.handshake.FMLHandshakeMessage; //#else //$$ import cpw.mods.fml.common.network.handshake.FMLHandshakeMessage; //#endif - -/** - * Filters out all handshake packets that were sent for recording but must - * not actually be handled. - * This handler is only present when connected to the integrated server as - * otherwise all packets must be handled. - * - * When in single player, the game state packets must never be handled - * otherwise wired bugs related to semi-singletons can occur. - * See https://bugs.replaymod.com/show_bug.cgi?id=44 - */ -public class FMLHandshakeFilter extends SimpleChannelInboundHandler { - @Override - protected void channelRead0(ChannelHandlerContext ctx, FMLHandshakeMessage msg) throws Exception { +//$$ +//$$ /** +//$$ * Filters out all handshake packets that were sent for recording but must +//$$ * not actually be handled. +//$$ * This handler is only present when connected to the integrated server as +//$$ * otherwise all packets must be handled. +//$$ * +//$$ * When in single player, the game state packets must never be handled +//$$ * otherwise wired bugs related to semi-singletons can occur. +//$$ * See https://bugs.replaymod.com/show_bug.cgi?id=44 +//$$ */ +//$$ public class FMLHandshakeFilter extends SimpleChannelInboundHandler { +//$$ @Override +//$$ protected void channelRead0(ChannelHandlerContext ctx, FMLHandshakeMessage msg) throws Exception { //#if MC>=10800 - if (!(msg instanceof FMLHandshakeMessage.RegistryData)) { + //$$ if (!(msg instanceof FMLHandshakeMessage.RegistryData)) { //#else //$$ if (!(msg instanceof FMLHandshakeMessage.ModIdData)) { //#endif - // Pass on everything but RegistryData messages - ctx.fireChannelRead(msg); - } - } -} +//$$ // Pass on everything but RegistryData messages +//$$ ctx.fireChannelRead(msg); +//$$ } +//$$ } +//$$ } +//#endif diff --git a/src/main/java/com/replaymod/recording/handler/GuiHandler.java b/src/main/java/com/replaymod/recording/handler/GuiHandler.java index 8a1e7b94..6d76f468 100644 --- a/src/main/java/com/replaymod/recording/handler/GuiHandler.java +++ b/src/main/java/com/replaymod/recording/handler/GuiHandler.java @@ -17,7 +17,11 @@ import net.minecraftforge.client.event.GuiScreenEvent; import net.minecraftforge.common.MinecraftForge; //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; //#endif diff --git a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java index 2a44af9e..a3313842 100755 --- a/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java +++ b/src/main/java/com/replaymod/recording/handler/RecordingEventHandler.java @@ -7,11 +7,15 @@ import net.minecraft.item.ItemStack; import net.minecraft.network.Packet; import net.minecraft.network.play.server.*; import net.minecraft.server.integrated.IntegratedServer; -import net.minecraftforge.event.entity.minecart.MinecartInteractEvent; +// FIXME not (yet?) 1.13 import net.minecraftforge.event.entity.minecart.MinecartInteractEvent; import net.minecraftforge.event.entity.player.PlayerSleepInBedEvent; //#if MC>=10904 -import net.minecraft.entity.EntityLiving; +//#if MC>=11300 +import net.minecraft.entity.EntityLivingBase; +//#else +//$$ import net.minecraft.entity.EntityLiving; +//#endif import net.minecraft.inventory.EntityEquipmentSlot; import net.minecraft.network.datasync.EntityDataManager; import net.minecraft.util.EnumHand; @@ -25,7 +29,11 @@ import net.minecraft.util.math.BlockPos; //#endif //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif import net.minecraftforge.fml.common.gameevent.PlayerEvent.ItemPickupEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; import net.minecraftforge.fml.common.gameevent.TickEvent.PlayerTickEvent; @@ -42,7 +50,7 @@ import static com.replaymod.core.versions.MCVer.*; public class RecordingEventHandler { - private final Minecraft mc = Minecraft.getMinecraft(); + private final Minecraft mc = getMinecraft(); private final PacketListener packetListener; private Double lastX, lastY, lastZ; @@ -75,6 +83,12 @@ public class RecordingEventHandler { } } + //#if MC>=11300 + public void onPacket(Packet packet) { + packetListener.save(packet); + } + //#endif + public void onPlayerJoin() { try { //#if MC>=10904 @@ -171,9 +185,15 @@ public class RecordingEventHandler { byte newPitch = (byte) ((int) (e.player.rotationPitch * 256.0F / 360.0F)); //#if MC>=10904 - packet = new SPacketEntity.S17PacketEntityLookMove(e.player.getEntityId(), + //#if MC>=11300 + packet = new SPacketEntity.Move( + //#else + //$$ packet = new SPacketEntity.S17PacketEntityLookMove( + //#endif + e.player.getEntityId(), (short) Math.round(dx * 4096), (short) Math.round(dy * 4096), (short) Math.round(dz * 4096), - newYaw, newPitch, e.player.onGround); + newYaw, newPitch, e.player.onGround + ); //#else //$$ packet = new S14PacketEntity.S17PacketEntityLookMove(e.player.getEntityId(), //$$ (byte) Math.round(dx * 32), (byte) Math.round(dy * 32), (byte) Math.round(dz * 32), @@ -300,9 +320,15 @@ public class RecordingEventHandler { //Leaving Ride - if((!player(mc).isRiding() && lastRiding != -1) || - (player(mc).isRiding() && lastRiding != getRidingEntity(player(mc)).getEntityId())) { - if(!player(mc).isRiding()) { + //#if MC>=11300 + if((!player(mc).isPassenger() && lastRiding != -1) || + (player(mc).isPassenger() && lastRiding != getRidingEntity(player(mc)).getEntityId())) { + if(!player(mc).isPassenger()) { + //#else + //$$ if((!player(mc).isRiding() && lastRiding != -1) || + //$$ (player(mc).isRiding() && lastRiding != getRidingEntity(player(mc)).getEntityId())) { + //$$ if(!player(mc).isRiding()) { + //#endif lastRiding = -1; } else { lastRiding = getRidingEntity(player(mc)).getEntityId(); @@ -331,7 +357,11 @@ public class RecordingEventHandler { lastActiveHand = player(mc).getActiveHand(); EntityDataManager dataManager = new EntityDataManager(null); int state = (wasHandActive ? 1 : 0) | (lastActiveHand == EnumHand.OFF_HAND ? 2 : 0); - dataManager.register(EntityLiving.HAND_STATES, (byte) state); + //#if MC>=11300 + dataManager.register(EntityLivingBase.LIVING_FLAGS, (byte) state); + //#else + //$$ dataManager.register(EntityLiving.HAND_STATES, (byte) state); + //#endif packetListener.save(new SPacketEntityMetadata(player(mc).getEntityId(), dataManager, true)); } //#endif @@ -346,8 +376,17 @@ public class RecordingEventHandler { try { //#if MC>=11100 //#if MC>=11200 - packetListener.save(new SPacketCollectItem(event.pickedUp.getEntityId(), event.player.getEntityId(), - event.pickedUp.getItem().getMaxStackSize())); + //#if MC>=11300 + ItemStack stack = event.getStack(); + packetListener.save(new SPacketCollectItem( + event.getOriginalEntity().getEntityId(), + event.getPlayer().getEntityId(), + event.getStack().getCount() + )); + //#else + //$$ packetListener.save(new SPacketCollectItem(event.pickedUp.getEntityId(), event.player.getEntityId(), + //$$ event.pickedUp.getItem().getMaxStackSize())); + //#endif //#else //$$ packetListener.save(new SPacketCollectItem(event.pickedUp.getEntityId(), event.player.getEntityId(), //$$ event.pickedUp.getEntityItem().getMaxStackSize())); @@ -394,6 +433,7 @@ public class RecordingEventHandler { } } + /* FIXME event not (yet?) on 1.13 @SubscribeEvent public void enterMinecart(MinecartInteractEvent event) { try { @@ -418,6 +458,7 @@ public class RecordingEventHandler { e.printStackTrace(); } } + */ //#if MC>=10800 public void onBlockBreakAnim(int breakerId, BlockPos pos, int progress) { diff --git a/src/main/java/com/replaymod/recording/mixin/MixinDownloadingPackFinder.java b/src/main/java/com/replaymod/recording/mixin/MixinDownloadingPackFinder.java new file mode 100644 index 00000000..226f21ce --- /dev/null +++ b/src/main/java/com/replaymod/recording/mixin/MixinDownloadingPackFinder.java @@ -0,0 +1,36 @@ +//#if MC>=11300 +package com.replaymod.recording.mixin; + +import com.google.common.util.concurrent.ListenableFuture; +import com.replaymod.recording.packet.ResourcePackRecorder; +import de.johni0702.minecraft.gui.utils.Consumer; +import net.minecraft.client.resources.DownloadingPackFinder; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import java.io.File; + +@Mixin(DownloadingPackFinder.class) +public abstract class MixinDownloadingPackFinder implements ResourcePackRecorder.IDownloadingPackFinder { + private Consumer requestCallback; + + @Override + public void setRequestCallback(Consumer callback) { + requestCallback = callback; + } + + @Shadow + public abstract ListenableFuture func_195741_a(File file); + + @Redirect(method = "downloadResourcePack", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/DownloadingPackFinder;func_195741_a(Ljava/io/File;)Lcom/google/common/util/concurrent/ListenableFuture;")) + private ListenableFuture recordDownloadedPack(DownloadingPackFinder downloadingPackFinder, File file) { + if (requestCallback != null) { + requestCallback.consume(file); + requestCallback = null; + } + return func_195741_a(file); + } +} +//#endif diff --git a/src/main/java/com/replaymod/recording/mixin/MixinKeyboardListener.java b/src/main/java/com/replaymod/recording/mixin/MixinKeyboardListener.java new file mode 100644 index 00000000..c52c209d --- /dev/null +++ b/src/main/java/com/replaymod/recording/mixin/MixinKeyboardListener.java @@ -0,0 +1,45 @@ +//#if MC>=11300 +package com.replaymod.recording.mixin; + +import com.replaymod.core.versions.MCVer; +import com.replaymod.extras.advancedscreenshots.AdvancedScreenshots; +import com.replaymod.replay.ReplayModReplay; +import net.minecraft.client.KeyboardListener; +import net.minecraft.client.settings.KeyBinding; +import net.minecraft.client.shader.Framebuffer; +import net.minecraft.util.ScreenShotHelper; +import net.minecraft.util.text.ITextComponent; +import net.minecraftforge.fml.hooks.BasicEventHooks; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.io.File; +import java.util.function.Consumer; + +@Mixin(KeyboardListener.class) +public abstract class MixinKeyboardListener { + @Inject(method = "onKeyEvent", at = @At("HEAD"), cancellable = true) + private void dispatchKeyEvent(long windowPointer, int key, int scanCode, int action, int modifiers, CallbackInfo ci) { + // FIXME this is a hack and will hopefully be done properly by forge before its release (MinecraftForge#5481) + // (and therefore before RM's release), this code will have to be removed then and handling code updated + KeyBinding keyBindChat = MCVer.getMinecraft().gameSettings.keyBindChat; + if (action == 1 && keyBindChat.matchesKey(key, scanCode)) { + keyBindChat.pressTime++; + BasicEventHooks.fireKeyInput(); + ci.cancel(); + } + } + + @Redirect(method = "onKeyEvent", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/ScreenShotHelper;saveScreenshot(Ljava/io/File;IILnet/minecraft/client/shader/Framebuffer;Ljava/util/function/Consumer;)V")) + private void takeScreenshot(File p_148260_0_, int p_148260_1_, int p_148260_2_, Framebuffer p_148260_3_, Consumer p_148260_4_) { + if (ReplayModReplay.instance.getReplayHandler() != null) { + AdvancedScreenshots.take(); + } else { + ScreenShotHelper.saveScreenshot(p_148260_0_, p_148260_1_, p_148260_2_, p_148260_3_, p_148260_4_); + } + } +} +//#endif diff --git a/src/main/java/com/replaymod/recording/mixin/MixinMouseHelper.java b/src/main/java/com/replaymod/recording/mixin/MixinMouseHelper.java new file mode 100644 index 00000000..4dba3a3f --- /dev/null +++ b/src/main/java/com/replaymod/recording/mixin/MixinMouseHelper.java @@ -0,0 +1,41 @@ +//#if MC>=11300 +package com.replaymod.recording.mixin; + +import com.replaymod.replay.InputReplayTimer; +import com.replaymod.replay.ReplayModReplay; +import net.minecraft.client.MouseHelper; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +@Mixin(MouseHelper.class) +public abstract class MixinMouseHelper { + //#ifdef DEV_ENV + @Shadow + private boolean mouseGrabbed; + + @Inject(method = "grabMouse", at = @At("HEAD"), cancellable = true) + private void noGrab(CallbackInfo ci) { + // Used to be provided by Forge for 1.12.2 and below + if (Boolean.valueOf(System.getProperty("fml.noGrab", "false"))) { + mouseGrabbed = true; + ci.cancel(); + } + } + //#endif + + @Inject(method = "scrollCallback", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/entity/EntityPlayerSP;isSpectator()Z"), + locals = LocalCapture.CAPTURE_FAILHARD, + cancellable = true) + private void handleReplayModScroll(long _p0, double _p1, double _p2, CallbackInfo ci, double _l1, double yOffsetAccumulated) { + if (ReplayModReplay.instance.getReplayHandler() != null) { + InputReplayTimer.handleScroll((int) yOffsetAccumulated); + ci.cancel(); + } + } +} +//#endif diff --git a/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerLoginClient.java b/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerLoginClient.java index 2431c3a0..e3df345e 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerLoginClient.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerLoginClient.java @@ -9,11 +9,17 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +//#if MC>=11300 +import com.replaymod.core.versions.MCVer; +import com.replaymod.recording.handler.RecordingEventHandler.RecordingEventSender; +import net.minecraft.network.login.server.SPacketCustomPayloadLogin; +//#else //#if MC>=10800 -import net.minecraftforge.fml.common.network.FMLNetworkEvent; +//$$ import net.minecraftforge.fml.common.network.FMLNetworkEvent; //#else //$$ import cpw.mods.fml.common.network.FMLNetworkEvent; //#endif +//#endif @Mixin(NetHandlerLoginClient.class) public abstract class MixinNetHandlerLoginClient { @@ -25,19 +31,33 @@ public abstract class MixinNetHandlerLoginClient { //$$ private NetworkManager field_147393_d; //#endif - /** - * Starts the recording right before switching into PLAY state. - * We cannot use the {@link FMLNetworkEvent.ClientConnectedToServerEvent} - * as it only fires after the forge handshake. - */ - @Inject(method = "handleLoginSuccess", at=@At("HEAD")) - public void replayModRecording_initiateRecording(CallbackInfo cb) { - //#if MC>=10800 + //#if MC>=11300 + @Inject(method = "func_209521_a", at=@At("HEAD")) + private void replayModRecording_initiateRecording(SPacketCustomPayloadLogin packet, CallbackInfo ci) { + RecordingEventSender eventSender = (RecordingEventSender) MCVer.getMinecraft().renderGlobal; + if (eventSender.getRecordingEventHandler() != null) { + return; // already recording + } ReplayModRecording.instance.initiateRecording(networkManager); + if (eventSender.getRecordingEventHandler() != null) { + eventSender.getRecordingEventHandler().onPacket(packet); + } + } + //#else + //$$ /** + //$$ * Starts the recording right before switching into PLAY state. + //$$ * We cannot use the {@link FMLNetworkEvent.ClientConnectedToServerEvent} + //$$ * as it only fires after the forge handshake. + //$$ */ + //$$ @Inject(method = "handleLoginSuccess", at=@At("HEAD")) + //$$ public void replayModRecording_initiateRecording(CallbackInfo cb) { + //#if MC>=10800 + //$$ ReplayModRecording.instance.initiateRecording(networkManager); //#else //$$ ReplayModRecording.instance.initiateRecording(field_147393_d); //#endif - } + //$$ } + //#endif //#if MC>=11200 @Inject(method = "handleLoginSuccess", at=@At("RETURN")) diff --git a/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerPlayClient.java b/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerPlayClient.java index c7642946..6af64ea5 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerPlayClient.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinNetHandlerPlayClient.java @@ -1,5 +1,6 @@ package com.replaymod.recording.mixin; +import com.replaymod.core.versions.MCVer; import com.replaymod.recording.handler.RecordingEventHandler; import net.minecraft.client.Minecraft; import net.minecraft.client.network.NetHandlerPlayClient; @@ -30,8 +31,7 @@ import static com.replaymod.core.versions.MCVer.*; @Mixin(NetHandlerPlayClient.class) public abstract class MixinNetHandlerPlayClient { - @Shadow - private Minecraft gameController; + private static Minecraft gameController = MCVer.getMinecraft(); //#if MC>=10800 @Shadow diff --git a/src/main/java/com/replaymod/recording/mixin/MixinNetworkDispatcher.java b/src/main/java/com/replaymod/recording/mixin/MixinNetworkDispatcher.java index a0a30a11..7f91df30 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinNetworkDispatcher.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinNetworkDispatcher.java @@ -1,75 +1,77 @@ -package com.replaymod.recording.mixin; - -import com.replaymod.recording.handler.FMLHandshakeFilter; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.embedded.EmbeddedChannel; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - +//#if MC<11300 +//$$ package com.replaymod.recording.mixin; +//$$ +//$$ import com.replaymod.recording.handler.FMLHandshakeFilter; +//$$ import io.netty.channel.ChannelPipeline; +//$$ import io.netty.channel.embedded.EmbeddedChannel; +//$$ import org.spongepowered.asm.mixin.Mixin; +//$$ import org.spongepowered.asm.mixin.Shadow; +//$$ import org.spongepowered.asm.mixin.injection.At; +//$$ import org.spongepowered.asm.mixin.injection.Inject; +//$$ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +//$$ //#if MC>=11200 -import io.netty.channel.ChannelConfig; -import net.minecraft.network.EnumConnectionState; -import net.minecraft.network.NetworkManager; -import org.spongepowered.asm.mixin.injection.Redirect; +//$$ import io.netty.channel.ChannelConfig; +//$$ import net.minecraft.network.EnumConnectionState; +//$$ import net.minecraft.network.NetworkManager; +//$$ import org.spongepowered.asm.mixin.injection.Redirect; //#endif - +//$$ //#if MC>=10800 -import net.minecraftforge.fml.common.network.handshake.FMLHandshakeCodec; -import net.minecraftforge.fml.common.network.handshake.NetworkDispatcher; -import net.minecraftforge.fml.relauncher.Side; +//$$ import net.minecraftforge.fml.common.network.handshake.FMLHandshakeCodec; +//$$ import net.minecraftforge.fml.common.network.handshake.NetworkDispatcher; +//$$ import net.minecraftforge.fml.relauncher.Side; //#else //$$ import cpw.mods.fml.common.network.handshake.FMLHandshakeCodec; //$$ import cpw.mods.fml.common.network.handshake.NetworkDispatcher; //$$ import cpw.mods.fml.relauncher.Side; //#endif - -@Mixin(value = NetworkDispatcher.class, remap = false) -public abstract class MixinNetworkDispatcher { - - @Shadow - private Side side; - - @Shadow - private EmbeddedChannel handshakeChannel; - - /** - * Always sets fml:isLocal to false on the server side. - * This effectively removes the difference in the FML handshake between SP and MP - * and forces the block/item ids, etc. to always be send. - * Injects a {@link FMLHandshakeFilter} on the client side to filter out - * those extra, unexpected packets. - */ - @Inject(method = "insertIntoChannel", at=@At("HEAD")) - public void replayModRecording_setupForLocalRecording(CallbackInfo cb) { - // If we're in multiplayer, everything is fine as is - if (!handshakeChannel.attr(NetworkDispatcher.IS_LOCAL).get()) return; - - if (side == Side.SERVER) { - // On the server side, force all packets to be sent - handshakeChannel.attr(NetworkDispatcher.IS_LOCAL).set(false); - } else { - // On the client side, discard additional packets - ChannelPipeline pipeline = handshakeChannel.pipeline(); - pipeline.addAfter(pipeline.context(FMLHandshakeCodec.class).name(), - "replaymod_filter", new FMLHandshakeFilter()); - } - } - +//$$ +//$$ @Mixin(value = NetworkDispatcher.class, remap = false) +//$$ public abstract class MixinNetworkDispatcher { +//$$ +//$$ @Shadow +//$$ private Side side; +//$$ +//$$ @Shadow +//$$ private EmbeddedChannel handshakeChannel; +//$$ +//$$ /** +//$$ * Always sets fml:isLocal to false on the server side. +//$$ * This effectively removes the difference in the FML handshake between SP and MP +//$$ * and forces the block/item ids, etc. to always be send. +//$$ * Injects a {@link FMLHandshakeFilter} on the client side to filter out +//$$ * those extra, unexpected packets. +//$$ */ +//$$ @Inject(method = "insertIntoChannel", at=@At("HEAD")) +//$$ public void replayModRecording_setupForLocalRecording(CallbackInfo cb) { +//$$ // If we're in multiplayer, everything is fine as is +//$$ if (!handshakeChannel.attr(NetworkDispatcher.IS_LOCAL).get()) return; +//$$ +//$$ if (side == Side.SERVER) { +//$$ // On the server side, force all packets to be sent +//$$ handshakeChannel.attr(NetworkDispatcher.IS_LOCAL).set(false); +//$$ } else { +//$$ // On the client side, discard additional packets +//$$ ChannelPipeline pipeline = handshakeChannel.pipeline(); +//$$ pipeline.addAfter(pipeline.context(FMLHandshakeCodec.class).name(), +//$$ "replaymod_filter", new FMLHandshakeFilter()); +//$$ } +//$$ } +//$$ //#if MC>=11200 - @Redirect(method = "clientListenForServerHandshake", at = @At(value = "INVOKE", remap = true, target = - "Lnet/minecraft/network/NetworkManager;setConnectionState(Lnet/minecraft/network/EnumConnectionState;)V")) - public void replayModRecording_raceConditionWorkAround1(NetworkManager self, EnumConnectionState ignored) { } - - @Redirect(method = "insertIntoChannel", at = @At(value = "INVOKE", target = - "Lio/netty/channel/ChannelConfig;setAutoRead(Z)Lio/netty/channel/ChannelConfig;")) - public ChannelConfig replayModRecording_raceConditionWorkAround2(ChannelConfig self, boolean autoRead) { - if (side == Side.CLIENT) { - autoRead = false; - } - return self.setAutoRead(autoRead); - } + //$$ @Redirect(method = "clientListenForServerHandshake", at = @At(value = "INVOKE", remap = true, target = + //$$ "Lnet/minecraft/network/NetworkManager;setConnectionState(Lnet/minecraft/network/EnumConnectionState;)V")) + //$$ public void replayModRecording_raceConditionWorkAround1(NetworkManager self, EnumConnectionState ignored) { } + //$$ + //$$ @Redirect(method = "insertIntoChannel", at = @At(value = "INVOKE", target = + //$$ "Lio/netty/channel/ChannelConfig;setAutoRead(Z)Lio/netty/channel/ChannelConfig;")) + //$$ public ChannelConfig replayModRecording_raceConditionWorkAround2(ChannelConfig self, boolean autoRead) { + //$$ if (side == Side.CLIENT) { + //$$ autoRead = false; + //$$ } + //$$ return self.setAutoRead(autoRead); + //$$ } //#endif -} +//$$ } +//#endif diff --git a/src/main/java/com/replaymod/recording/mixin/MixinPlayerControllerMP.java b/src/main/java/com/replaymod/recording/mixin/MixinPlayerControllerMP.java index 91b216ff..37bf6451 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinPlayerControllerMP.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinPlayerControllerMP.java @@ -1,32 +1,32 @@ package com.replaymod.recording.mixin; -//#if MC>=10904 -import com.replaymod.recording.handler.RecordingEventHandler; -import net.minecraft.client.Minecraft; -import net.minecraft.client.multiplayer.PlayerControllerMP; -import net.minecraft.util.math.BlockPos; -import net.minecraft.world.World; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; - -import static com.replaymod.core.versions.MCVer.*; - -@Mixin(PlayerControllerMP.class) -public abstract class MixinPlayerControllerMP implements RecordingEventHandler.RecordingEventSender { - - @Shadow - private Minecraft mc; - - // Redirects the call to playEvent without the initial player argument to the method with that argument - // The new method will then play it and (if applicable) record it. (See MixinWorldClient) - // This is necessary for the block break event (particles and sound) to be recorded. Otherwise it looks like the - // event was emitted because of a packet (player will be null) and not as it actually was (by the player). - @Redirect(method = "onPlayerDestroyBlock", at = @At(value = "INVOKE", - target = "Lnet/minecraft/world/World;playEvent(ILnet/minecraft/util/math/BlockPos;I)V")) - public void replayModRecording_playEvent_fixed(World world, int type, BlockPos pos, int data) { - world.playEvent(player(mc), type, pos, data); - } -} +//#if MC>=10904 && MC<11300 +//$$ import com.replaymod.recording.handler.RecordingEventHandler; +//$$ import net.minecraft.client.Minecraft; +//$$ import net.minecraft.client.multiplayer.PlayerControllerMP; +//$$ import net.minecraft.util.math.BlockPos; +//$$ import net.minecraft.world.World; +//$$ import org.spongepowered.asm.mixin.Mixin; +//$$ import org.spongepowered.asm.mixin.Shadow; +//$$ import org.spongepowered.asm.mixin.injection.At; +//$$ import org.spongepowered.asm.mixin.injection.Redirect; +//$$ +//$$ import static com.replaymod.core.versions.MCVer.*; +//$$ +//$$ @Mixin(PlayerControllerMP.class) +//$$ public abstract class MixinPlayerControllerMP implements RecordingEventHandler.RecordingEventSender { +//$$ +//$$ @Shadow +//$$ private Minecraft mc; +//$$ +//$$ // Redirects the call to playEvent without the initial player argument to the method with that argument +//$$ // The new method will then play it and (if applicable) record it. (See MixinWorldClient) +//$$ // This is necessary for the block break event (particles and sound) to be recorded. Otherwise it looks like the +//$$ // event was emitted because of a packet (player will be null) and not as it actually was (by the player). +//$$ @Redirect(method = "onPlayerDestroyBlock", at = @At(value = "INVOKE", +//$$ target = "Lnet/minecraft/world/World;playEvent(ILnet/minecraft/util/math/BlockPos;I)V")) +//$$ public void replayModRecording_playEvent_fixed(World world, int type, BlockPos pos, int data) { +//$$ world.playEvent(player(mc), type, pos, data); +//$$ } +//$$ } //#endif diff --git a/src/main/java/com/replaymod/recording/mixin/MixinRenderGlobal.java b/src/main/java/com/replaymod/recording/mixin/MixinRenderGlobal.java index ad65c8c7..637ba04a 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinRenderGlobal.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinRenderGlobal.java @@ -1,12 +1,17 @@ package com.replaymod.recording.mixin; import com.replaymod.recording.handler.RecordingEventHandler; -import net.minecraft.client.renderer.RenderGlobal; 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; +//#if MC>=11300 +import net.minecraft.client.renderer.WorldRenderer; +//#else +//$$ import net.minecraft.client.renderer.RenderGlobal; +//#endif + //#if MC>=10904 import net.minecraft.util.math.BlockPos; //#else @@ -15,7 +20,11 @@ import net.minecraft.util.math.BlockPos; //#endif //#endif -@Mixin(RenderGlobal.class) +//#if MC>=11300 +@Mixin(WorldRenderer.class) +//#else +//$$ @Mixin(RenderGlobal.class) +//#endif public abstract class MixinRenderGlobal implements RecordingEventHandler.RecordingEventSender { private RecordingEventHandler recordingEventHandler; diff --git a/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java b/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java index 2cbce729..39137b2d 100644 --- a/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java +++ b/src/main/java/com/replaymod/recording/mixin/MixinWorldClient.java @@ -10,7 +10,6 @@ import net.minecraft.util.SoundCategory; import net.minecraft.util.SoundEvent; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; -import net.minecraft.world.WorldProvider; import net.minecraft.world.storage.ISaveHandler; import net.minecraft.world.storage.WorldInfo; import org.spongepowered.asm.mixin.Mixin; @@ -19,6 +18,13 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +//#if MC>=11300 +import net.minecraft.world.dimension.Dimension; +import net.minecraft.world.storage.WorldSavedDataStorage; +//#else +//$$ import net.minecraft.world.WorldProvider; +//#endif + import static com.replaymod.core.versions.MCVer.*; @Mixin(WorldClient.class) @@ -26,8 +32,22 @@ public abstract class MixinWorldClient extends World implements RecordingEventHa @Shadow private Minecraft mc; - protected MixinWorldClient(ISaveHandler saveHandlerIn, WorldInfo info, WorldProvider providerIn, Profiler profilerIn, boolean client) { - super(saveHandlerIn, info, providerIn, profilerIn, client); + protected MixinWorldClient(ISaveHandler saveHandlerIn, + //#if MC>=11300 + WorldSavedDataStorage mapStorage, + //#endif + WorldInfo info, + //#if MC>=11300 + Dimension providerIn, + //#else + //$$ WorldProvider providerIn, + //#endif + Profiler profilerIn, boolean client) { + super(saveHandlerIn, + //#if MC>=11300 + mapStorage, + //#endif + info, providerIn, profilerIn, client); } private RecordingEventHandler replayModRecording_getRecordingEventHandler() { diff --git a/src/main/java/com/replaymod/recording/packet/PacketListener.java b/src/main/java/com/replaymod/recording/packet/PacketListener.java index eddd68f1..dc3d8371 100755 --- a/src/main/java/com/replaymod/recording/packet/PacketListener.java +++ b/src/main/java/com/replaymod/recording/packet/PacketListener.java @@ -1,9 +1,16 @@ package com.replaymod.recording.packet; +import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.Restrictions; +import com.replaymod.editor.gui.MarkerProcessor; +import com.replaymod.recording.ReplayModRecording; +import com.replaymod.recording.Setting; +import com.replaymod.recording.handler.ConnectionEventHandler; import com.replaymod.replaystudio.data.Marker; import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ReplayMetaData; +import de.johni0702.minecraft.gui.element.GuiLabel; +import de.johni0702.minecraft.gui.utils.Colors; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; @@ -17,6 +24,10 @@ import net.minecraft.network.play.server.*; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +//#if MC>=11300 +import net.minecraft.network.login.server.SPacketLoginSuccess; +//#endif + //#if MC>=10904 import net.minecraft.network.datasync.EntityDataManager; import net.minecraft.util.text.TextComponentString; @@ -29,13 +40,16 @@ import net.minecraft.util.text.TextComponentString; //#if MC>=10800 import net.minecraft.network.EnumPacketDirection; -import net.minecraftforge.fml.common.network.internal.FMLProxyPacket; +//#if MC<11300 +//$$ import net.minecraftforge.fml.common.network.internal.FMLProxyPacket; +//#endif //#else //$$ import cpw.mods.fml.common.network.internal.FMLProxyPacket; //#endif import java.io.DataOutputStream; import java.io.IOException; +import java.nio.file.Path; import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -49,9 +63,11 @@ import static com.replaymod.core.versions.MCVer.*; public class PacketListener extends ChannelInboundHandlerAdapter { - private static final Minecraft mc = Minecraft.getMinecraft(); + private static final Minecraft mc = getMinecraft(); private static final Logger logger = LogManager.getLogger(); + private final ReplayMod core; + private final Path outputPath; private final ReplayFile replayFile; private final ResourcePackRecorder resourcePackRecorder; @@ -67,6 +83,11 @@ public class PacketListener extends ChannelInboundHandlerAdapter { private long lastSentPacket; private long timePassedWhilePaused; private volatile boolean serverWasPaused; + //#if MC>=11300 + private EnumConnectionState connectionState = EnumConnectionState.LOGIN; + //#else + //$$ private EnumConnectionState connectionState = EnumConnectionState.PLAY; + //#endif /** * Used to keep track of the last metadata save job submitted to the save service and @@ -74,7 +95,9 @@ public class PacketListener extends ChannelInboundHandlerAdapter { */ private final AtomicInteger lastSaveMetaDataId = new AtomicInteger(); - public PacketListener(ReplayFile replayFile, ReplayMetaData metaData) throws IOException { + public PacketListener(ReplayMod core, Path outputPath, ReplayFile replayFile, ReplayMetaData metaData) throws IOException { + this.core = core; + this.outputPath = outputPath; this.replayFile = replayFile; this.metaData = metaData; this.resourcePackRecorder = new ResourcePackRecorder(replayFile); @@ -147,6 +170,12 @@ public class PacketListener extends ChannelInboundHandlerAdapter { throw new RuntimeException(e); } }); + + //#if MC>=11300 + if (packet instanceof SPacketLoginSuccess) { + connectionState = EnumConnectionState.PLAY; + } + //#endif } catch(Exception e) { logger.error("Writing packet:", e); } @@ -157,21 +186,39 @@ public class PacketListener extends ChannelInboundHandlerAdapter { metaData.setDuration((int) lastSentPacket); saveMetaData(); - saveService.shutdown(); - try { - saveService.awaitTermination(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - logger.error("Waiting for save service termination:", e); - } - - synchronized (replayFile) { - try { - replayFile.save(); - replayFile.close(); - } catch (IOException e) { - logger.error("Saving replay file:", e); + core.runLater(() -> { + ConnectionEventHandler connectionEventHandler = ReplayModRecording.instance.getConnectionEventHandler(); + if (connectionEventHandler.getPacketListener() == this) { + connectionEventHandler.reset(); } - } + }); + + GuiLabel savingLabel = new GuiLabel().setI18nText("replaymod.gui.replaysaving.title").setColor(Colors.BLACK); + new Thread(() -> { + core.runLater(() -> core.getBackgroundProcesses().addProcess(savingLabel)); + + saveService.shutdown(); + try { + saveService.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + logger.error("Waiting for save service termination:", e); + } + + synchronized (replayFile) { + try { + replayFile.save(); + replayFile.close(); + + if (core.getSettingsRegistry().get(Setting.AUTO_POST_PROCESS)) { + MarkerProcessor.apply(outputPath, progress -> {}); + } + } catch (IOException e) { + logger.error("Saving replay file:", e); + } + } + + core.runLater(() -> core.getBackgroundProcesses().removeProcess(savingLabel)); + }).start(); } @Override @@ -229,16 +276,18 @@ public class PacketListener extends ChannelInboundHandlerAdapter { //#endif //#endif - if (packet instanceof FMLProxyPacket) { - // This packet requires special handling + //#if MC<11300 + //$$ if (packet instanceof FMLProxyPacket) { + //$$ // This packet requires special handling //#if MC>=10800 - ((FMLProxyPacket) packet).toS3FPackets().forEach(this::save); + //$$ ((FMLProxyPacket) packet).toS3FPackets().forEach(this::save); //#else //$$ save(((FMLProxyPacket) packet).toS3FPacket()); //#endif - super.channelRead(ctx, msg); - return; - } + //$$ super.channelRead(ctx, msg); + //$$ return; + //$$ } + //#endif save(packet); @@ -337,9 +386,9 @@ public class PacketListener extends ChannelInboundHandlerAdapter { //#endif //#if MC>=10800 - Integer packetId = EnumConnectionState.PLAY.getPacketId(EnumPacketDirection.CLIENTBOUND, packet); + Integer packetId = connectionState.getPacketId(EnumPacketDirection.CLIENTBOUND, packet); //#else - //$$ Integer packetId = (Integer) EnumConnectionState.PLAY.func_150755_b().inverse().get(packet.getClass()); + //$$ Integer packetId = (Integer) connectionState.func_150755_b().inverse().get(packet.getClass()); //#endif if (packetId == null) { throw new IOException("Unknown packet type:" + packet.getClass()); @@ -357,11 +406,15 @@ public class PacketListener extends ChannelInboundHandlerAdapter { return array; } - public void addMarker() { - Entity view = getRenderViewEntity(Minecraft.getMinecraft()); - int timestamp = (int) (System.currentTimeMillis() - startTime); + public void addMarker(String name) { + addMarker(name, (int) getCurrentDuration()); + } + + public void addMarker(String name, int timestamp) { + Entity view = getRenderViewEntity(mc); Marker marker = new Marker(); + marker.setName(name); marker.setTime(timestamp); marker.setX(view.posX); marker.setY(view.posY); @@ -382,6 +435,10 @@ public class PacketListener extends ChannelInboundHandlerAdapter { }); } + public long getCurrentDuration() { + return lastSentPacket; + } + public void setServerWasPaused() { this.serverWasPaused = true; } diff --git a/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java index c5818c9e..82de6586 100644 --- a/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java +++ b/src/main/java/com/replaymod/recording/packet/ResourcePackRecorder.java @@ -4,16 +4,22 @@ import com.google.common.hash.Hashing; import com.google.common.io.Files; import com.replaymod.replaystudio.replay.ReplayFile; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiScreenWorking; import net.minecraft.client.gui.GuiYesNo; import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.client.multiplayer.ServerData; import net.minecraft.client.resources.I18n; -import net.minecraft.client.resources.ResourcePackRepository; -import net.minecraft.util.HttpUtil; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +//#if MC>=11300 +import de.johni0702.minecraft.gui.utils.Consumer; +import net.minecraft.client.resources.DownloadingPackFinder; +//#else +//$$ import net.minecraft.client.gui.GuiScreenWorking; +//$$ import net.minecraft.client.resources.ResourcePackRepository; +//$$ import net.minecraft.util.HttpUtil; +//#endif + //#if MC>=10904 import net.minecraft.network.play.client.CPacketResourcePackStatus; import net.minecraft.network.play.client.CPacketResourcePackStatus.Action; @@ -33,11 +39,11 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.network.NetworkManager; -import org.apache.commons.io.FileUtils; +//#if MC<11300 +//$$ import org.apache.commons.io.FileUtils; +//#endif import javax.annotation.Nonnull; - -import static com.replaymod.core.versions.MCVer.*; //#else //$$ import net.minecraft.client.multiplayer.ServerData.ServerResourceMode; //$$ import net.minecraft.client.multiplayer.ServerList; @@ -52,12 +58,14 @@ import java.io.OutputStream; import java.util.HashMap; import java.util.Map; +import static com.replaymod.core.versions.MCVer.*; + /** * Records resource packs and handles incoming resource pack packets during recording. */ public class ResourcePackRecorder { private static final Logger logger = LogManager.getLogger(); - private static final Minecraft mc = Minecraft.getMinecraft(); + private static final Minecraft mc = getMinecraft(); private final ReplayFile replayFile; @@ -131,12 +139,12 @@ public class ResourcePackRecorder { if (url.startsWith("level://")) { String levelName = url.substring("level://".length()); - File savesDir = new File(mc.mcDataDir, "saves"); + File savesDir = new File(mcDataDir(mc), "saves"); final File levelDir = new File(savesDir, levelName); if (levelDir.isFile()) { netManager.sendPacket(makeStatusPacket(hash, Action.ACCEPTED)); - Futures.addCallback(setServerResourcePack(mc.getResourcePackRepository(), levelDir), new FutureCallback() { + Futures.addCallback(setServerResourcePack(levelDir), new FutureCallback() { @Override public void onSuccess(Object result) { recordResourcePack(levelDir, requestId); @@ -163,7 +171,11 @@ public class ResourcePackRecorder { //noinspection Convert2Lambda mc.addScheduledTask(() -> mc.displayGuiScreen(new GuiYesNo(new GuiYesNoCallback() { @Override - public void confirmClicked(boolean result, int id) { + //#if MC>=11300 + public void confirmResult(boolean result, int id) { + //#else + //$$ public void confirmClicked(boolean result, int id) { + //#endif if (serverData != null) { serverData.setResourceMode(result ? ServerData.ServerResourceMode.ENABLED : ServerData.ServerResourceMode.DISABLED); } @@ -202,100 +214,112 @@ public class ResourcePackRecorder { }); } + //#if MC>=11300 private ListenableFuture downloadResourcePack(final int requestId, String url, String hash) { - final ResourcePackRepository repo = mc.mcResourcePackRepository; - String fileName; - if (hash.matches("^[a-f0-9]{40}$")) { - fileName = hash; - } else { - fileName = url.substring(url.lastIndexOf("/") + 1); + DownloadingPackFinder packFinder = mc.getPackFinder(); + ((IDownloadingPackFinder) packFinder).setRequestCallback(file -> recordResourcePack(file, requestId)); + return packFinder.downloadResourcePack(url, hash); + } - if (fileName.contains("?")) { - fileName = fileName.substring(0, fileName.indexOf("?")); - } - - if (!fileName.endsWith(".zip")) { - return Futures.immediateFailedFuture(new IllegalArgumentException("Invalid filename; must end in .zip")); - } - - fileName = "legacy_" + fileName.replaceAll("\\W", ""); - } - - final File file = new File(repo.dirServerResourcepacks, fileName); + public interface IDownloadingPackFinder { + void setRequestCallback(Consumer callback); + } + //#else + //$$ private ListenableFuture downloadResourcePack(final int requestId, String url, String hash) { + //$$ final ResourcePackRepository repo = mc.mcResourcePackRepository; + //$$ String fileName; + //$$ if (hash.matches("^[a-f0-9]{40}$")) { + //$$ fileName = hash; + //$$ } else { + //$$ fileName = url.substring(url.lastIndexOf("/") + 1); + //$$ + //$$ if (fileName.contains("?")) { + //$$ fileName = fileName.substring(0, fileName.indexOf("?")); + //$$ } + //$$ + //$$ if (!fileName.endsWith(".zip")) { + //$$ return Futures.immediateFailedFuture(new IllegalArgumentException("Invalid filename; must end in .zip")); + //$$ } + //$$ + //$$ fileName = "legacy_" + fileName.replaceAll("\\W", ""); + //$$ } + //$$ + //$$ final File file = new File(repo.dirServerResourcepacks, fileName); //#if MC>=10809 - repo.lock.lock(); + //$$ repo.lock.lock(); //#else //$$ repo.field_177321_h.lock(); //#endif - try { + //$$ try { //#if MC>=10809 - repo.clearResourcePack(); + //$$ repo.clearResourcePack(); //#else //$$ repo.func_148529_f(); //#endif - - if (file.exists() && hash.length() == 40) { - try { - String fileHash = Hashing.sha1().hashBytes(Files.toByteArray(file)).toString(); - if (fileHash.equals(hash)) { - recordResourcePack(file, requestId); - return setServerResourcePack(repo, file); - } - - logger.warn("File " + file + " had wrong hash (expected " + hash + ", found " + fileHash + "). Deleting it."); - FileUtils.deleteQuietly(file); - } catch (IOException ioexception) { - logger.warn("File " + file + " couldn\'t be hashed. Deleting it.", ioexception); - FileUtils.deleteQuietly(file); - } - } - - final GuiScreenWorking guiScreen = new GuiScreenWorking(); - final Minecraft mc = Minecraft.getMinecraft(); - - Futures.getUnchecked(mc.addScheduledTask(() -> mc.displayGuiScreen(guiScreen))); - + //$$ + //$$ if (file.exists() && hash.length() == 40) { + //$$ try { + //$$ String fileHash = Hashing.sha1().hashBytes(Files.toByteArray(file)).toString(); + //$$ if (fileHash.equals(hash)) { + //$$ recordResourcePack(file, requestId); + //$$ return setServerResourcePack(file); + //$$ } + //$$ + //$$ logger.warn("File " + file + " had wrong hash (expected " + hash + ", found " + fileHash + "). Deleting it."); + //$$ FileUtils.deleteQuietly(file); + //$$ } catch (IOException ioexception) { + //$$ logger.warn("File " + file + " couldn\'t be hashed. Deleting it.", ioexception); + //$$ FileUtils.deleteQuietly(file); + //$$ } + //$$ } + //$$ + //$$ final GuiScreenWorking guiScreen = new GuiScreenWorking(); + //$$ final Minecraft mc = Minecraft.getMinecraft(); + //$$ + //$$ Futures.getUnchecked(mc.addScheduledTask(() -> mc.displayGuiScreen(guiScreen))); + //$$ //#if MC>=10809 //#if MC>=11002 //#if MC>=11100 - Map sessionInfo = ResourcePackRepository.getDownloadHeaders(); + //$$ Map sessionInfo = ResourcePackRepository.getDownloadHeaders(); //#else //$$ Map sessionInfo = ResourcePackRepository.func_190115_a(); //#endif //#else //$$ Map sessionInfo = Minecraft.getSessionInfo(); //#endif - repo.downloadingPacks = HttpUtil.downloadResourcePack(file, url, sessionInfo, 50 * 1024 * 1024, guiScreen, mc.getProxy()); - Futures.addCallback(repo.downloadingPacks, new FutureCallback() { + //$$ repo.downloadingPacks = HttpUtil.downloadResourcePack(file, url, sessionInfo, 50 * 1024 * 1024, guiScreen, mc.getProxy()); + //$$ Futures.addCallback(repo.downloadingPacks, new FutureCallback() { //#else //$$ Map sessionInfo = Minecraft.getSessionInfo(); //$$ repo.field_177322_i = HttpUtil.func_180192_a(file, url, sessionInfo, 50 * 1024 * 1024, guiScreen, mc.getProxy()); //$$ Futures.addCallback(repo.field_177322_i, new FutureCallback() { //#endif - @Override - public void onSuccess(Object value) { - recordResourcePack(file, requestId); - setServerResourcePack(repo, file); - } - - @Override - public void onFailure(@Nonnull Throwable throwable) { - throwable.printStackTrace(); - } - }); + //$$ @Override + //$$ public void onSuccess(Object value) { + //$$ recordResourcePack(file, requestId); + //$$ setServerResourcePack(file); + //$$ } + //$$ + //$$ @Override + //$$ public void onFailure(@Nonnull Throwable throwable) { + //$$ throwable.printStackTrace(); + //$$ } + //$$ }); //#if MC>=10809 - return repo.downloadingPacks; + //$$ return repo.downloadingPacks; //#else //$$ return repo.field_177322_i; //#endif - } finally { + //$$ } finally { //#if MC>=10809 - repo.lock.unlock(); + //$$ repo.lock.unlock(); //#else //$$ repo.field_177321_h.unlock(); //#endif - } - } + //$$ } + //$$ } + //#endif //#else //$$ public synchronized S3FPacketCustomPayload handleResourcePack(S3FPacketCustomPayload packet) { //$$ final int requestId = nextRequestId++; diff --git a/src/main/java/com/replaymod/render/RenderSettings.java b/src/main/java/com/replaymod/render/RenderSettings.java index 60fff513..59e2b336 100644 --- a/src/main/java/com/replaymod/render/RenderSettings.java +++ b/src/main/java/com/replaymod/render/RenderSettings.java @@ -1,12 +1,29 @@ package com.replaymod.render; +import com.replaymod.core.versions.MCVer; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableColor; import lombok.AllArgsConstructor; import lombok.Data; import lombok.Getter; import net.minecraft.client.resources.I18n; -import org.lwjgl.util.ReadableColor; +import net.minecraft.util.Util; import java.io.File; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Optional; + +import static com.replaymod.render.ReplayModRender.LOGGER; + +//#if MC>=11300 +import org.apache.maven.artifact.versioning.ComparableVersion; +//#else +//#if MC>=10800 +//$$ import net.minecraftforge.fml.common.versioning.ComparableVersion; +//#else +//$$ import cpw.mods.fml.common.versioning.ComparableVersion; +//#endif +//#endif @Data public class RenderSettings { @@ -21,6 +38,14 @@ public class RenderSettings { public String getDescription() { return I18n.format("replaymod.gui.rendersettings.renderer." + name().toLowerCase() + ".description"); } + + public boolean isSpherical() { + return this == EQUIRECTANGULAR || this == ODS; + } + + public boolean hasFixedAspectRatio() { + return this == EQUIRECTANGULAR || this == ODS || this == CUBIC; + } } public enum EncodingPreset { @@ -49,7 +74,7 @@ public class RenderSettings { } public String getValue() { - return "-y -f rawvideo -pix_fmt rgb24 -s %WIDTH%x%HEIGHT% -r %FPS% -i - %FILTERS%" + preset; + return "-y -f rawvideo -pix_fmt bgra -s %WIDTH%x%HEIGHT% -r %FPS% -i - %FILTERS%" + preset; } public String getFileExtension() { @@ -94,7 +119,9 @@ public class RenderSettings { private final boolean stabilizePitch; private final boolean stabilizeRoll; private final ReadableColor chromaKeyingColor; - private final boolean inject360Metadata; + private final int sphericalFovX; + private final int sphericalFovY; + private final boolean injectSphericalMetadata; private final AntiAliasing antiAliasing; private final String exportCommand; @@ -131,12 +158,66 @@ public class RenderSettings { } public String getVideoFilters() { - if (antiAliasing == AntiAliasing.NONE) { - return ""; - } else { + StringBuilder filters = new StringBuilder(); + + if (antiAliasing != AntiAliasing.NONE) { double factor = 1.0 / antiAliasing.getFactor(); - return String.format("-vf scale=iw*%1$s:ih*%1$s ", factor); + filters.append(String.format("-filter:v scale=iw*%1$s:ih*%1$s ", factor)); } + + return filters.toString(); + } + + public String getExportCommandOrDefault() { + return exportCommand.isEmpty() ? findFFmpeg() : exportCommand; + } + + private static String findFFmpeg() { + switch (Util.getOSType()) { + case WINDOWS: + // Allow windows users to unpack the ffmpeg archive into a sub-folder of their .minecraft folder + File inDotMinecraft = new File(MCVer.mcDataDir(MCVer.getMinecraft()), "ffmpeg/bin/ffmpeg.exe"); + if (inDotMinecraft.exists()) { + LOGGER.debug("FFmpeg found in .minecraft/ffmpeg"); + return inDotMinecraft.getAbsolutePath(); + } + break; + case OSX: + // The PATH doesn't seem to be set as expected on OSX, therefore we check some common locations ourselves + for (String path : new String[]{"/usr/local/bin/ffmpeg", "/usr/bin/ffmpeg"}) { + File file = new File(path); + if (file.exists()) { + LOGGER.debug("Found FFmpeg at {}", path); + return path; + } else { + LOGGER.debug("FFmpeg not located at {}", path); + } + } + // Homebrew doesn't seem to reliably symlink its installed binaries either + File homebrewFolder = new File("/usr/local/Cellar/ffmpeg"); + String[] homebrewVersions = homebrewFolder.list(); + if (homebrewVersions != null) { + Optional latestOpt = Arrays.stream(homebrewVersions) + .map(ComparableVersion::new) // Convert file name to comparable version + .sorted(Comparator.reverseOrder()) // Sort for latest version + .map(ComparableVersion::toString) // Convert back to file name + .map(v -> new File(new File(homebrewFolder, v), "bin/ffmpeg")) // Convert to binary files + .filter(File::exists) // Filter invalid installations (missing executable) + .findFirst(); // Take first one + if (latestOpt.isPresent()) { + File latest = latestOpt.get(); + LOGGER.debug("Found {} versions of FFmpeg installed with homebrew, chose {}", + homebrewVersions.length, latest); + return latest.getAbsolutePath(); + } + } + break; + case LINUX: // Linux users are entrusted to have their PATH configured correctly (most package manager do this) + case SOLARIS: // Never heard of anyone running this mod on Solaris having any problems + case UNKNOWN: // Unknown OS, just try to use "ffmpeg" + } + LOGGER.debug("Using default FFmpeg executable"); + return "ffmpeg"; } } diff --git a/src/main/java/com/replaymod/render/ReplayModRender.java b/src/main/java/com/replaymod/render/ReplayModRender.java index 1f9cfcda..15cf3414 100644 --- a/src/main/java/com/replaymod/render/ReplayModRender.java +++ b/src/main/java/com/replaymod/render/ReplayModRender.java @@ -1,65 +1,57 @@ package com.replaymod.render; +import com.replaymod.core.Module; import com.replaymod.core.ReplayMod; import com.replaymod.render.utils.RenderJob; import com.replaymod.replay.events.ReplayCloseEvent; import net.minecraft.crash.CrashReport; -import net.minecraft.util.ReportedException; -import net.minecraftforge.common.config.Configuration; import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; //#if MC>=10800 import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif //#else //$$ import cpw.mods.fml.common.Mod; -//$$ import cpw.mods.fml.common.event.FMLPreInitializationEvent; //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; //#endif import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import static com.replaymod.core.versions.MCVer.*; -@Mod(modid = ReplayModRender.MOD_ID, - version = "@MOD_VERSION@", - acceptedMinecraftVersions = "@MC_VERSION@", - acceptableRemoteVersions = "*", - //#if MC>=10800 - clientSideOnly = true, - //#endif - useMetadata = true) -public class ReplayModRender { - public static final String MOD_ID = "replaymod-render"; - - @Mod.Instance(MOD_ID) +public class ReplayModRender implements Module { + { instance = this; } public static ReplayModRender instance; private ReplayMod core; - public static Logger LOGGER; + public static Logger LOGGER = LogManager.getLogger(); - private Configuration configuration; private final List renderQueue = new ArrayList<>(); + public ReplayModRender(ReplayMod core) { + this.core = core; + + core.getSettingsRegistry().register(Setting.class); + } + public ReplayMod getCore() { return core; } - @Mod.EventHandler - public void preInit(FMLPreInitializationEvent event) { - LOGGER = event.getModLog(); - core = ReplayMod.instance; - configuration = new Configuration(event.getSuggestedConfigurationFile()); - + @Override + public void initClient() { FML_BUS.register(this); - - core.getSettingsRegistry().register(Setting.class); } @SubscribeEvent @@ -69,17 +61,17 @@ public class ReplayModRender { public File getVideoFolder() { String path = core.getSettingsRegistry().get(Setting.RENDER_PATH); - File folder = new File(path.startsWith("./") ? core.getMinecraft().mcDataDir : null, path); + File folder = new File(path.startsWith("./") ? mcDataDir(core.getMinecraft()) : null, path); try { FileUtils.forceMkdir(folder); } catch (IOException e) { - throw new ReportedException(CrashReport.makeCrashReport(e, "Cannot create video folder.")); + throw newReportedException(CrashReport.makeCrashReport(e, "Cannot create video folder.")); } return folder; } - public Configuration getConfiguration() { - return configuration; + public Path getRenderSettingsPath() { + return mcDataDir(core.getMinecraft()).toPath().resolve("config/replaymod-rendersettings.json"); } public List getRenderQueue() { diff --git a/src/main/java/com/replaymod/render/VideoWriter.java b/src/main/java/com/replaymod/render/VideoWriter.java index d3502a3c..e90cedcb 100755 --- a/src/main/java/com/replaymod/render/VideoWriter.java +++ b/src/main/java/com/replaymod/render/VideoWriter.java @@ -1,19 +1,18 @@ package com.replaymod.render; +import com.replaymod.core.versions.MCVer; import com.replaymod.render.frame.RGBFrame; import com.replaymod.render.rendering.FrameConsumer; import com.replaymod.render.rendering.VideoRenderer; import com.replaymod.render.utils.ByteBufferPool; import com.replaymod.render.utils.StreamPipe; -import net.minecraft.client.Minecraft; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReportCategory; -import net.minecraft.util.Util; import org.apache.commons.exec.CommandLine; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.TeeOutputStream; -import org.lwjgl.util.ReadableDimension; import java.io.ByteArrayOutputStream; import java.io.File; @@ -22,17 +21,8 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Optional; import java.util.concurrent.TimeUnit; -//#if MC>=10800 -import net.minecraftforge.fml.common.versioning.ComparableVersion; -//#else -//$$ import cpw.mods.fml.common.versioning.ComparableVersion; -//#endif - import static com.replaymod.render.ReplayModRender.LOGGER; import static org.apache.commons.lang3.Validate.isTrue; @@ -64,7 +54,7 @@ public class VideoWriter implements FrameConsumer { .replace("%BITRATE%", String.valueOf(settings.getBitRate())) .replace("%FILTERS%", settings.getVideoFilters()); - String executable = settings.getExportCommand().isEmpty() ? findFFmpeg() : settings.getExportCommand(); + String executable = settings.getExportCommandOrDefault(); LOGGER.info("Starting {} with args: {}", executable, commandArgs); String[] cmdline; try { @@ -78,7 +68,7 @@ public class VideoWriter implements FrameConsumer { } catch (IOException e) { throw new NoFFmpegException(e); } - File exportLogFile = new File(Minecraft.getMinecraft().mcDataDir, "export.log"); + File exportLogFile = new File(MCVer.mcDataDir(MCVer.getMinecraft()), "export.log"); OutputStream exportLogOut = new TeeOutputStream(new FileOutputStream(exportLogFile), ffmpegLog); new StreamPipe(process.getInputStream(), exportLogOut).start(); new StreamPipe(process.getErrorStream(), exportLogOut).start(); @@ -86,54 +76,6 @@ public class VideoWriter implements FrameConsumer { channel = Channels.newChannel(outputStream); } - private String findFFmpeg() { - switch (Util.getOSType()) { - case WINDOWS: - // Allow windows users to unpack the ffmpeg archive into a sub-folder of their .minecraft folder - File inDotMinecraft = new File(Minecraft.getMinecraft().mcDataDir, "ffmpeg/bin/ffmpeg.exe"); - if (inDotMinecraft.exists()) { - LOGGER.debug("FFmpeg found in .minecraft/ffmpeg"); - return inDotMinecraft.getAbsolutePath(); - } - break; - case OSX: - // The PATH doesn't seem to be set as expected on OSX, therefore we check some common locations ourselves - for (String path : new String[]{"/usr/local/bin/ffmpeg", "/usr/bin/ffmpeg"}) { - File file = new File(path); - if (file.exists()) { - LOGGER.debug("Found FFmpeg at {}", path); - return path; - } else { - LOGGER.debug("FFmpeg not located at {}", path); - } - } - // Homebrew doesn't seem to reliably symlink its installed binaries either - File homebrewFolder = new File("/usr/local/Cellar/ffmpeg"); - String[] homebrewVersions = homebrewFolder.list(); - if (homebrewVersions != null) { - Optional latestOpt = Arrays.stream(homebrewVersions) - .map(ComparableVersion::new) // Convert file name to comparable version - .sorted(Comparator.reverseOrder()) // Sort for latest version - .map(ComparableVersion::toString) // Convert back to file name - .map(v -> new File(new File(homebrewFolder, v), "bin/ffmpeg")) // Convert to binary files - .filter(File::exists) // Filter invalid installations (missing executable) - .findFirst(); // Take first one - if (latestOpt.isPresent()) { - File latest = latestOpt.get(); - LOGGER.debug("Found {} versions of FFmpeg installed with homebrew, chose {}", - homebrewVersions.length, latest); - return latest.getAbsolutePath(); - } - } - break; - case LINUX: // Linux users are entrusted to have their PATH configured correctly (most package manager do this) - case SOLARIS: // Never heard of anyone running this mod on Solaris having any problems - case UNKNOWN: // Unknown OS, just try to use "ffmpeg" - } - LOGGER.debug("Using default FFmpeg executable"); - return "ffmpeg"; - } - @Override public void close() throws IOException { IOUtils.closeQuietly(outputStream); @@ -182,9 +124,9 @@ public class VideoWriter implements FrameConsumer { } CrashReport report = CrashReport.makeCrashReport(t, "Exporting frame"); CrashReportCategory exportDetails = report.makeCategory("Export details"); - exportDetails.addCrashSection("Export command", settings.getExportCommand()); - exportDetails.addCrashSection("Export args", commandArgs); - Minecraft.getMinecraft().crashed(report); + MCVer.addDetail(exportDetails, "Export command", settings::getExportCommand); + MCVer.addDetail(exportDetails, "Export args", commandArgs::toString); + MCVer.getMinecraft().crashed(report); } } diff --git a/src/main/java/com/replaymod/render/blend/exporters/ModelRendererExporter.java b/src/main/java/com/replaymod/render/blend/exporters/ModelRendererExporter.java index 601e8de3..a9d5efd8 100644 --- a/src/main/java/com/replaymod/render/blend/exporters/ModelRendererExporter.java +++ b/src/main/java/com/replaymod/render/blend/exporters/ModelRendererExporter.java @@ -5,10 +5,16 @@ import com.replaymod.render.blend.BlendMeshBuilder; import com.replaymod.render.blend.Exporter; import com.replaymod.render.blend.data.DMesh; import com.replaymod.render.blend.data.DObject; -import net.minecraft.client.model.ModelBox; -import net.minecraft.client.model.ModelRenderer; import org.lwjgl.opengl.GL11; +//#if MC>=11300 +import net.minecraft.client.renderer.entity.model.ModelBox; +import net.minecraft.client.renderer.entity.model.ModelRenderer; +//#else +//$$ import net.minecraft.client.model.ModelBox; +//$$ import net.minecraft.client.model.ModelRenderer; +//#endif + import java.io.IOException; import static com.replaymod.core.versions.MCVer.*; diff --git a/src/main/java/com/replaymod/render/capturer/MultiFramePboOpenGlFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/MultiFramePboOpenGlFrameCapturer.java index 55eccf01..2af60ffe 100644 --- a/src/main/java/com/replaymod/render/capturer/MultiFramePboOpenGlFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/MultiFramePboOpenGlFrameCapturer.java @@ -4,8 +4,8 @@ import com.replaymod.render.frame.OpenGlFrame; import com.replaymod.render.rendering.Frame; import com.replaymod.render.utils.ByteBufferPool; import com.replaymod.render.utils.PixelBufferObject; -import net.minecraft.client.renderer.OpenGlHelper; import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; import java.io.IOException; import java.nio.ByteBuffer; @@ -19,7 +19,7 @@ public abstract class MultiFramePboOpenGlFrameCapturer=10800 -import net.minecraft.client.renderer.GlStateManager; import static net.minecraft.client.renderer.GlStateManager.*; //#else //$$ import com.replaymod.render.hooks.GLStateTracker.BooleanState; @@ -36,9 +33,7 @@ public class ODSFrameCapturer implements FrameCapturer { private final Program.Uniform leftEyeVariable; private final BooleanState[] previousStates = new BooleanState[3]; - private final BooleanState previousFogState; - - private final Minecraft mc = Minecraft.getMinecraft(); + private BooleanState previousFogState; public ODSFrameCapturer(WorldRenderer worldRenderer, final RenderInfo renderInfo, int frameSize) { RenderInfo fakeInfo = new RenderInfo() { @@ -62,9 +57,9 @@ public class ODSFrameCapturer implements FrameCapturer { @Override public float updateForNextFrame() { if (call++ % 2 == 0) { - shaderProgram.stopUsing(); + unbindProgram(); partialTicks = renderInfo.updateForNextFrame(); - shaderProgram.use(); + bindProgram(); } return partialTicks; } @@ -78,30 +73,43 @@ public class ODSFrameCapturer implements FrameCapturer { right = new CubicStereoFrameCapturer(worldRenderer, fakeInfo, frameSize); try { shaderProgram = new Program(vertexResource, fragmentResource); - shaderProgram.use(); leftEyeVariable = shaderProgram.getUniformVariable("leftEye"); directionVariable = shaderProgram.getUniformVariable("direction"); - setTexture("texture", 0); - setTexture("lightMap", 1); - linkState(0, "textureEnabled"); - linkState(1, "lightMapEnabled"); - linkState(2, "hurtTextureEnabled"); - final Program.Uniform uniform = shaderProgram.getUniformVariable("fogEnabled"); - previousFogState = fog(); - uniform.set(previousFogState.currentState); - fog(new BooleanState(previousFogState.capability) { - @Override - public void setState(boolean state) { - super.setState(state); - uniform.set(state); - } - }); - shaderProgram.stopUsing(); } catch (Exception e) { - throw new ReportedException(CrashReport.makeCrashReport(e, "Creating ODS shaders")); + throw newReportedException(CrashReport.makeCrashReport(e, "Creating ODS shaders")); } } + private void bindProgram() { + shaderProgram.use(); + setTexture("texture", 0); + setTexture("lightMap", 1); + linkState(0, "textureEnabled"); + linkState(1, "lightMapEnabled"); + linkState(2, "hurtTextureEnabled"); + final Program.Uniform uniform = shaderProgram.getUniformVariable("fogEnabled"); + previousFogState = fog(); + uniform.set(previousFogState.currentState); + fog(new BooleanState(previousFogState.capability) { + { currentState = previousFogState.currentState; } + @Override + public void setState(boolean state) { + super.setState(state); + uniform.set(state); + } + }); + } + + private void unbindProgram() { + for (int i = 0; i < previousStates.length; i++) { + previousStates[i].currentState = texture2DState(i).currentState; + texture2DState(i, previousStates[i]); + } + previousFogState.currentState = fog().currentState; + fog(previousFogState); + shaderProgram.stopUsing(); + } + private void setTexture(String texture, int i) { shaderProgram.getUniformVariable(texture).set(i); } @@ -111,6 +119,7 @@ public class ODSFrameCapturer implements FrameCapturer { previousStates[id] = texture2DState(id); uniform.set(previousStates[id].currentState); texture2DState(id, new BooleanState(previousStates[id].capability) { + { currentState = previousStates[id].currentState; } @Override public void setState(boolean state) { super.setState(state); @@ -126,12 +135,12 @@ public class ODSFrameCapturer implements FrameCapturer { @Override public ODSOpenGlFrame process() { - shaderProgram.use(); + bindProgram(); leftEyeVariable.set(true); CubicOpenGlFrame leftFrame = left.process(); leftEyeVariable.set(false); CubicOpenGlFrame rightFrame = right.process(); - shaderProgram.stopUsing(); + unbindProgram(); if (leftFrame != null && rightFrame != null) { return new ODSOpenGlFrame(leftFrame, rightFrame); @@ -144,10 +153,6 @@ public class ODSFrameCapturer implements FrameCapturer { left.close(); right.close(); shaderProgram.delete(); - for (int i = 0; i < previousStates.length; i++) { - texture2DState(i, previousStates[i]); - } - fog(previousFogState); } private class CubicStereoFrameCapturer extends CubicPboOpenGlFrameCapturer { diff --git a/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java index 6654b43f..f716ac0c 100644 --- a/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/OpenGlFrameCapturer.java @@ -1,16 +1,17 @@ package com.replaymod.render.capturer; +import com.replaymod.core.versions.MCVer; import com.replaymod.render.frame.OpenGlFrame; import com.replaymod.render.rendering.Frame; import com.replaymod.render.rendering.FrameCapturer; import com.replaymod.render.utils.ByteBufferPool; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; +import de.johni0702.minecraft.gui.utils.lwjgl.WritableDimension; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.OpenGlHelper; import net.minecraft.client.shader.Framebuffer; import org.lwjgl.opengl.GL11; -import org.lwjgl.util.Dimension; -import org.lwjgl.util.ReadableDimension; -import org.lwjgl.util.WritableDimension; +import org.lwjgl.opengl.GL12; import java.io.IOException; import java.nio.ByteBuffer; @@ -30,7 +31,7 @@ public abstract class OpenGlFrameCapturer=11300 + Framebuffer fb = mc.getFramebuffer(); + if (fb.framebufferWidth != width || fb.framebufferHeight != height) { + fb.createFramebuffer(width, height); } - } - - private void setWindowSize(int width, int height) { - mc.resize(width, height); + mc.mainWindow.framebufferWidth = width; + mc.mainWindow.framebufferHeight = height; + //#else + //$$ if (width != mc.displayWidth || height != mc.displayHeight) { + //$$ mc.resize(width, height); + //$$ } + //#endif } @Override diff --git a/src/main/java/com/replaymod/render/capturer/RenderInfo.java b/src/main/java/com/replaymod/render/capturer/RenderInfo.java index 72f7596e..826214c3 100644 --- a/src/main/java/com/replaymod/render/capturer/RenderInfo.java +++ b/src/main/java/com/replaymod/render/capturer/RenderInfo.java @@ -1,7 +1,7 @@ package com.replaymod.render.capturer; import com.replaymod.render.RenderSettings; -import org.lwjgl.util.ReadableDimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; public interface RenderInfo { ReadableDimension getFrameSize(); diff --git a/src/main/java/com/replaymod/render/capturer/SimplePboOpenGlFrameCapturer.java b/src/main/java/com/replaymod/render/capturer/SimplePboOpenGlFrameCapturer.java index 13492562..52132e9f 100644 --- a/src/main/java/com/replaymod/render/capturer/SimplePboOpenGlFrameCapturer.java +++ b/src/main/java/com/replaymod/render/capturer/SimplePboOpenGlFrameCapturer.java @@ -3,9 +3,9 @@ package com.replaymod.render.capturer; import com.replaymod.render.frame.OpenGlFrame; import com.replaymod.render.utils.ByteBufferPool; import com.replaymod.render.utils.PixelBufferObject; -import net.minecraft.client.renderer.OpenGlHelper; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import org.lwjgl.opengl.GL11; -import org.lwjgl.util.ReadableDimension; +import org.lwjgl.opengl.GL12; import java.io.IOException; import java.nio.ByteBuffer; @@ -18,7 +18,7 @@ public class SimplePboOpenGlFrameCapturer extends OpenGlFrameCapturer=10800 -import net.minecraftforge.fml.common.eventhandler.Event; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.Event; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.Event; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.Event; //#endif diff --git a/src/main/java/com/replaymod/render/frame/OpenGlFrame.java b/src/main/java/com/replaymod/render/frame/OpenGlFrame.java index 1d595aab..82d40770 100644 --- a/src/main/java/com/replaymod/render/frame/OpenGlFrame.java +++ b/src/main/java/com/replaymod/render/frame/OpenGlFrame.java @@ -1,9 +1,9 @@ package com.replaymod.render.frame; import com.replaymod.render.rendering.Frame; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.lwjgl.util.ReadableDimension; import java.nio.ByteBuffer; diff --git a/src/main/java/com/replaymod/render/frame/RGBFrame.java b/src/main/java/com/replaymod/render/frame/RGBFrame.java index 8a1f5a8c..c0c0eb07 100644 --- a/src/main/java/com/replaymod/render/frame/RGBFrame.java +++ b/src/main/java/com/replaymod/render/frame/RGBFrame.java @@ -1,9 +1,9 @@ package com.replaymod.render.frame; import com.replaymod.render.rendering.Frame; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import lombok.Getter; import org.apache.commons.lang3.Validate; -import org.lwjgl.util.ReadableDimension; import java.nio.ByteBuffer; @@ -18,9 +18,9 @@ public class RGBFrame implements Frame { private final ByteBuffer byteBuffer; public RGBFrame(int frameId, ReadableDimension size, ByteBuffer byteBuffer) { - Validate.isTrue(size.getWidth() * size.getHeight() * 3 == byteBuffer.remaining(), + Validate.isTrue(size.getWidth() * size.getHeight() * 4 == byteBuffer.remaining(), "Buffer size is %d (cap: %d) but should be %d", - byteBuffer.remaining(), byteBuffer.capacity(), size.getWidth() * size.getHeight() * 3); + byteBuffer.remaining(), byteBuffer.capacity(), size.getWidth() * size.getHeight() * 4); this.frameId = frameId; this.size = size; this.byteBuffer = byteBuffer; diff --git a/src/main/java/com/replaymod/render/gui/GuiExportFailed.java b/src/main/java/com/replaymod/render/gui/GuiExportFailed.java index b86e7118..d9eb4e1c 100644 --- a/src/main/java/com/replaymod/render/gui/GuiExportFailed.java +++ b/src/main/java/com/replaymod/render/gui/GuiExportFailed.java @@ -13,11 +13,12 @@ import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReportCategory; -import net.minecraft.util.ReportedException; import java.util.Arrays; import java.util.function.Consumer; +import static com.replaymod.core.versions.MCVer.addDetail; +import static com.replaymod.core.versions.MCVer.newReportedException; import static com.replaymod.render.ReplayModRender.LOGGER; public class GuiExportFailed extends GuiScreen { @@ -31,9 +32,9 @@ public class GuiExportFailed extends GuiScreen { // If they haven't, then this is probably a faulty ffmpeg installation and there's nothing we can do CrashReport crashReport = CrashReport.makeCrashReport(e, "Exporting video"); CrashReportCategory details = crashReport.makeCategory("Export details"); - details.addCrashSection("Settings", settings); - details.addCrashSection("FFmpeg log", e.getLog()); - throw new ReportedException(crashReport); + addDetail(details, "Settings", settings::toString); + addDetail(details, "FFmpeg log", e::getLog); + throw newReportedException(crashReport); } else { // If they have, ask them whether it was intentional GuiExportFailed gui = new GuiExportFailed(e, doRestart); @@ -93,7 +94,9 @@ public class GuiExportFailed extends GuiScreen { oldSettings.isStabilizePitch(), oldSettings.isStabilizeRoll(), oldSettings.getChromaKeyingColor(), - oldSettings.isInject360Metadata(), + oldSettings.getSphericalFovX(), + oldSettings.getSphericalFovY(), + oldSettings.isInjectSphericalMetadata(), oldSettings.getAntiAliasing(), oldSettings.getExportCommand(), oldSettings.getEncodingPreset().getValue(), diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java index 7801479f..ef3cf4e3 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderQueue.java @@ -27,10 +27,10 @@ import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import net.minecraft.client.gui.GuiErrorScreen; import net.minecraft.crash.CrashReport; -import org.lwjgl.util.Dimension; -import org.lwjgl.util.ReadableDimension; import javax.annotation.Nullable; import java.util.List; diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java index 028f7198..2aadeedd 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderSettings.java @@ -1,5 +1,6 @@ package com.replaymod.render.gui; +import com.google.common.base.Preconditions; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.gson.Gson; @@ -27,18 +28,22 @@ import de.johni0702.minecraft.gui.popup.GuiFileChooserPopup; import de.johni0702.minecraft.gui.utils.Colors; import de.johni0702.minecraft.gui.utils.Consumer; import de.johni0702.minecraft.gui.utils.Utils; +import de.johni0702.minecraft.gui.utils.lwjgl.Color; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableColor; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import net.minecraft.client.gui.GuiErrorScreen; import net.minecraft.client.resources.I18n; import net.minecraft.crash.CrashReport; -import net.minecraftforge.common.config.Configuration; -import net.minecraftforge.common.config.Property; -import org.lwjgl.util.Color; -import org.lwjgl.util.Dimension; -import org.lwjgl.util.ReadableColor; -import org.lwjgl.util.ReadableDimension; import javax.annotation.Nullable; import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Map; @@ -128,7 +133,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { new GuiLabel().setI18nText("replaymod.gui.rendersettings.renderer"), renderMethodDropdown, new GuiLabel().setI18nText("replaymod.gui.rendersettings.presets"), encodingPresetDropdown, new GuiLabel().setI18nText("replaymod.gui.rendersettings.customresolution"), videoResolutionPanel, - new GuiLabel().setI18nText("replaymod.gui.settings.bitrate"), new GuiPanel().addElements(null, + new GuiLabel().setI18nText("replaymod.gui.rendersettings.bitrate"), new GuiPanel().addElements(null, new GuiPanel().addElements(null, bitRateField, bitRateUnit).setLayout(new HorizontalLayout()), frameRateSlider).setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(3)), new GuiLabel().setI18nText("replaymod.gui.rendersettings.outputfile"), outputFileButton) @@ -149,8 +154,22 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { .setI18nLabel("replaymod.gui.rendersettings.chromakey"); public final GuiColorPicker chromaKeyingColor = new GuiColorPicker().setSize(30, 15); - public final GuiCheckbox inject360Metadata = new GuiCheckbox() - .setI18nLabel("replaymod.gui.rendersettings.360metadata"); + public static final int MIN_SPHERICAL_FOV = 120; + public static final int MAX_SPHERICAL_FOV = 360; + public static final int SPHERICAL_FOV_STEP_SIZE = 5; + public final GuiSlider sphericalFovSlider = new GuiSlider() + .onValueChanged(new Runnable() { + @Override + public void run() { + sphericalFovSlider.setText(I18n.format("replaymod.gui.rendersettings.sphericalFov") + + ": " + (MIN_SPHERICAL_FOV + sphericalFovSlider.getValue() * SPHERICAL_FOV_STEP_SIZE) + "°"); + + updateInputs(); + } + }).setSize(200, 20).setSteps((MAX_SPHERICAL_FOV - MIN_SPHERICAL_FOV) / SPHERICAL_FOV_STEP_SIZE); + + public final GuiCheckbox injectSphericalMetadata = new GuiCheckbox() + .setI18nLabel("replaymod.gui.rendersettings.sphericalmetadata"); public final GuiDropdownMenu antiAliasingDropdown = new GuiDropdownMenu() .setSize(200, 20).setValues(RenderSettings.AntiAliasing.values()).setSelected(RenderSettings.AntiAliasing.NONE); @@ -161,8 +180,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { .addElements(new GridLayout.Data(0, 0.5), new GuiLabel().setI18nText("replaymod.gui.rendersettings.stabilizecamera"), stabilizePanel, chromaKeyingCheckbox, chromaKeyingColor, - inject360Metadata, - new GuiLabel(), // to show the anti-aliasing options in a new line + injectSphericalMetadata, sphericalFovSlider, new GuiLabel().setI18nText("replaymod.gui.rendersettings.antialiasing"), antiAliasingDropdown)); public final GuiTextField exportCommand = new GuiTextField().setI18nHint("replaymod.gui.rendersettings.command") @@ -280,7 +298,13 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { this.replayHandler = replayHandler; this.timeline = timeline; - String json = getConfigProperty(ReplayModRender.instance.getConfiguration()).getString(); + String json = "{}"; + try { + json = new String(Files.readAllBytes(getSettingsPath()), StandardCharsets.UTF_8); + } catch (NoSuchFileException | FileNotFoundException ignored) { + } catch (IOException e) { + LOGGER.error("Reading render settings:", e); + } RenderSettings settings = new GsonBuilder() .registerTypeAdapter(RenderSettings.class, (InstanceCreator) type -> getDefaultRenderSettings()) .registerTypeAdapter(ReadableColor.class, new Gson().getAdapter(Color.class)) @@ -289,8 +313,13 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { } protected void updateInputs() { + RenderSettings.RenderMethod renderMethod = renderMethodDropdown.getSelectedValue(); + + // Enable/Disable video with input + videoWidth.setEnabled(!renderMethod.hasFixedAspectRatio()); + // Validate video width and height - String error = isResolutionValid(); + String error = updateResolution(); if (error == null) { renderButton.setEnabled().setTooltip(null); videoWidth.setTextColor(Colors.WHITE); @@ -311,7 +340,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { } // Enable/Disable camera stabilization checkboxes - switch (renderMethodDropdown.getSelectedValue()) { + switch (renderMethod) { case CUBIC: case EQUIRECTANGULAR: case ODS: @@ -321,52 +350,94 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { stabilizePanel.forEach(IGuiCheckbox.class).setDisabled(); } + // Enable/Disable Spherical FOV slider + sphericalFovSlider.setEnabled(renderMethod.isSpherical()); + // Enable/Disable inject metadata checkbox if (encodingPresetDropdown.getSelectedValue().getFileExtension().equals("mp4") - && (renderMethodDropdown.getSelectedValue() == RenderSettings.RenderMethod.EQUIRECTANGULAR - || renderMethodDropdown.getSelectedValue() == RenderSettings.RenderMethod.ODS)) { - inject360Metadata.setEnabled().setTooltip(null); + && renderMethod.isSpherical()) { + injectSphericalMetadata.setEnabled().setTooltip(null); } else { - inject360Metadata.setDisabled().setTooltip(new GuiTooltip().setColor(Colors.RED) - .setI18nText("replaymod.gui.rendersettings.360metadata.error")); + injectSphericalMetadata.setDisabled().setTooltip(new GuiTooltip().setColor(Colors.RED) + .setI18nText("replaymod.gui.rendersettings.sphericalmetadata.error")); } } - protected String isResolutionValid() { + protected String updateResolution() { RenderSettings.EncodingPreset preset = encodingPresetDropdown.getSelectedValue(); RenderSettings.RenderMethod method = renderMethodDropdown.getSelectedValue(); - int videoWidth = this.videoWidth.getInteger(); + int videoHeight = this.videoHeight.getInteger(); + int videoWidth; + if (method.hasFixedAspectRatio()) { + // cubic rendering requires an aspect ratio of 4:3, + // therefore the height must be divisible by 3 + if (method == RenderSettings.RenderMethod.CUBIC && videoHeight % 3 != 0) { + return "replaymod.gui.rendersettings.customresolution.warning.cubic.height"; + } + + int sphericalFov = MIN_SPHERICAL_FOV + sphericalFovSlider.getValue() * SPHERICAL_FOV_STEP_SIZE; + videoWidth = videoWidthForHeight(method, videoHeight, sphericalFov, sphericalFov); + + this.videoWidth.setValue(videoWidth); + } else { + videoWidth = this.videoWidth.getInteger(); + } // Make sure the export arguments haven't been changed manually if (exportArguments.getText().equals(preset.getValue())) { // Yuv420 requires both dimensions to be even if (preset.isYuv420() && (videoWidth % 2 != 0 || videoHeight % 2 != 0)) { + + if (method == RenderSettings.RenderMethod.CUBIC) { + // cubic yuv rendering has the special case that the height must be + // divisible by both 3 and 2 - tell the user about it with a special message + return "replaymod.gui.rendersettings.customresolution.warning.yuv420.cubic"; + } + return "replaymod.gui.rendersettings.customresolution.warning.yuv420"; } } - if (method == RenderSettings.RenderMethod.CUBIC - && (videoWidth * 3 / 4 != videoHeight || videoWidth * 3 % 4 != 0)) { - return "replaymod.gui.rendersettings.customresolution.warning.cubic"; - } - if (method == RenderSettings.RenderMethod.EQUIRECTANGULAR - && (videoWidth / 2 != videoHeight || videoWidth % 2 != 0)) { - return "replaymod.gui.rendersettings.customresolution.warning.equirectangular"; - } - if (method == RenderSettings.RenderMethod.ODS - && videoWidth != videoHeight) { - return "replaymod.gui.rendersettings.customresolution.warning.ods"; - } //#if MC<10800 //$$ if (method == RenderSettings.RenderMethod.BLEND) { //$$ return "replaymod.gui.rendersettings.no_blend_on_1_7_10"; //$$ } //#endif + return null; } + protected int videoWidthForHeight(RenderSettings.RenderMethod method, int height, + int sphericalFovX, int sphericalFovY) { + if (method.isSpherical()) { + if (sphericalFovY < 180) { + // calculate the non-cropped height of the video + height = Math.round(height * 180 / (float) sphericalFovY); + } + + int width = height * 2; + + if (sphericalFovX < 360) { + // crop the resulting width + width = Math.round(width * (float) sphericalFovX / 360); + } + + if (method == RenderSettings.RenderMethod.ODS) { + width = Math.round(width / 2f); + } + + return width; + + } else if (method == RenderSettings.RenderMethod.CUBIC) { + Preconditions.checkArgument(height % 3 == 0); + return height / 3 * 4; + } + + throw new IllegalArgumentException(); + } + public void load(RenderSettings settings) { renderMethodDropdown.setSelected(settings.getRenderMethod()); encodingPresetDropdown.setSelected(settings.getEncodingPreset()); @@ -402,7 +473,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { chromaKeyingCheckbox.setChecked(true); chromaKeyingColor.setColor(settings.getChromaKeyingColor()); } - inject360Metadata.setChecked(settings.isInject360Metadata()); + sphericalFovSlider.setValue((settings.getSphericalFovX() - MIN_SPHERICAL_FOV) / SPHERICAL_FOV_STEP_SIZE); + injectSphericalMetadata.setChecked(settings.isInjectSphericalMetadata()); antiAliasingDropdown.setSelected(settings.getAntiAliasing()); exportCommand.setText(settings.getExportCommand()); exportArguments.setText(settings.getExportArguments()); @@ -411,6 +483,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { } public RenderSettings save(boolean serialize) { + int sphericalFov = MIN_SPHERICAL_FOV + sphericalFovSlider.getValue() * SPHERICAL_FOV_STEP_SIZE; + return new RenderSettings( renderMethodDropdown.getSelectedValue(), encodingPresetDropdown.getSelectedValue(), @@ -424,7 +498,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { stabilizePitch.isChecked() && (serialize || stabilizePitch.isEnabled()), stabilizeRoll.isChecked() && (serialize || stabilizeRoll.isEnabled()), chromaKeyingCheckbox.isChecked() ? chromaKeyingColor.getColor() : null, - inject360Metadata.isChecked() && (serialize || inject360Metadata.isEnabled()), + sphericalFov, Math.min(180, sphericalFov), + injectSphericalMetadata.isChecked() && (serialize || injectSphericalMetadata.isEnabled()), antiAliasingDropdown.getSelectedValue(), exportCommand.getText(), exportArguments.getText(), @@ -438,23 +513,24 @@ public class GuiRenderSettings extends GuiScreen implements Closeable { return new File(folder, fileName + "." + encodingPreset.getFileExtension()); } + protected Path getSettingsPath() { + return ReplayModRender.instance.getRenderSettingsPath(); + } + private RenderSettings getDefaultRenderSettings() { return new RenderSettings(RenderSettings.RenderMethod.DEFAULT, RenderSettings.EncodingPreset.MP4_DEFAULT, 1920, 1080, 60, 10 << 20, null, - true, false, false, false, null, false, RenderSettings.AntiAliasing.NONE, "", RenderSettings.EncodingPreset.MP4_DEFAULT.getValue(), false); + true, false, false, false, null, 360, 180, false, RenderSettings.AntiAliasing.NONE, "", RenderSettings.EncodingPreset.MP4_DEFAULT.getValue(), false); } @Override public void close() { RenderSettings settings = save(true); String json = new Gson().toJson(settings); - Configuration config = ReplayModRender.instance.getConfiguration(); - getConfigProperty(config).set(json); - config.save(); - } - - protected Property getConfigProperty(Configuration configuration) { - return configuration.get("rendersettings", "settings", "{}", - "Last state of the render settings GUI. Internal use only."); + try { + Files.write(getSettingsPath(), json.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + LOGGER.error("Saving render settings:", e); + } } public ReplayHandler getReplayHandler() { diff --git a/src/main/java/com/replaymod/render/gui/GuiRenderingDone.java b/src/main/java/com/replaymod/render/gui/GuiRenderingDone.java index 659f145e..6e534d4d 100644 --- a/src/main/java/com/replaymod/render/gui/GuiRenderingDone.java +++ b/src/main/java/com/replaymod/render/gui/GuiRenderingDone.java @@ -1,6 +1,7 @@ package com.replaymod.render.gui; import com.replaymod.core.SettingsRegistry; +import com.replaymod.core.versions.MCVer; import com.replaymod.render.RenderSettings; import com.replaymod.render.ReplayModRender; import com.replaymod.render.Setting; @@ -13,13 +14,8 @@ import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; import lombok.RequiredArgsConstructor; -import net.minecraft.util.Util; -import org.apache.logging.log4j.LogManager; -import org.lwjgl.Sys; -import java.awt.*; import java.io.File; -import java.io.IOException; @RequiredArgsConstructor public class GuiRenderingDone extends GuiScreen { @@ -34,30 +30,7 @@ public class GuiRenderingDone extends GuiScreen { public final GuiButton openFolder = new GuiButton().onClick(new Runnable() { @Override public void run() { - File folder = videoFile.getParentFile(); - String path = folder.getAbsolutePath(); - - // First try OS specific methods - try { - switch (Util.getOSType()) { - case WINDOWS: - Runtime.getRuntime().exec(String.format("cmd.exe /C start \"Open file\" \"%s\"", path)); - return; - case OSX: - Runtime.getRuntime().exec(new String[]{"/usr/bin/open", path}); - return; - } - } catch (IOException e) { - LogManager.getLogger().error("Cannot open file", e); - } - - // Otherwise try the java way - try { - Desktop.getDesktop().browse(folder.toURI()); - } catch (Throwable throwable) { - // And if all fails, lwjgl - Sys.openURL("file://" + path); - } + MCVer.openFile(videoFile.getParentFile()); } }).setSize(200, 20).setI18nLabel("replaymod.gui.openfolder"); diff --git a/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java b/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java index 0f60f3b4..0ecfe297 100644 --- a/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java +++ b/src/main/java/com/replaymod/render/gui/GuiVideoRenderer.java @@ -11,18 +11,23 @@ import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiCheckbox; import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar; +import de.johni0702.minecraft.gui.function.Tickable; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint; import net.minecraft.client.renderer.texture.DynamicTexture; import net.minecraft.client.resources.I18n; import net.minecraft.util.ResourceLocation; -import org.lwjgl.util.Dimension; -import org.lwjgl.util.ReadableDimension; -import org.lwjgl.util.ReadablePoint; + +//#if MC>=11300 +import net.minecraft.client.renderer.texture.NativeImage; +//#endif import java.nio.ByteBuffer; -public class GuiVideoRenderer extends GuiScreen { +public class GuiVideoRenderer extends GuiScreen implements Tickable { private static final ResourceLocation NO_PREVIEW_TEXTURE = new ResourceLocation("replaymod", "logo.jpg"); private final VideoRenderer renderer; @@ -135,7 +140,7 @@ public class GuiVideoRenderer extends GuiScreen { private int currentIndex = 0; @Override - public void draw(GuiRenderer guiRenderer, ReadableDimension size, RenderInfo renderInfo) { + public void tick() { long current = System.currentTimeMillis(); //first, update the total render time (only if rendering is not paused and has already started) @@ -206,8 +211,6 @@ public class GuiVideoRenderer extends GuiScreen { int framesDone = renderer.getFramesDone(), framesTotal = renderer.getTotalFrames(); progressBar.setI18nLabel("replaymod.gui.rendering.progress", framesDone, framesTotal); progressBar.setProgress((float) framesDone / framesTotal); - - super.draw(guiRenderer, size, renderInfo); } private String secToString(int seconds) { @@ -229,7 +232,11 @@ public class GuiVideoRenderer extends GuiScreen { final int videoHeight = videoSize.getHeight(); if (previewTexture == null) { - previewTexture = new DynamicTexture(videoWidth, videoHeight); + //#if MC>=11300 + previewTexture = new DynamicTexture(videoWidth, videoHeight, true); + //#else + //$$ previewTexture = new DynamicTexture(videoWidth, videoHeight); + //#endif } if (previewTextureDirty) { @@ -263,13 +270,27 @@ public class GuiVideoRenderer extends GuiScreen { ByteBuffer buffer = frame.getByteBuffer(); buffer.mark(); synchronized (this) { - int[] data = previewTexture.getTextureData(); - // Optifine changes the texture data array to be three times as long (for use by shaders), - // we only want to initialize the first third which is why we use the length of the buffer instead - // of the length of the data array - for (int i = 0; buffer.remaining() > 0; i++) { - data[i] = 0xff << 24 | (buffer.get() & 0xff) << 16 | (buffer.get() & 0xff) << 8 | (buffer.get() & 0xff); + //#if MC>=11300 + NativeImage data = previewTexture.getTextureData(); + assert data != null; + for (int y = 0; y < data.getHeight(); y++) { + for (int x = 0; x < data.getWidth(); x++) { + int r = buffer.get() & 0xff; + int g = buffer.get() & 0xff; + int b = buffer.get() & 0xff; + int value = 0xff << 24 | b << 16 | g << 8 | r; + data.setPixelRGBA(x, y, value); // actually takes ABGR, not RGBA + } } + //#else + //$$ int[] data = previewTexture.getTextureData(); + //$$ // Optifine changes the texture data array to be three times as long (for use by shaders), + //$$ // we only want to initialize the first third which is why we use the length of the buffer instead + //$$ // of the length of the data array + //$$ for (int i = 0; buffer.remaining() > 0; i++) { + //$$ data[i] = 0xff << 24 | (buffer.get() & 0xff) << 16 | (buffer.get() & 0xff) << 8 | (buffer.get() & 0xff); + //$$ } + //#endif previewTextureDirty = true; } buffer.reset(); diff --git a/src/main/java/com/replaymod/render/hooks/ChunkLoadingRenderGlobal.java b/src/main/java/com/replaymod/render/hooks/ChunkLoadingRenderGlobal.java index 8fe6c2b9..91996a9b 100644 --- a/src/main/java/com/replaymod/render/hooks/ChunkLoadingRenderGlobal.java +++ b/src/main/java/com/replaymod/render/hooks/ChunkLoadingRenderGlobal.java @@ -3,8 +3,6 @@ package com.replaymod.render.hooks; import com.replaymod.render.utils.JailingQueue; import net.minecraft.client.renderer.RegionRenderCacheBuilder; -import net.minecraft.client.renderer.RenderGlobal; -import net.minecraft.client.renderer.chunk.ChunkCompileTaskGenerator; import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher; import net.minecraft.client.renderer.chunk.ChunkRenderWorker; import net.minecraft.client.renderer.chunk.RenderChunk; @@ -12,6 +10,14 @@ import net.minecraft.client.renderer.chunk.RenderChunk; import java.lang.reflect.Field; import java.util.Iterator; +//#if MC>=11300 +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.chunk.ChunkRenderTask; +//#else +//$$ import net.minecraft.client.renderer.RenderGlobal; +//$$ import net.minecraft.client.renderer.chunk.ChunkCompileTaskGenerator; +//#endif + //#if MC>=10904 import java.util.concurrent.PriorityBlockingQueue; //#else @@ -22,14 +28,28 @@ import static com.replaymod.core.versions.MCVer.*; public class ChunkLoadingRenderGlobal { - private final RenderGlobal hooked; + //#if MC>=11300 + private final WorldRenderer hooked; + //#else + //$$ private final RenderGlobal hooked; + //#endif private ChunkRenderDispatcher renderDispatcher; - private JailingQueue workerJailingQueue; + //#if MC>=11300 + private JailingQueue workerJailingQueue; + //#else + //$$ private JailingQueue workerJailingQueue; + //#endif private CustomChunkRenderWorker renderWorker; private int frame; @SuppressWarnings("unchecked") - public ChunkLoadingRenderGlobal(RenderGlobal renderGlobal) { + public ChunkLoadingRenderGlobal( + //#if MC>=11300 + WorldRenderer renderGlobal + //#else + //$$ RenderGlobal renderGlobal + //#endif + ) { this.hooked = renderGlobal; setup(renderGlobal.renderDispatcher); @@ -51,14 +71,22 @@ public class ChunkLoadingRenderGlobal { int workerThreads = renderDispatcher.listThreadedWorkers.size(); //#if MC>=10904 - PriorityBlockingQueue queueChunkUpdates = renderDispatcher.queueChunkUpdates; + //#if MC>=11300 + PriorityBlockingQueue queueChunkUpdates = renderDispatcher.queueChunkUpdates; + //#else + //$$ PriorityBlockingQueue queueChunkUpdates = renderDispatcher.queueChunkUpdates; + //#endif //#else //$$ BlockingQueue queueChunkUpdates = renderDispatcher.queueChunkUpdates; //#endif workerJailingQueue = new JailingQueue<>(queueChunkUpdates); renderDispatcher.queueChunkUpdates = workerJailingQueue; //#if MC>=10904 - ChunkCompileTaskGenerator element = new ChunkCompileTaskGenerator(null, null, 0); + //#if MC>=11300 + ChunkRenderTask element = new ChunkRenderTask(null, null, 0); + //#else + //$$ ChunkCompileTaskGenerator element = new ChunkCompileTaskGenerator(null, null, 0); + //#endif //#else //$$ ChunkCompileTaskGenerator element = new ChunkCompileTaskGenerator(null, null); //#endif @@ -75,7 +103,11 @@ public class ChunkLoadingRenderGlobal { renderDispatcher.queueChunkUpdates = queueChunkUpdates; try { - Field hookField = RenderGlobal.class.getField("replayModRender_hook"); + //#if MC>=11300 + Field hookField = WorldRenderer.class.getField("replayModRender_hook"); + //#else + //$$ Field hookField = RenderGlobal.class.getField("replayModRender_hook"); + //#endif hookField.set(hooked, this); } catch (NoSuchFieldException | IllegalAccessException e) { throw new Error(e); @@ -119,7 +151,11 @@ public class ChunkLoadingRenderGlobal { workerJailingQueue.freeAll(); try { - Field hookField = RenderGlobal.class.getField("replayModRender_hook"); + //#if MC>=11300 + Field hookField = WorldRenderer.class.getField("replayModRender_hook"); + //#else + //$$ Field hookField = RenderGlobal.class.getField("replayModRender_hook"); + //#endif hookField.set(hooked, null); } catch (NoSuchFieldException | IllegalAccessException e) { throw new Error(e); @@ -139,7 +175,13 @@ public class ChunkLoadingRenderGlobal { } @Override - protected void processTask(ChunkCompileTaskGenerator p_178474_1_) throws InterruptedException { + protected void processTask( + //#if MC>=11300 + ChunkRenderTask p_178474_1_ + //#else + //$$ ChunkCompileTaskGenerator p_178474_1_ + //#endif + ) throws InterruptedException { super.processTask(p_178474_1_); } } diff --git a/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java b/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java index fa6d2ef3..fe6ef9fa 100644 --- a/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java +++ b/src/main/java/com/replaymod/render/hooks/EntityRendererHandler.java @@ -1,5 +1,6 @@ package com.replaymod.render.hooks; +import com.replaymod.core.versions.MCVer; import com.replaymod.render.RenderSettings; import com.replaymod.render.capturer.CaptureData; import com.replaymod.render.capturer.RenderInfo; @@ -8,8 +9,12 @@ import lombok.Getter; import net.minecraft.client.Minecraft; //#if MC>=10800 -import net.minecraft.client.renderer.GlStateManager; -import net.minecraftforge.fml.common.FMLCommonHandler; +//#if MC>=11300 +import net.minecraftforge.fml.hooks.BasicEventHooks; +//#else +//$$ import net.minecraft.client.renderer.GlStateManager; +//$$ import net.minecraftforge.fml.common.FMLCommonHandler; +//#endif //#else //$$ import com.replaymod.core.versions.MCVer.GlStateManager; //$$ import cpw.mods.fml.common.FMLCommonHandler; @@ -18,7 +23,7 @@ import net.minecraftforge.fml.common.FMLCommonHandler; import java.io.IOException; public class EntityRendererHandler implements WorldRenderer { - public final Minecraft mc = Minecraft.getMinecraft(); + public final Minecraft mc = MCVer.getMinecraft(); @Getter protected final RenderSettings settings; @@ -44,21 +49,33 @@ public class EntityRendererHandler implements WorldRenderer { } public void renderWorld(float partialTicks, long finishTimeNano) { - FMLCommonHandler.instance().onRenderTickStart(partialTicks); - - mc.entityRenderer.updateLightmap(partialTicks); - - GlStateManager.enableDepth(); - GlStateManager.enableAlpha(); - GlStateManager.alphaFunc(516, 0.5F); + //#if MC>=11300 + BasicEventHooks.onRenderTickStart(partialTicks); + //#else + //$$ FMLCommonHandler.instance().onRenderTickStart(partialTicks); + //#endif + //#if MC>=11300 + mc.entityRenderer.renderWorld(partialTicks, finishTimeNano); + //#else + //$$ mc.entityRenderer.updateLightmap(partialTicks); + //$$ + //$$ GlStateManager.enableDepth(); + //$$ GlStateManager.enableAlpha(); + //$$ GlStateManager.alphaFunc(516, 0.5F); + //$$ //#if MC>=10800 - mc.entityRenderer.renderWorldPass(2, partialTicks, finishTimeNano); + //$$ mc.entityRenderer.renderWorldPass(2, partialTicks, finishTimeNano); //#else //$$ mc.entityRenderer.renderWorld(partialTicks, finishTimeNano); //#endif + //#endif - FMLCommonHandler.instance().onRenderTickEnd(partialTicks); + //#if MC>=11300 + BasicEventHooks.onRenderTickEnd(partialTicks); + //#else + //$$ FMLCommonHandler.instance().onRenderTickEnd(partialTicks); + //#endif } @Override @@ -71,10 +88,6 @@ public class EntityRendererHandler implements WorldRenderer { this.omnidirectional = omnidirectional; } - public interface GluPerspective { - void replayModRender_gluPerspective(float fovY, float aspect, float zNear, float zFar); - } - public interface IEntityRenderer { void replayModRender_setHandler(EntityRendererHandler handler); EntityRendererHandler replayModRender_getHandler(); diff --git a/src/main/java/com/replaymod/render/metadata/MetadataInjector.java b/src/main/java/com/replaymod/render/metadata/MetadataInjector.java index 1b8d76ba..9042eeb0 100644 --- a/src/main/java/com/replaymod/render/metadata/MetadataInjector.java +++ b/src/main/java/com/replaymod/render/metadata/MetadataInjector.java @@ -4,6 +4,8 @@ import com.coremedia.iso.IsoFile; import com.coremedia.iso.boxes.*; import com.google.common.primitives.Bytes; import com.googlecode.mp4parser.BasicContainer; +import com.replaymod.render.RenderSettings; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; @@ -30,12 +32,20 @@ public class MetadataInjector { "true " + ""+STITCHING_SOFTWARE+" " + "equirectangular "; + private static final String SPHERICAL_CROP_XML = + "%d " + + "%d " + + "%d " + + "%d " + + "%d " + + "%d "; private static final String STEREO_XML_CONTENTS = "top-bottom"; private static final String SPHERICAL_XML_FOOTER = ""; - private static final String XML_360_METADATA = SPHERICAL_XML_HEADER + SPHERICAL_XML_CONTENTS + SPHERICAL_XML_FOOTER; - private static final String XML_ODS_METADATA = SPHERICAL_XML_HEADER + SPHERICAL_XML_CONTENTS - + STEREO_XML_CONTENTS + SPHERICAL_XML_FOOTER; + private static final String XML_MONO_METADATA = SPHERICAL_XML_HEADER + SPHERICAL_XML_CONTENTS + + SPHERICAL_CROP_XML + SPHERICAL_XML_FOOTER; + private static final String XML_STEREO_METADATA = SPHERICAL_XML_HEADER + SPHERICAL_XML_CONTENTS + + SPHERICAL_CROP_XML + STEREO_XML_CONTENTS + SPHERICAL_XML_FOOTER; /** * These bytes are taken from the variable 'spherical_uuid_id' @@ -48,18 +58,43 @@ public class MetadataInjector { (byte)0x02, (byte)0x52, (byte)0x1f, (byte)0xdd }; - private static final byte[] BYTES_360_METADATA = Bytes.concat(UUID_BYTES, XML_360_METADATA.getBytes()); - private static final byte[] BYTES_ODS_METADATA = Bytes.concat(UUID_BYTES, XML_ODS_METADATA.getBytes()); + public static void injectMetadata(RenderSettings.RenderMethod renderMethod, File videoFile, + int videoWidth, int videoHeight, + int sphericalFovX, int sphericalFovY) { + String xmlString; + switch (renderMethod) { + case EQUIRECTANGULAR: + xmlString = XML_MONO_METADATA; + break; + case ODS: + xmlString = XML_STEREO_METADATA; + break; + default: + throw new IllegalArgumentException("Invalid render method"); + } - public static void inject360Metadata(File videoFile) { - writeMetadata(videoFile, BYTES_360_METADATA); + Dimension original = getOriginalDimensions(videoWidth, videoHeight, sphericalFovX, sphericalFovY); + writeMetadata(videoFile, String.format(xmlString, + original.getWidth(), original.getHeight(), videoWidth, videoHeight, + (original.getWidth() - videoWidth) / 2, (original.getHeight() - videoHeight) / 2)); } - public static void injectODSMetadata(File videoFile) { - writeMetadata(videoFile, BYTES_ODS_METADATA); + private static Dimension getOriginalDimensions(int videoWidth, int videoHeight, + int sphericalFovX, int sphericalFovY) { + if (sphericalFovX < 360) { + videoWidth = Math.round(videoWidth * 360 / (float) sphericalFovX); + } + + if (sphericalFovY < 180) { + videoHeight = Math.round(videoHeight * 180 / (float) sphericalFovY); + } + + return new Dimension(videoWidth, videoHeight); } - private static void writeMetadata(File videoFile, byte[] metadata) { + private static void writeMetadata(File videoFile, String metadata) { + byte[] bytes = Bytes.concat(UUID_BYTES, metadata.getBytes()); + File tempFile = null; FileOutputStream videoFileOutputStream = null; IsoFile tempIsoFile = null; @@ -80,7 +115,7 @@ public class MetadataInjector { //create a new UserBox, which actually contains the Metadata bytes UserBox metadataBox = new UserBox(new byte[0]); - metadataBox.setData(metadata); + metadataBox.setData(bytes); //add the Metadata UserBox to the Movie Track trackBox.addBox(metadataBox); @@ -97,7 +132,7 @@ public class MetadataInjector { videoFileOutputStream = new FileOutputStream(videoFile); tempIsoFile.getBox(videoFileOutputStream.getChannel()); } catch(Exception e) { - LOGGER.error("360 Degree Metadata couldn't be injected", e); + LOGGER.error("Spherical Metadata couldn't be injected", e); } finally { IOUtils.closeQuietly(tempIsoFile); IOUtils.closeQuietly(videoFileOutputStream); diff --git a/src/main/java/com/replaymod/render/mixin/MixinEntityRenderer.java b/src/main/java/com/replaymod/render/mixin/MixinEntityRenderer.java index 919a88bc..65c04f07 100644 --- a/src/main/java/com/replaymod/render/mixin/MixinEntityRenderer.java +++ b/src/main/java/com/replaymod/render/mixin/MixinEntityRenderer.java @@ -1,20 +1,16 @@ package com.replaymod.render.mixin; +import com.replaymod.core.versions.MCVer; import com.replaymod.render.RenderSettings; import com.replaymod.render.capturer.CubicOpenGlFrameCapturer; import com.replaymod.render.capturer.StereoscopicOpenGlFrameCapturer; import com.replaymod.render.hooks.EntityRendererHandler; import com.replaymod.replay.camera.CameraEntity; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableColor; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.EntityRenderer; -import net.minecraft.client.renderer.RenderGlobal; -import net.minecraft.client.settings.GameSettings; import net.minecraft.entity.Entity; -import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import org.lwjgl.opengl.GL11; -import org.lwjgl.util.ReadableColor; -import org.lwjgl.util.glu.Project; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; @@ -22,6 +18,18 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +//#if MC>=11300 +import net.minecraft.client.GameSettings; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.client.renderer.Matrix4f; +import net.minecraft.client.renderer.WorldRenderer; +//#else +//$$ import net.minecraft.client.renderer.EntityRenderer; +//$$ import net.minecraft.client.renderer.RenderGlobal; +//$$ import net.minecraft.client.settings.GameSettings; +//$$ import org.lwjgl.util.glu.Project; +//#endif + //#if MC>=10904 import net.minecraft.util.math.RayTraceResult; //#else @@ -31,13 +39,18 @@ import net.minecraft.util.math.RayTraceResult; //#if MC>=10800 import net.minecraft.client.renderer.GlStateManager; //#else +//$$ import net.minecraft.entity.EntityLivingBase; //$$ import com.replaymod.core.versions.MCVer.GlStateManager; //#endif import static com.replaymod.core.versions.MCVer.*; -@Mixin(value = EntityRenderer.class) -public abstract class MixinEntityRenderer implements EntityRendererHandler.IEntityRenderer, EntityRendererHandler.GluPerspective { +//#if MC>=11300 +@Mixin(value = GameRenderer.class) +//#else +//$$ @Mixin(value = EntityRenderer.class) +//#endif +public abstract class MixinEntityRenderer implements EntityRendererHandler.IEntityRenderer { @Shadow public Minecraft mc; @@ -64,18 +77,21 @@ public abstract class MixinEntityRenderer implements EntityRendererHandler.IEnti //$$ } //#endif - @Inject(method = "setupFog", at = @At("HEAD"), cancellable = true) - private void replayModRender_onSetupFog(int fogDistanceFlag, float partialTicks, CallbackInfo ci) { - if (replayModRender_handler == null) return; - if (replayModRender_handler.getSettings().getChromaKeyingColor() != null) { - ci.cancel(); - } - } + // Moved to MixinFogRenderer in 1.13 + //#if MC<11300 + //$$ @Inject(method = "setupFog", at = @At("HEAD"), cancellable = true) + //$$ private void replayModRender_onSetupFog(int fogDistanceFlag, float partialTicks, CallbackInfo ci) { + //$$ if (replayModRender_handler == null) return; + //$$ if (replayModRender_handler.getSettings().getChromaKeyingColor() != null) { + //$$ ci.cancel(); + //$$ } + //$$ } + //#endif @Inject(method = "orientCamera", at = @At("HEAD")) private void replayModRender_resetRotationIfNeeded(float partialTicks, CallbackInfo ci) { if (replayModRender_handler != null) { - Entity entity = getRenderViewEntity(Minecraft.getMinecraft()); + Entity entity = getRenderViewEntity(MCVer.getMinecraft()); RenderSettings settings = replayModRender_handler.getSettings(); if (settings.isStabilizeYaw()) { entity.prevRotationYaw = entity.rotationYaw = 0; @@ -96,9 +112,15 @@ public abstract class MixinEntityRenderer implements EntityRendererHandler.IEnti private float orgRoll; @Inject(method = "setupCameraTransform", at = @At("HEAD")) - private void replayModRender_beforeSetupCameraTransform(float partialTicks, int renderPass, CallbackInfo ci) { + private void replayModRender_beforeSetupCameraTransform( + float partialTicks, + //#if MC<11300 + //$$ int renderPass, + //#endif + CallbackInfo ci + ) { if (replayModRender_handler != null) { - Entity entity = getRenderViewEntity(Minecraft.getMinecraft()); + Entity entity = getRenderViewEntity(MCVer.getMinecraft()); orgYaw = entity.rotationYaw; orgPitch = entity.rotationPitch; orgPrevYaw = entity.prevRotationYaw; @@ -108,9 +130,15 @@ public abstract class MixinEntityRenderer implements EntityRendererHandler.IEnti } @Inject(method = "setupCameraTransform", at = @At("RETURN")) - private void replayModRender_afterSetupCameraTransform(float partialTicks, int renderPass, CallbackInfo ci) { + private void replayModRender_afterSetupCameraTransform( + float partialTicks, + //#if MC<11300 + //$$ int renderPass, + //#endif + CallbackInfo ci + ) { if (replayModRender_handler != null) { - Entity entity = getRenderViewEntity(Minecraft.getMinecraft()); + Entity entity = getRenderViewEntity(MCVer.getMinecraft()); entity.rotationYaw = orgYaw; entity.rotationPitch = orgPitch; entity.prevRotationYaw = orgPrevYaw; @@ -122,29 +150,46 @@ public abstract class MixinEntityRenderer implements EntityRendererHandler.IEnti } @Inject(method = "renderHand", at = @At("HEAD"), cancellable = true) - private void replayModRender_renderSpectatorHand(float partialTicks, int renderPass, CallbackInfo ci) { + private void replayModRender_renderSpectatorHand( + float partialTicks, + //#if MC<11300 + //$$ int renderPass, + //#endif + CallbackInfo ci + ) { if (replayModRender_handler != null) { if (replayModRender_handler.omnidirectional) { + // No spectator hands during 360° view, we wouldn't even know where to put it ci.cancel(); - return; // No spectator hands during 360° view, we wouldn't even know where to put it - } - Entity currentEntity = getRenderViewEntity(Minecraft.getMinecraft()); - if (currentEntity instanceof EntityPlayer && !(currentEntity instanceof CameraEntity)) { - if (renderPass == 2) { // Need to update render pass - renderPass = replayModRender_handler.data == StereoscopicOpenGlFrameCapturer.Data.LEFT_EYE ? 1 : 0; - renderHand(partialTicks, renderPass); - ci.cancel(); - } // else, render normally + //#if MC>=11300 } + //#else + //$$ } else { + //$$ Entity currentEntity = getRenderViewEntity(MCVer.getMinecraft()); + //$$ if (currentEntity instanceof EntityPlayer && !(currentEntity instanceof CameraEntity)) { + //$$ if (renderPass == 2) { // Need to update render pass + //$$ renderPass = replayModRender_handler.data == StereoscopicOpenGlFrameCapturer.Data.LEFT_EYE ? 1 : 0; + //$$ renderHand(partialTicks, renderPass); + //$$ ci.cancel(); + //$$ } // else, render normally + //$$ } + //$$ } + //#endif } } - @Shadow - public abstract void renderHand(float partialTicks, int renderPass); + //#if MC<11300 + //$$ @Shadow + //$$ public abstract void renderHand(float partialTicks, int renderPass); + //#endif + //#if MC>=11300 + @Redirect(method = "updateCameraAndRender(FJ)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/WorldRenderer;drawSelectionBox(Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/util/math/RayTraceResult;IF)V")) + private void replayModRender_drawSelectionBox(WorldRenderer instance, EntityPlayer player, RayTraceResult rtr, int alwaysZero, float partialTicks) { + //#else //#if MC>=10904 - @Redirect(method = "renderWorldPass", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderGlobal;drawSelectionBox(Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/util/math/RayTraceResult;IF)V")) - private void replayModRender_drawSelectionBox(RenderGlobal instance, EntityPlayer player, RayTraceResult rtr, int alwaysZero, float partialTicks) { + //$$ @Redirect(method = "renderWorldPass", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderGlobal;drawSelectionBox(Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/util/math/RayTraceResult;IF)V")) + //$$ private void replayModRender_drawSelectionBox(RenderGlobal instance, EntityPlayer player, RayTraceResult rtr, int alwaysZero, float partialTicks) { //#else //#if MC>=10800 //$$ @Redirect(method = "renderWorldPass", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderGlobal;drawSelectionBox(Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/util/MovingObjectPosition;IF)V")) @@ -152,6 +197,7 @@ public abstract class MixinEntityRenderer implements EntityRendererHandler.IEnti //$$ @Redirect(method = "renderWorld", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderGlobal;drawSelectionBox(Lnet/minecraft/entity/player/EntityPlayer;Lnet/minecraft/util/MovingObjectPosition;IF)V")) //#endif //$$ private void replayModRender_drawSelectionBox(RenderGlobal instance, EntityPlayer player, MovingObjectPosition rtr, int alwaysZero, float partialTicks) { + //#endif //#endif if (replayModRender_handler == null) { instance.drawSelectionBox(player, rtr, alwaysZero, partialTicks); @@ -160,38 +206,51 @@ public abstract class MixinEntityRenderer implements EntityRendererHandler.IEnti private int orgRenderDistanceChunks; + //#if MC>=11300 + @Inject(method = "updateCameraAndRender(FJ)V", at = @At(value = "JUMP", ordinal = 0)) + //#else //#if MC>=10800 - @Inject(method = "renderWorldPass", at = @At(value = "JUMP", ordinal = 0)) + //$$ @Inject(method = "renderWorldPass", at = @At(value = "JUMP", ordinal = 0)) //#else //$$ @Inject(method = "renderWorld", at = @At(value = "INVOKE", ordinal = 0, shift = At.Shift.AFTER, //$$ target = "Lnet/minecraft/client/renderer/culling/ClippingHelperImpl;getInstance()Lnet/minecraft/client/renderer/culling/ClippingHelper;")) //#endif + //#endif private void replayModRender_beforeRenderSky(CallbackInfo ci) { if (replayModRender_handler != null && replayModRender_handler.getSettings().getChromaKeyingColor() != null) { - GameSettings settings = Minecraft.getMinecraft().gameSettings; + GameSettings settings = MCVer.getMinecraft().gameSettings; orgRenderDistanceChunks = settings.renderDistanceChunks; settings.renderDistanceChunks = 5; // Set render distance to 5 so we're always rendering sky when chroma keying } } + //#if MC>=11300 + @Redirect(method = "updateCameraAndRender(FJ)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/WorldRenderer;renderSky(F)V")) + private void replayModRender_renderSky(WorldRenderer instance, float partialTicks) { + //#else //#if MC>=10800 - @Redirect(method = "renderWorldPass", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderGlobal;renderSky(FI)V")) - private void replayModRender_renderSky(RenderGlobal instance, float partialTicks, int renderPass) { + //$$ @Redirect(method = "renderWorldPass", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderGlobal;renderSky(FI)V")) + //$$ private void replayModRender_renderSky(RenderGlobal instance, float partialTicks, int renderPass) { //#else //$$ @Redirect(method = "renderWorld", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/RenderGlobal;renderSky(F)V")) //$$ private void replayModRender_renderSky(RenderGlobal instance, float partialTicks) { + //#endif //#endif if (replayModRender_handler != null && replayModRender_handler.getSettings().getChromaKeyingColor() != null) { - Minecraft.getMinecraft().gameSettings.renderDistanceChunks = orgRenderDistanceChunks; + MCVer.getMinecraft().gameSettings.renderDistanceChunks = orgRenderDistanceChunks; ReadableColor color = replayModRender_handler.getSettings().getChromaKeyingColor(); GlStateManager.clearColor(color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f, 1); GlStateManager.clear(GL11.GL_COLOR_BUFFER_BIT); } else { + //#if MC>=11300 + instance.renderSky(partialTicks); + //#else //#if MC>=10800 - instance.renderSky(partialTicks, renderPass); + //$$ instance.renderSky(partialTicks, renderPass); //#else //$$ instance.renderSky(partialTicks); //#endif + //#endif } } @@ -204,12 +263,18 @@ public abstract class MixinEntityRenderer implements EntityRendererHandler.IEnti //#else //$$ @Inject(method = "setupCameraTransform", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glLoadIdentity()V", shift = At.Shift.AFTER, ordinal = 0, remap = false)) //#endif - private void replayModRender_setupStereoscopicProjection(float partialTicks, int renderPass, CallbackInfo ci) { + private void replayModRender_setupStereoscopicProjection( + float partialTicks, + //#if MC<11300 + //$$ int renderPass, + //#endif + CallbackInfo ci + ) { if (replayModRender_getHandler() != null) { if (replayModRender_getHandler().data == StereoscopicOpenGlFrameCapturer.Data.LEFT_EYE) { - GlStateManager.translate(0.07, 0, 0); + GL11.glTranslatef(0.07f, 0, 0); } else if (replayModRender_getHandler().data == StereoscopicOpenGlFrameCapturer.Data.RIGHT_EYE) { - GlStateManager.translate(-0.07, 0, 0); + GL11.glTranslatef(-0.07f, 0, 0); } } } @@ -219,12 +284,18 @@ public abstract class MixinEntityRenderer implements EntityRendererHandler.IEnti //#else //$$ @Inject(method = "setupCameraTransform", at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glLoadIdentity()V", shift = At.Shift.AFTER, ordinal = 1, remap = false)) //#endif - private void replayModRender_setupStereoscopicModelView(float partialTicks, int renderPass, CallbackInfo ci) { + private void replayModRender_setupStereoscopicModelView( + float partialTicks, + //#if MC<11300 + //$$ int renderPass, + //#endif + CallbackInfo ci + ) { if (replayModRender_getHandler() != null) { if (replayModRender_getHandler().data == StereoscopicOpenGlFrameCapturer.Data.LEFT_EYE) { - GlStateManager.translate(0.1, 0, 0); + GL11.glTranslatef(0.1f, 0, 0); } else if (replayModRender_getHandler().data == StereoscopicOpenGlFrameCapturer.Data.RIGHT_EYE) { - GlStateManager.translate(-0.1, 0, 0); + GL11.glTranslatef(-0.1f, 0, 0); } } } @@ -233,63 +304,91 @@ public abstract class MixinEntityRenderer implements EntityRendererHandler.IEnti * Cubic Renderer */ - @Redirect(method = "setupCameraTransform", at = @At(value = "INVOKE", target = "Lorg/lwjgl/util/glu/Project;gluPerspective(FFFF)V", remap = false)) - private void replayModRender_gluPerspective$0(float fovY, float aspect, float zNear, float zFar) { - replayModRender_gluPerspective(fovY, aspect, zNear, zFar); + //#if MC>=11300 + @Redirect(method = "setupCameraTransform", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Matrix4f;perspective(DFFF)Lnet/minecraft/client/renderer/Matrix4f;")) + private Matrix4f replayModRender_perspective$0(double fovY, float aspect, float zNear, float zFar) { + return replayModRender_perspective((float) fovY, aspect, zNear, zFar); } - //#if MC>=10800 - @Redirect(method = "renderWorldPass", at = @At(value = "INVOKE", target = "Lorg/lwjgl/util/glu/Project;gluPerspective(FFFF)V", remap = false)) - //#else - //$$ @Redirect(method = "renderHand", at = @At(value = "INVOKE", target = "Lorg/lwjgl/util/glu/Project;gluPerspective(FFFF)V", remap = false)) - //#endif - private void replayModRender_gluPerspective$1(float fovY, float aspect, float zNear, float zFar) { - replayModRender_gluPerspective(fovY, aspect, zNear, zFar); + @Redirect(method = "updateCameraAndRender(FJ)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Matrix4f;perspective(DFFF)Lnet/minecraft/client/renderer/Matrix4f;")) + private Matrix4f replayModRender_perspective$1(double fovY, float aspect, float zNear, float zFar) { + return replayModRender_perspective((float) fovY, aspect, zNear, zFar); } - //#if MC>=10800 - @Redirect(method = "renderCloudsCheck", at = @At(value = "INVOKE", target = "Lorg/lwjgl/util/glu/Project;gluPerspective(FFFF)V", remap = false)) - private void replayModRender_gluPerspective$2(float fovY, float aspect, float zNear, float zFar) { - replayModRender_gluPerspective(fovY, aspect, zNear, zFar); + @Redirect(method = "renderCloudsCheck", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Matrix4f;perspective(DFFF)Lnet/minecraft/client/renderer/Matrix4f;")) + private Matrix4f replayModRender_perspective$2(double fovY, float aspect, float zNear, float zFar) { + return replayModRender_perspective((float) fovY, aspect, zNear, zFar); } - //#endif - @Override - public void replayModRender_gluPerspective(float fovY, float aspect, float zNear, float zFar) { + private Matrix4f replayModRender_perspective(float fovY, float aspect, float zNear, float zFar) { if (replayModRender_getHandler() != null && replayModRender_getHandler().omnidirectional) { fovY = 90; aspect = 1; } - Project.gluPerspective(fovY, aspect, zNear, zFar); + return Matrix4f.perspective(fovY, aspect, zNear, zFar); } + //#else + //$$ @Redirect(method = "setupCameraTransform", at = @At(value = "INVOKE", target = "Lorg/lwjgl/util/glu/Project;gluPerspective(FFFF)V", remap = false)) + //$$ private void replayModRender_gluPerspective$0(float fovY, float aspect, float zNear, float zFar) { + //$$ replayModRender_gluPerspective(fovY, aspect, zNear, zFar); + //$$ } + //$$ + //#if MC>=10800 + //$$ @Redirect(method = "renderWorldPass", at = @At(value = "INVOKE", target = "Lorg/lwjgl/util/glu/Project;gluPerspective(FFFF)V", remap = false)) + //#else + //$$ @Redirect(method = "renderHand", at = @At(value = "INVOKE", target = "Lorg/lwjgl/util/glu/Project;gluPerspective(FFFF)V", remap = false)) + //#endif + //$$ private void replayModRender_gluPerspective$1(float fovY, float aspect, float zNear, float zFar) { + //$$ replayModRender_gluPerspective(fovY, aspect, zNear, zFar); + //$$ } + //$$ + //#if MC>=10800 + //$$ @Redirect(method = "renderCloudsCheck", at = @At(value = "INVOKE", target = "Lorg/lwjgl/util/glu/Project;gluPerspective(FFFF)V", remap = false)) + //$$ private void replayModRender_gluPerspective$2(float fovY, float aspect, float zNear, float zFar) { + //$$ replayModRender_gluPerspective(fovY, aspect, zNear, zFar); + //$$ } + //#endif + //$$ + //$$ private void replayModRender_gluPerspective(float fovY, float aspect, float zNear, float zFar) { + //$$ if (replayModRender_getHandler() != null && replayModRender_getHandler().omnidirectional) { + //$$ fovY = 90; + //$$ aspect = 1; + //$$ } + //$$ Project.gluPerspective(fovY, aspect, zNear, zFar); + //$$ } + //#endif @Inject(method = "orientCamera", at = @At("HEAD")) private void replayModRender_setupCubicFrameRotation(float partialTicks, CallbackInfo ci) { if (replayModRender_getHandler() != null && replayModRender_getHandler().data instanceof CubicOpenGlFrameCapturer.Data) { switch ((CubicOpenGlFrameCapturer.Data) replayModRender_getHandler().data) { case FRONT: - GlStateManager.rotate(0, 0.0F, 1.0F, 0.0F); + GL11.glRotatef(0, 0.0F, 1.0F, 0.0F); break; case RIGHT: - GlStateManager.rotate(90, 0.0F, 1.0F, 0.0F); + GL11.glRotatef(90, 0.0F, 1.0F, 0.0F); break; case BACK: - GlStateManager.rotate(180, 0.0F, 1.0F, 0.0F); + GL11.glRotatef(180, 0.0F, 1.0F, 0.0F); break; case LEFT: - GlStateManager.rotate(-90, 0.0F, 1.0F, 0.0F); + GL11.glRotatef(-90, 0.0F, 1.0F, 0.0F); break; case BOTTOM: - GlStateManager.rotate(90, 1.0F, 0.0F, 0.0F); + GL11.glRotatef(90, 1.0F, 0.0F, 0.0F); break; case TOP: - GlStateManager.rotate(-90, 1.0F, 0.0F, 0.0F); + GL11.glRotatef(-90, 1.0F, 0.0F, 0.0F); break; } } if (replayModRender_getHandler() != null && replayModRender_getHandler().omnidirectional) { // Minecraft goes back a little so we have to revert that - GlStateManager.translate(0.0F, 0.0F, 0.1F); + //#if MC>=11300 + GL11.glTranslatef(0.0F, 0.0F, -0.05F); + //#else + //$$ GL11.glTranslatef(0.0F, 0.0F, 0.1F); + //#endif } } } diff --git a/src/main/java/com/replaymod/render/mixin/MixinFogRenderer.java b/src/main/java/com/replaymod/render/mixin/MixinFogRenderer.java new file mode 100644 index 00000000..5f51e5a5 --- /dev/null +++ b/src/main/java/com/replaymod/render/mixin/MixinFogRenderer.java @@ -0,0 +1,24 @@ +//#if MC>=11300 +package com.replaymod.render.mixin; + +import com.replaymod.core.versions.MCVer; +import com.replaymod.render.hooks.EntityRendererHandler; +import net.minecraft.client.renderer.FogRenderer; +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(FogRenderer.class) +public abstract class MixinFogRenderer { + @Inject(method = "setupFog", at = @At("HEAD"), cancellable = true) + private void replayModRender_onSetupFog(int fogDistanceFlag, float partialTicks, CallbackInfo ci) { + EntityRendererHandler handler = + ((EntityRendererHandler.IEntityRenderer) MCVer.getMinecraft().entityRenderer).replayModRender_getHandler(); + if (handler == null) return; + if (handler.getSettings().getChromaKeyingColor() != null) { + ci.cancel(); + } + } +} +//#endif diff --git a/src/main/java/com/replaymod/render/mixin/MixinFrustum.java b/src/main/java/com/replaymod/render/mixin/MixinFrustum.java index 4a6facbe..aad8f545 100644 --- a/src/main/java/com/replaymod/render/mixin/MixinFrustum.java +++ b/src/main/java/com/replaymod/render/mixin/MixinFrustum.java @@ -1,7 +1,7 @@ package com.replaymod.render.mixin; +import com.replaymod.core.versions.MCVer; import com.replaymod.render.hooks.EntityRendererHandler; -import net.minecraft.client.Minecraft; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -21,7 +21,7 @@ import net.minecraft.client.renderer.culling.Frustum; public abstract class MixinFrustum { @Inject(method = "isBoxInFrustum", at = @At("HEAD"), cancellable = true) public void isBoxInFrustum(double minX, double minY, double minZ, double maxX, double maxY, double maxZ, CallbackInfoReturnable ci) { - EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) Minecraft.getMinecraft().entityRenderer).replayModRender_getHandler(); + EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) MCVer.getMinecraft().entityRenderer).replayModRender_getHandler(); if (handler != null && handler.omnidirectional && handler.data == null) { // Normally the camera is always facing the direction of the omnidirectional image face that is currently // getting rendered. With ODS however, the camera is always facing forwards and the turning happens in the diff --git a/src/main/java/com/replaymod/render/mixin/MixinParticleManager.java b/src/main/java/com/replaymod/render/mixin/MixinParticleManager.java index 2092239b..8ce3cf60 100644 --- a/src/main/java/com/replaymod/render/mixin/MixinParticleManager.java +++ b/src/main/java/com/replaymod/render/mixin/MixinParticleManager.java @@ -1,10 +1,10 @@ package com.replaymod.render.mixin; //#if MC>=10904 +import com.replaymod.core.versions.MCVer; import com.replaymod.render.blend.BlendState; import com.replaymod.render.blend.exporters.ParticlesExporter; import com.replaymod.render.hooks.EntityRendererHandler; -import net.minecraft.client.Minecraft; import net.minecraft.client.particle.Particle; import net.minecraft.client.particle.ParticleManager; import net.minecraft.entity.Entity; @@ -48,7 +48,7 @@ public abstract class MixinParticleManager { //$$ private void renderParticle(Particle particle, VertexBuffer vertexBuffer, Entity view, float partialTicks, //#endif float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) { - EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) Minecraft.getMinecraft().entityRenderer).replayModRender_getHandler(); + EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) MCVer.getMinecraft().entityRenderer).replayModRender_getHandler(); if (handler != null && handler.omnidirectional) { // Align all particles towards the camera double dx = particle.prevPosX + (particle.posX - particle.prevPosX) * partialTicks - view.posX; diff --git a/src/main/java/com/replaymod/render/mixin/MixinRender.java b/src/main/java/com/replaymod/render/mixin/MixinRender.java index 898f7140..5f1bfd71 100644 --- a/src/main/java/com/replaymod/render/mixin/MixinRender.java +++ b/src/main/java/com/replaymod/render/mixin/MixinRender.java @@ -1,7 +1,7 @@ package com.replaymod.render.mixin; +import com.replaymod.core.versions.MCVer; import com.replaymod.render.hooks.EntityRendererHandler; -import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.entity.Render; import net.minecraft.entity.Entity; import org.spongepowered.asm.mixin.Mixin; @@ -13,7 +13,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; public abstract class MixinRender { @Inject(method = "renderLivingLabel", at = @At("HEAD"), cancellable = true) private void replayModRender_areAllNamesHidden(Entity entityIn, String str, double x, double y, double z, int maxDistance, CallbackInfo ci) { - EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) Minecraft.getMinecraft().entityRenderer).replayModRender_getHandler(); + EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) MCVer.getMinecraft().entityRenderer).replayModRender_getHandler(); if (handler != null && !handler.getSettings().isRenderNameTags()) { ci.cancel(); } diff --git a/src/main/java/com/replaymod/render/mixin/MixinRenderGlobal.java b/src/main/java/com/replaymod/render/mixin/MixinRenderGlobal.java index c81175b0..86374b7e 100644 --- a/src/main/java/com/replaymod/render/mixin/MixinRenderGlobal.java +++ b/src/main/java/com/replaymod/render/mixin/MixinRenderGlobal.java @@ -2,7 +2,6 @@ package com.replaymod.render.mixin; import com.replaymod.render.hooks.ChunkLoadingRenderGlobal; -import net.minecraft.client.renderer.RenderGlobal; import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher; import net.minecraft.client.renderer.culling.ICamera; import net.minecraft.entity.Entity; @@ -10,16 +9,25 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +//#if MC>=11300 +import net.minecraft.client.renderer.WorldRenderer; +//#else +//$$ import net.minecraft.client.renderer.RenderGlobal; +//#endif + //#if MC<10904 //$$ import net.minecraft.client.renderer.chunk.RenderChunk; //$$ import net.minecraft.util.BlockPos; //$$ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; //#endif -@Mixin(RenderGlobal.class) +//#if MC>=11300 +@Mixin(WorldRenderer.class) +//#else +//$$ @Mixin(RenderGlobal.class) +//#endif public abstract class MixinRenderGlobal { public ChunkLoadingRenderGlobal replayModRender_hook; private boolean replayModRender_passThroughSetupTerrain; @@ -31,12 +39,31 @@ public abstract class MixinRenderGlobal { public ChunkRenderDispatcher renderDispatcher; @Shadow - public abstract void setupTerrain(Entity viewEntity, double partialTicks, ICamera camera, - int frameCount, boolean playerSpectator); + public abstract void setupTerrain( + Entity viewEntity, + //#if MC>=11300 + float partialTicks, + //#else + //$$ double partialTicks, + //#endif + ICamera camera, + int frameCount, + boolean playerSpectator + ); @Inject(method = "setupTerrain", at = @At("HEAD"), cancellable = true) - public void replayModRender_setupTerrain(Entity viewEntity, double partialTicks, ICamera camera, - int frameCount, boolean playerSpectator, CallbackInfo ci) { + private void replayModRender_setupTerrain( + Entity viewEntity, + //#if MC>=11300 + float partialTicks, + //#else + //$$ double partialTicks, + //#endif + ICamera camera, + int frameCount, + boolean playerSpectator, + CallbackInfo ci + ) { if (replayModRender_hook != null && !replayModRender_passThroughSetupTerrain) { replayModRender_passThroughSetupTerrain = true; diff --git a/src/main/java/com/replaymod/render/mixin/MixinRenderManager.java b/src/main/java/com/replaymod/render/mixin/MixinRenderManager.java index 7f9f1562..389c7d88 100644 --- a/src/main/java/com/replaymod/render/mixin/MixinRenderManager.java +++ b/src/main/java/com/replaymod/render/mixin/MixinRenderManager.java @@ -1,7 +1,7 @@ package com.replaymod.render.mixin; +import com.replaymod.core.versions.MCVer; import com.replaymod.render.hooks.EntityRendererHandler; -import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.entity.RenderManager; import net.minecraft.entity.Entity; import org.spongepowered.asm.mixin.Mixin; @@ -23,13 +23,17 @@ public abstract class MixinRenderManager { @Shadow private float playerViewY; - @Inject(method = "doRenderEntity", at = @At("HEAD")) + //#if MC>=11300 + @Inject(method = "renderEntity", at = @At("HEAD")) + //#else + //$$ @Inject(method = "doRenderEntity", at = @At("HEAD")) + //#endif //#if MC>=10904 private void replayModRender_reorientForCubicRendering(Entity entity, double dx, double dy, double dz, float iDoNotKnow, float partialTicks, boolean iDoNotCare, CallbackInfo ci) { //#else //$$ private void replayModRender_reorientForCubicRendering(Entity entity, double dx, double dy, double dz, float iDoNotKnow, float partialTicks, boolean iDoNotCare, CallbackInfoReturnable ci) { //#endif - EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) Minecraft.getMinecraft().entityRenderer).replayModRender_getHandler(); + EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) MCVer.getMinecraft().entityRenderer).replayModRender_getHandler(); if (handler != null && handler.omnidirectional) { double pitch = -Math.atan2(dy, Math.sqrt(dx * dx + dz * dz)); double yaw = -Math.atan2(dx, dz); diff --git a/src/main/java/com/replaymod/render/processor/CubicToRGBProcessor.java b/src/main/java/com/replaymod/render/processor/CubicToRGBProcessor.java index 223ea375..b014f77b 100644 --- a/src/main/java/com/replaymod/render/processor/CubicToRGBProcessor.java +++ b/src/main/java/com/replaymod/render/processor/CubicToRGBProcessor.java @@ -3,7 +3,7 @@ package com.replaymod.render.processor; import com.replaymod.render.frame.CubicOpenGlFrame; import com.replaymod.render.frame.RGBFrame; import com.replaymod.render.utils.ByteBufferPool; -import org.lwjgl.util.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; import java.nio.ByteBuffer; @@ -15,7 +15,7 @@ public class CubicToRGBProcessor extends AbstractFrameProcessor= 1) { @@ -74,9 +97,9 @@ public class EquirectangularToRGBProcessor extends AbstractFrameProcessor { private final EquirectangularToRGBProcessor processor; - public ODSToRGBProcessor(int frameSize) { - processor = new EquirectangularToRGBProcessor(frameSize); + public ODSToRGBProcessor(int outputWidth, int outputHeight, int sphericalFovX) { + processor = new EquirectangularToRGBProcessor(outputWidth, outputHeight / 2, sphericalFovX); } @Override @@ -21,7 +21,7 @@ public class ODSToRGBProcessor extends AbstractFrameProcessor=11300 +import org.lwjgl.glfw.GLFW; +//#else +//$$ import org.lwjgl.opengl.Display; +//#endif import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import static com.replaymod.core.versions.MCVer.newReportedException; + public class Pipeline implements Runnable { private final FrameCapturer capturer; @@ -48,9 +55,13 @@ public class Pipeline implements Runnable { } }, new ThreadPoolExecutor.DiscardPolicy()); - Minecraft mc = Minecraft.getMinecraft(); + Minecraft mc = MCVer.getMinecraft(); while (!capturer.isDone() && !Thread.currentThread().isInterrupted()) { - if (Display.isCloseRequested() || mc.hasCrashed) { + //#if MC>=11300 + if (GLFW.glfwWindowShouldClose(mc.mainWindow.getHandle()) || mc.hasCrashed) { + //#else + //$$ if (Display.isCloseRequested() || mc.hasCrashed) { + //#endif Thread.currentThread().interrupt(); return; } @@ -73,7 +84,7 @@ public class Pipeline implements Runnable { consumer.close(); } catch (Throwable t) { CrashReport crashReport = CrashReport.makeCrashReport(t, "Cleaning up rendering pipeline"); - throw new ReportedException(crashReport); + throw newReportedException(crashReport); } } @@ -105,7 +116,7 @@ public class Pipeline implements Runnable { } } catch (Throwable t) { CrashReport crashReport = CrashReport.makeCrashReport(t, "Processing frame"); - Minecraft.getMinecraft().crashed(crashReport); + MCVer.getMinecraft().crashed(crashReport); } } } diff --git a/src/main/java/com/replaymod/render/rendering/Pipelines.java b/src/main/java/com/replaymod/render/rendering/Pipelines.java index 012258e4..89b0585b 100644 --- a/src/main/java/com/replaymod/render/rendering/Pipelines.java +++ b/src/main/java/com/replaymod/render/rendering/Pipelines.java @@ -82,20 +82,28 @@ public class Pipelines { public static Pipeline newEquirectangularPipeline(RenderInfo renderInfo, FrameConsumer consumer) { RenderSettings settings = renderInfo.getRenderSettings(); + + EquirectangularToRGBProcessor processor = new EquirectangularToRGBProcessor(settings.getVideoWidth(), + settings.getVideoHeight(), settings.getSphericalFovX()); + FrameCapturer capturer; if (PixelBufferObject.SUPPORTED) { - capturer = new CubicPboOpenGlFrameCapturer(new EntityRendererHandler(settings, renderInfo), renderInfo, settings.getVideoWidth() / 4); + capturer = new CubicPboOpenGlFrameCapturer(new EntityRendererHandler(settings, renderInfo), renderInfo, processor.getFrameSize()); } else { - capturer = new CubicOpenGlFrameCapturer(new EntityRendererHandler(settings, renderInfo), renderInfo, settings.getVideoWidth() / 4); + capturer = new CubicOpenGlFrameCapturer(new EntityRendererHandler(settings, renderInfo), renderInfo, processor.getFrameSize()); } - return new Pipeline<>(capturer, new EquirectangularToRGBProcessor(settings.getVideoWidth() / 4), consumer); + return new Pipeline<>(capturer, processor, consumer); } public static Pipeline newODSPipeline(RenderInfo renderInfo, FrameConsumer consumer) { RenderSettings settings = renderInfo.getRenderSettings(); + + ODSToRGBProcessor processor = new ODSToRGBProcessor(settings.getVideoWidth(), + settings.getVideoHeight(), settings.getSphericalFovX()); + FrameCapturer capturer = - new ODSFrameCapturer(new EntityRendererHandler(settings, renderInfo), renderInfo, settings.getVideoWidth() / 4); - return new Pipeline<>(capturer, new ODSToRGBProcessor(settings.getVideoWidth() / 4), consumer); + new ODSFrameCapturer(new EntityRendererHandler(settings, renderInfo), renderInfo, processor.getFrameSize()); + return new Pipeline<>(capturer, processor, consumer); } public static Pipeline newBlendPipeline(RenderInfo renderInfo) { diff --git a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java index 39cfa4ee..b338f9c9 100644 --- a/src/main/java/com/replaymod/render/rendering/VideoRenderer.java +++ b/src/main/java/com/replaymod/render/rendering/VideoRenderer.java @@ -1,6 +1,7 @@ package com.replaymod.render.rendering; import com.replaymod.core.utils.WrappedTimer; +import com.replaymod.core.versions.MCVer; import com.replaymod.pathing.player.AbstractTimelinePlayer; import com.replaymod.pathing.player.ReplayTimer; import com.replaymod.pathing.properties.TimestampProperty; @@ -19,15 +20,20 @@ import com.replaymod.replay.ReplayHandler; import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.Timeline; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.ScaledResolution; import net.minecraft.client.renderer.OpenGlHelper; import net.minecraft.client.shader.Framebuffer; import net.minecraft.util.Timer; -import org.lwjgl.input.Mouse; -import org.lwjgl.opengl.Display; -import org.lwjgl.util.Dimension; -import org.lwjgl.util.ReadableDimension; + +//#if MC>=11300 +import org.lwjgl.glfw.GLFW; +//#else +//$$ import net.minecraft.client.gui.ScaledResolution; +//$$ import org.lwjgl.input.Mouse; +//$$ import org.lwjgl.opengl.Display; +//#endif //#if MC>=10904 import net.minecraft.util.SoundCategory; @@ -57,7 +63,7 @@ import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; public class VideoRenderer implements RenderInfo { - private final Minecraft mc = Minecraft.getMinecraft(); + private final Minecraft mc = MCVer.getMinecraft(); private final RenderSettings settings; private final ReplayHandler replayHandler; private final Timeline timeline; @@ -158,12 +164,14 @@ public class VideoRenderer implements RenderInfo { renderingPipeline.run(); - if (settings.isInject360Metadata()) { - if (settings.getRenderMethod() == RenderSettings.RenderMethod.ODS) { - MetadataInjector.injectODSMetadata(settings.getOutputFile()); - } else { - MetadataInjector.inject360Metadata(settings.getOutputFile()); - } + if (mc.hasCrashed) { + throw newReportedException(mc.crashReporter); + } + + if (settings.isInjectSphericalMetadata()) { + MetadataInjector.injectMetadata(settings.getRenderMethod(), settings.getOutputFile(), + settings.getTargetVideoWidth(), settings.getTargetVideoHeight(), + settings.getSphericalFovX(), settings.getSphericalFovY()); } finish(); @@ -180,18 +188,28 @@ public class VideoRenderer implements RenderInfo { @Override public float updateForNextFrame() { // because the jGui lib uses Minecraft's displayWidth and displayHeight values, update these temporarily - int displayWidthBefore = mc.displayWidth; - int displayHeightBefore = mc.displayHeight; - - mc.displayWidth = displayWidth; - mc.displayHeight = displayHeight; + //#if MC>=11300 + int displayWidthBefore = mc.mainWindow.framebufferWidth; + int displayHeightBefore = mc.mainWindow.framebufferHeight; + mc.mainWindow.framebufferWidth = displayWidth; + mc.mainWindow.framebufferHeight = displayHeight; + //#else + //$$ int displayWidthBefore = mc.displayWidth; + //$$ int displayHeightBefore = mc.displayHeight; + //$$ mc.displayWidth = displayWidth; + //$$ mc.displayHeight = displayHeight; + //#endif if (!settings.isHighPerformance() || framesDone % fps == 0) { drawGui(); } // Updating the timer will cause the timeline player to update the game state - mc.timer.updateTimer(); + mc.timer.updateTimer( + //#if MC>=11300 + MCVer.milliTime() + //#endif + ); int elapsedTicks = mc.timer.elapsedTicks; while (elapsedTicks-- > 0) { @@ -199,8 +217,13 @@ public class VideoRenderer implements RenderInfo { } // change Minecraft's display size back - mc.displayWidth = displayWidthBefore; - mc.displayHeight = displayHeightBefore; + //#if MC>=11300 + mc.mainWindow.framebufferWidth = displayWidthBefore; + mc.mainWindow.framebufferHeight = displayHeightBefore; + //#else + //$$ mc.displayWidth = displayWidthBefore; + //$$ mc.displayHeight = displayHeightBefore; + //#endif framesDone++; return mc.timer.renderPartialTicks; @@ -216,31 +239,52 @@ public class VideoRenderer implements RenderInfo { timelinePlayerFuture = timelinePlayer.start(timeline); if (!OpenGlHelper.isFramebufferEnabled()) { - Display.setResizable(false); + //#if MC>=11300 + GLFW.glfwSetWindowAttrib(mc.mainWindow.getHandle(), GLFW.GLFW_RESIZABLE, 1); + //#else + //$$ Display.setResizable(false); + //#endif } if (mc.gameSettings.showDebugInfo) { debugInfoWasShown = true; mc.gameSettings.showDebugInfo = false; } - if (Mouse.isGrabbed()) { + //#if MC>=11300 + if (mc.mouseHelper.isMouseGrabbed()) { mouseWasGrabbed = true; } - Mouse.setGrabbed(false); + mc.mouseHelper.ungrabMouse(); + //#else + //$$ if (Mouse.isGrabbed()) { + //$$ mouseWasGrabbed = true; + //$$ } + //$$ Mouse.setGrabbed(false); + //#endif // Mute all sounds except GUI sounds (buttons, etc.) - Map mutedSounds = new EnumMap<>(SoundCategory.class); + //#if MC>=11300 + originalSoundLevels = new EnumMap<>(SoundCategory.class); for (SoundCategory category : SoundCategory.values()) { - mutedSounds.put(category, 0f); + if (category != SoundCategory.MASTER) { + originalSoundLevels.put(category, mc.gameSettings.getSoundLevel(category)); + mc.gameSettings.setSoundLevel(category, 0); + } } + //#else + //$$ Map mutedSounds = new EnumMap<>(SoundCategory.class); + //$$ for (SoundCategory category : SoundCategory.values()) { + //$$ mutedSounds.put(category, 0f); + //$$ } //#if MC>=10904 - originalSoundLevels = mc.gameSettings.soundLevels; - mutedSounds.put(SoundCategory.MASTER, originalSoundLevels.get(SoundCategory.MASTER)); - mc.gameSettings.soundLevels = mutedSounds; + //$$ originalSoundLevels = mc.gameSettings.soundLevels; + //$$ mutedSounds.put(SoundCategory.MASTER, originalSoundLevels.get(SoundCategory.MASTER)); + //$$ mc.gameSettings.soundLevels = mutedSounds; //#else //$$ originalSoundLevels = mc.gameSettings.mapSoundLevels; //$$ mutedSounds.put(SoundCategory.MASTER, (Float) originalSoundLevels.get(SoundCategory.MASTER)); //$$ mc.gameSettings.mapSoundLevels = mutedSounds; //#endif + //#endif fps = settings.getFramesPerSecond(); @@ -282,14 +326,28 @@ public class VideoRenderer implements RenderInfo { timelinePlayer.onTick(new ReplayTimer.UpdatedEvent()); if (!OpenGlHelper.isFramebufferEnabled()) { - Display.setResizable(true); + //#if MC>=11300 + GLFW.glfwSetWindowAttrib(mc.mainWindow.getHandle(), GLFW.GLFW_RESIZABLE, 0); + //#else + //$$ Display.setResizable(true); + //#endif } mc.gameSettings.showDebugInfo = debugInfoWasShown; if (mouseWasGrabbed) { - mc.mouseHelper.grabMouseCursor(); + //#if MC>=11300 + mc.mouseHelper.grabMouse(); + //#else + //$$ mc.mouseHelper.grabMouseCursor(); + //#endif } //#if MC>=10904 - mc.gameSettings.soundLevels = originalSoundLevels; + //#if MC>=11300 + for (Map.Entry entry : originalSoundLevels.entrySet()) { + mc.gameSettings.setSoundLevel(entry.getKey(), entry.getValue()); + } + //#else + //$$ mc.gameSettings.soundLevels = originalSoundLevels; + //#endif //#else //$$ mc.gameSettings.mapSoundLevels = originalSoundLevels; //#endif @@ -311,7 +369,13 @@ public class VideoRenderer implements RenderInfo { } // Finally, resize the Minecraft framebuffer to the actual width/height of the window - mc.resize(displayWidth, displayHeight); + //#if MC>=11300 + mc.getFramebuffer().createFramebuffer(displayWidth, displayHeight); + mc.mainWindow.framebufferWidth = displayWidth; + mc.mainWindow.framebufferHeight = displayHeight; + //#else + //$$ mc.resize(displayWidth, displayHeight); + //#endif } private void tick() { @@ -322,14 +386,14 @@ public class VideoRenderer implements RenderInfo { } mc.currentScreen = gui.toMinecraft(); - //#if MC>=10800 - try { - mc.runTick(); - } catch (IOException e) { - throw new RuntimeException(e); - } + //#if MC>=10800 && MC<11300 + //$$ try { + //$$ mc.runTick(); + //$$ } catch (IOException e) { + //$$ throw new RuntimeException(e); + //$$ } //#else - //$$ mc.runTick(); + mc.runTick(); //#endif } @@ -338,7 +402,11 @@ public class VideoRenderer implements RenderInfo { // Resize the GUI framebuffer if the display size changed if (!settings.isHighPerformance() && displaySizeChanged()) { updateDisplaySize(); - guiFramebuffer.createBindFramebuffer(mc.displayWidth, mc.displayHeight); + //#if MC>=11300 + guiFramebuffer.createBindFramebuffer(mc.mainWindow.getFramebufferWidth(), mc.mainWindow.getFramebufferHeight()); + //#else + //$$ guiFramebuffer.createBindFramebuffer(mc.displayWidth, mc.displayHeight); + //#endif } pushMatrix(); @@ -346,27 +414,47 @@ public class VideoRenderer implements RenderInfo { enableTexture2D(); guiFramebuffer.bindFramebuffer(true); - mc.entityRenderer.setupOverlayRendering(); + //#if MC>=11300 + mc.mainWindow.setupOverlayRendering(); + //#else + //$$ mc.entityRenderer.setupOverlayRendering(); + //#endif - ScaledResolution scaled = newScaledResolution(mc); - gui.toMinecraft().setWorldAndResolution(mc, scaled.getScaledWidth(), scaled.getScaledHeight()); + //#if MC>=11300 + gui.toMinecraft().setWorldAndResolution(mc, mc.mainWindow.getScaledWidth(), mc.mainWindow.getScaledHeight()); + //#else + //$$ ScaledResolution scaled = newScaledResolution(mc); + //$$ gui.toMinecraft().setWorldAndResolution(mc, scaled.getScaledWidth(), scaled.getScaledHeight()); + //#endif + // Events are polled on 1.13+ in mainWindow.update which is called later + //#if MC<11300 //#if MC>=10800 - try { - gui.toMinecraft().handleInput(); - } catch (IOException e) { - // That's a strange exception from this kind of method O_o - // It isn't actually thrown here, so we'll deal with it the easy way - throw new RuntimeException(e); - } + //$$ try { + //$$ gui.toMinecraft().handleInput(); + //$$ } catch (IOException e) { + //$$ // That's a strange exception from this kind of method O_o + //$$ // It isn't actually thrown here, so we'll deal with it the easy way + //$$ throw new RuntimeException(e); + //$$ } //#else //$$ gui.toMinecraft().handleInput(); //#endif + //#endif - int mouseX = Mouse.getX() * scaled.getScaledWidth() / mc.displayWidth; - int mouseY = scaled.getScaledHeight() - Mouse.getY() * scaled.getScaledHeight() / mc.displayHeight - 1; + //#if MC>=11300 + int mouseX = (int) mc.mouseHelper.getMouseX(); + int mouseY = (int) mc.mouseHelper.getMouseY(); - gui.toMinecraft().drawScreen(mouseX, mouseY, 0); + gui.toMinecraft().tick(); + gui.toMinecraft().render(mouseX, mouseY, 0); + //#else + //$$ int mouseX = Mouse.getX() * scaled.getScaledWidth() / mc.displayWidth; + //$$ int mouseY = scaled.getScaledHeight() - Mouse.getY() * scaled.getScaledHeight() / mc.displayHeight - 1; + //$$ + //$$ gui.toMinecraft().updateScreen(); + //$$ gui.toMinecraft().drawScreen(mouseX, mouseY, 0); + //#endif guiFramebuffer.unbindFramebuffer(); popMatrix(); @@ -374,20 +462,30 @@ public class VideoRenderer implements RenderInfo { guiFramebuffer.framebufferRender(displayWidth, displayHeight); popMatrix(); - // if not in high performance mode, update the gui size if screen size changed - // otherwise just swap the progress gui to screen - if (settings.isHighPerformance()) { - Display.update(); - } else { + //#if MC>=11300 + mc.mainWindow.update(false); + //#else + //$$ // if not in high performance mode, update the gui size if screen size changed + //$$ // otherwise just swap the progress gui to screen + //$$ if (settings.isHighPerformance()) { + //$$ Display.update(); + //$$ } else { //#if MC>=10800 - mc.updateDisplay(); + //$$ mc.updateDisplay(); //#else //$$ mc.resetSize(); //#endif + //$$ } + //#endif + //#if MC>=11300 + if (mc.mouseHelper.isMouseGrabbed()) { + mc.mouseHelper.ungrabMouse(); } - if (Mouse.isGrabbed()) { - Mouse.setGrabbed(false); - } + //#else + //$$ if (Mouse.isGrabbed()) { + //$$ Mouse.setGrabbed(false); + //$$ } + //#endif if (paused) { try { Thread.sleep(50); @@ -400,12 +498,21 @@ public class VideoRenderer implements RenderInfo { } private boolean displaySizeChanged() { - return displayWidth != Display.getWidth() || displayHeight != Display.getHeight(); + //#if MC>=11300 + return displayWidth != mc.mainWindow.getWidth() || displayHeight != mc.mainWindow.getHeight(); + //#else + //$$ return displayWidth != Display.getWidth() || displayHeight != Display.getHeight(); + //#endif } private void updateDisplaySize() { - displayWidth = Display.getWidth(); - displayHeight = Display.getHeight(); + //#if MC>=11300 + displayWidth = mc.mainWindow.getWidth(); + displayHeight = mc.mainWindow.getHeight(); + //#else + //$$ displayWidth = Display.getWidth(); + //$$ displayHeight = Display.getHeight(); + //#endif } public int getFramesDone() { diff --git a/src/main/java/com/replaymod/render/shader/Program.java b/src/main/java/com/replaymod/render/shader/Program.java index b4d2c8e8..3481c5f9 100644 --- a/src/main/java/com/replaymod/render/shader/Program.java +++ b/src/main/java/com/replaymod/render/shader/Program.java @@ -1,7 +1,6 @@ package com.replaymod.render.shader; -import net.minecraft.client.Minecraft; -import net.minecraft.client.resources.IResource; +import com.replaymod.core.versions.MCVer; import net.minecraft.util.ResourceLocation; import org.apache.commons.io.IOUtils; import org.lwjgl.opengl.ARBFragmentShader; @@ -9,6 +8,12 @@ import org.lwjgl.opengl.ARBShaderObjects; import org.lwjgl.opengl.ARBVertexShader; import org.lwjgl.opengl.GL11; +//#if MC>=11300 +import net.minecraft.resources.IResource; +//#else +//$$ import net.minecraft.client.resources.IResource; +//#endif + import java.io.InputStream; import static org.lwjgl.opengl.ARBShaderObjects.*; @@ -47,7 +52,7 @@ public class Program { if(shader == 0) throw new Exception("glCreateShaderObjectARB failed"); - IResource resource = Minecraft.getMinecraft().getResourceManager().getResource(resourceLocation); + IResource resource = MCVer.getMinecraft().getResourceManager().getResource(resourceLocation); try (InputStream is = resource.getInputStream()) { glShaderSourceARB(shader, IOUtils.toString(is)); } diff --git a/src/main/java/com/replaymod/render/utils/PixelBufferObject.java b/src/main/java/com/replaymod/render/utils/PixelBufferObject.java index 87794da9..7d400846 100644 --- a/src/main/java/com/replaymod/render/utils/PixelBufferObject.java +++ b/src/main/java/com/replaymod/render/utils/PixelBufferObject.java @@ -1,12 +1,18 @@ package com.replaymod.render.utils; import com.google.common.base.Objects; +import com.replaymod.core.versions.MCVer; import lombok.RequiredArgsConstructor; -import net.minecraft.client.Minecraft; import org.apache.logging.log4j.LogManager; -import org.lwjgl.opengl.ARBBufferObject; -import org.lwjgl.opengl.GLContext; -import org.lwjgl.opengl.Util; + +//#if MC>=11300 +import org.lwjgl.opengl.ARBVertexBufferObject; +import org.lwjgl.opengl.GL; +//#else +//$$ import org.lwjgl.opengl.ARBBufferObject; +//$$ import org.lwjgl.opengl.GLContext; +//$$ import org.lwjgl.opengl.Util; +//#endif import java.nio.ByteBuffer; @@ -14,6 +20,10 @@ import static org.lwjgl.opengl.ARBPixelBufferObject.*; import static org.lwjgl.opengl.GL15.*; import static org.lwjgl.opengl.GL21.GL_PIXEL_PACK_BUFFER; +//#if MC>=11300 +import static org.lwjgl.opengl.ARBVertexBufferObject.*; +//#endif + public class PixelBufferObject { @RequiredArgsConstructor public enum Usage { @@ -24,8 +34,13 @@ public class PixelBufferObject { private final int arb, gl15; } - public static final boolean SUPPORTED = GLContext.getCapabilities().GL_ARB_pixel_buffer_object || GLContext.getCapabilities().OpenGL15; - private static final boolean arb = !GLContext.getCapabilities().OpenGL15; + //#if MC>=11300 + public static final boolean SUPPORTED = GL.getCapabilities().GL_ARB_pixel_buffer_object || GL.getCapabilities().OpenGL15; + private static final boolean arb = !GL.getCapabilities().OpenGL15; + //#else + //$$ public static final boolean SUPPORTED = GLContext.getCapabilities().GL_ARB_pixel_buffer_object || GLContext.getCapabilities().OpenGL15; + //$$ private static final boolean arb = !GLContext.getCapabilities().OpenGL15; + //#endif private static ThreadLocal bound = new ThreadLocal<>(); private static ThreadLocal mapped = new ThreadLocal<>(); @@ -39,12 +54,20 @@ public class PixelBufferObject { } this.size = size; - this.handle = arb ? ARBBufferObject.glGenBuffersARB() : glGenBuffers(); + //#if MC>=11300 + this.handle = arb ? ARBVertexBufferObject.glGenBuffersARB() : glGenBuffers(); + //#else + //$$ this.handle = arb ? ARBBufferObject.glGenBuffersARB() : glGenBuffers(); + //#endif bind(); if (arb) { - ARBBufferObject.glBufferDataARB(GL_PIXEL_PACK_BUFFER_ARB, size, usage.arb); + //#if MC>=11300 + ARBVertexBufferObject.glBufferDataARB(GL_PIXEL_PACK_BUFFER_ARB, size, usage.arb); + //#else + //$$ ARBBufferObject.glBufferDataARB(GL_PIXEL_PACK_BUFFER_ARB, size, usage.arb); + //#endif } else { glBufferData(GL_PIXEL_PACK_BUFFER, size, usage.gl15); } @@ -61,7 +84,11 @@ public class PixelBufferObject { public void bind() { if (arb) { - ARBBufferObject.glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, getHandle()); + //#if MC>=11300 + ARBVertexBufferObject.glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, getHandle()); + //#else + //$$ ARBBufferObject.glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, getHandle()); + //#endif } else { glBindBuffer(GL_PIXEL_PACK_BUFFER, getHandle()); } @@ -71,7 +98,11 @@ public class PixelBufferObject { public void unbind() { checkBound(); if (arb) { - ARBBufferObject.glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, 0); + //#if MC>=11300 + ARBVertexBufferObject.glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, 0); + //#else + //$$ ARBBufferObject.glBindBufferARB(GL_PIXEL_PACK_BUFFER_ARB, 0); + //#endif } else { glBindBuffer(GL_PIXEL_PACK_BUFFER, 0); } @@ -95,13 +126,19 @@ public class PixelBufferObject { checkNotMapped(); ByteBuffer buffer; if (arb) { - buffer = ARBBufferObject.glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_READ_ONLY_ARB, size, null); + //#if MC>=11300 + buffer = ARBVertexBufferObject.glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_READ_ONLY_ARB, size, null); + //#else + //$$ buffer = ARBBufferObject.glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_READ_ONLY_ARB, size, null); + //#endif } else { buffer = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY, size, null); } - if (buffer == null) { - Util.checkGLError(); - } + //#if MC<11300 + //$$ if (buffer == null) { + //$$ Util.checkGLError(); + //$$ } + //#endif mapped.set(getHandle()); return buffer; } @@ -111,13 +148,19 @@ public class PixelBufferObject { checkNotMapped(); ByteBuffer buffer; if (arb) { - buffer = ARBBufferObject.glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_WRITE_ONLY_ARB, size, null); + //#if MC>=11300 + buffer = ARBVertexBufferObject.glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_WRITE_ONLY_ARB, size, null); + //#else + //$$ buffer = ARBBufferObject.glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_WRITE_ONLY_ARB, size, null); + //#endif } else { buffer = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_WRITE_ONLY, size, null); } - if (buffer == null) { - Util.checkGLError(); - } + //#if MC<11300 + //$$ if (buffer == null) { + //$$ Util.checkGLError(); + //$$ } + //#endif mapped.set(getHandle()); return buffer; } @@ -127,13 +170,19 @@ public class PixelBufferObject { checkNotMapped(); ByteBuffer buffer; if (arb) { - buffer = ARBBufferObject.glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_READ_WRITE_ARB, size, null); + //#if MC>=11300 + buffer = ARBVertexBufferObject.glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_READ_WRITE_ARB, size, null); + //#else + //$$ buffer = ARBBufferObject.glMapBufferARB(GL_PIXEL_PACK_BUFFER_ARB, GL_READ_WRITE_ARB, size, null); + //#endif } else { buffer = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_WRITE, size, null); } - if (buffer == null) { - Util.checkGLError(); - } + //#if MC<11300 + //$$ if (buffer == null) { + //$$ Util.checkGLError(); + //$$ } + //#endif mapped.set(getHandle()); return buffer; } @@ -144,7 +193,11 @@ public class PixelBufferObject { throw new IllegalStateException("Buffer not mapped."); } if (arb) { - ARBBufferObject.glUnmapBufferARB(GL_PIXEL_PACK_BUFFER_ARB); + //#if MC>=11300 + ARBVertexBufferObject.glUnmapBufferARB(GL_PIXEL_PACK_BUFFER_ARB); + //#else + //$$ ARBBufferObject.glUnmapBufferARB(GL_PIXEL_PACK_BUFFER_ARB); + //#endif } else { glUnmapBuffer(GL_PIXEL_PACK_BUFFER); } @@ -154,7 +207,11 @@ public class PixelBufferObject { public void delete() { if (handle != -1) { if (arb) { - ARBBufferObject.glDeleteBuffersARB(getHandle()); + //#if MC>=11300 + ARBVertexBufferObject.glDeleteBuffersARB(getHandle()); + //#else + //$$ ARBBufferObject.glDeleteBuffersARB(getHandle()); + //#endif } else { glDeleteBuffers(getHandle()); } @@ -167,12 +224,7 @@ public class PixelBufferObject { super.finalize(); if (handle != -1) { LogManager.getLogger().warn("PBO garbage collected before deleted!"); - Minecraft.getMinecraft().addScheduledTask(new Runnable() { - @Override - public void run() { - delete(); - } - }); + MCVer.getMinecraft().addScheduledTask(this::delete); } } } diff --git a/src/main/java/com/replaymod/render/utils/SoundHandler.java b/src/main/java/com/replaymod/render/utils/SoundHandler.java index c0ea3f1d..8959689d 100644 --- a/src/main/java/com/replaymod/render/utils/SoundHandler.java +++ b/src/main/java/com/replaymod/render/utils/SoundHandler.java @@ -1,6 +1,6 @@ package com.replaymod.render.utils; -import net.minecraft.client.Minecraft; +import com.replaymod.core.versions.MCVer; import net.minecraft.util.ResourceLocation; import org.apache.commons.io.IOUtils; @@ -24,7 +24,7 @@ public class SoundHandler { */ public void playSound(ResourceLocation loc) { try { - InputStream is = Minecraft.getMinecraft().getResourceManager().getResource(loc).getInputStream(); + InputStream is = MCVer.getMinecraft().getResourceManager().getResource(loc).getInputStream(); byte[] bytes = IOUtils.toByteArray(is); is.close(); AudioInputStream ais = AudioSystem.getAudioInputStream(new ByteArrayInputStream(bytes)); diff --git a/src/main/java/com/replaymod/render/utils/Utils.java b/src/main/java/com/replaymod/render/utils/Utils.java index aac54ccf..19f3187c 100644 --- a/src/main/java/com/replaymod/render/utils/Utils.java +++ b/src/main/java/com/replaymod/render/utils/Utils.java @@ -13,12 +13,12 @@ public class Utils { * @param width Target image width */ public static void openGlBytesToRBG(ByteBuffer buffer, int bufferWidth, int xOffset, int yOffset, ByteBuffer to, int width) { - byte[] rowBuf = new byte[bufferWidth * 3]; + byte[] rowBuf = new byte[bufferWidth * 4]; // Copy image flipped vertically to target buffer - int rows = buffer.remaining() / 3 / bufferWidth; + int rows = buffer.remaining() / 4 / bufferWidth; for (int i = 0; i < rows; i++) { buffer.get(rowBuf); - to.position(((yOffset + rows - i - 1) * width + xOffset) * 3); + to.position(((yOffset + rows - i - 1) * width + xOffset) * 4); to.put(rowBuf); } to.rewind(); diff --git a/src/main/java/com/replaymod/replay/FullReplaySender.java b/src/main/java/com/replaymod/replay/FullReplaySender.java new file mode 100755 index 00000000..cf7d4e04 --- /dev/null +++ b/src/main/java/com/replaymod/replay/FullReplaySender.java @@ -0,0 +1,1233 @@ +package com.replaymod.replay; + +import com.google.common.base.Preconditions; +import com.google.common.io.Files; +import com.replaymod.core.ReplayMod; +import com.replaymod.core.utils.Restrictions; +import com.replaymod.replay.camera.CameraEntity; +import com.replaymod.replaystudio.io.ReplayInputStream; +import com.replaymod.replaystudio.replay.ReplayFile; +import com.replaymod.replaystudio.studio.ReplayStudio; +import com.replaymod.replaystudio.studio.protocol.StudioCodec; +import com.replaymod.replaystudio.studio.protocol.StudioSession; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.EntityOtherPlayerMP; +import net.minecraft.client.gui.GuiDownloadTerrain; +import net.minecraft.client.gui.GuiErrorScreen; +import net.minecraft.client.resources.I18n; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.network.*; +import net.minecraft.network.play.server.*; +import net.minecraft.world.EnumDifficulty; +import net.minecraft.world.World; +import net.minecraft.world.WorldType; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.IChunkProvider; +import net.minecraftforge.common.MinecraftForge; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; + +//#if MC>=11300 +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.dimension.DimensionType; +//#endif + +//#if MC>=11200 +import com.replaymod.core.utils.WrappedTimer; +//#endif +//#if MC>=11002 +import net.minecraft.world.GameType; +//#else +//$$ import net.minecraft.world.WorldSettings.GameType; +//#endif +//#if MC>=10904 +import net.minecraft.network.login.server.SPacketLoginSuccess; +import net.minecraft.util.text.ITextComponent; +//#else +//$$ import net.minecraft.network.login.server.S02PacketLoginSuccess; +//$$ import net.minecraft.util.IChatComponent; +//#endif + +//#if MC>=10800 +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif +import net.minecraftforge.fml.common.gameevent.TickEvent; +//#else +//$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; +//$$ import cpw.mods.fml.common.gameevent.TickEvent; +//$$ import org.apache.commons.io.Charsets; +//#endif + +import java.io.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static com.replaymod.core.versions.MCVer.*; + +/** + * Sends replay packets to netty channels. + * Even though {@link Sharable}, this should never be added to multiple pipes at once, it may however be re-added when + * the replay restart from the beginning. + */ +@Sharable +public class FullReplaySender extends ChannelDuplexHandler implements ReplaySender { + /** + * These packets are ignored completely during replay. + */ + private static final List BAD_PACKETS = Arrays.asList( + //#if MC>=11200 + SPacketRecipeBook.class, + SPacketAdvancementInfo.class, + SPacketSelectAdvancementsTab.class, + //#endif + //#if MC>=10904 + // TODO Update possibly more? + SPacketUpdateHealth.class, + SPacketOpenWindow.class, + SPacketCloseWindow.class, + SPacketSetSlot.class, + SPacketWindowItems.class, + SPacketSignEditorOpen.class, + SPacketStatistics.class, + SPacketSetExperience.class, + SPacketCamera.class, + SPacketPlayerAbilities.class, + SPacketTitle.class + //#else + //#if MC>=10800 + //$$ S43PacketCamera.class, + //$$ S45PacketTitle.class, + //#endif + //$$ S06PacketUpdateHealth.class, + //$$ S2DPacketOpenWindow.class, + //$$ S2EPacketCloseWindow.class, + //$$ S2FPacketSetSlot.class, + //$$ S30PacketWindowItems.class, + //$$ S36PacketSignEditorOpen.class, + //$$ S37PacketStatistics.class, + //$$ S1FPacketSetExperience.class, + //$$ S39PacketPlayerAbilities.class + //#endif + ); + + private static int TP_DISTANCE_LIMIT = 128; + + /** + * The replay handler responsible for the current replay. + */ + private final ReplayHandler replayHandler; + + /** + * Whether to work in async mode. + * + * When in async mode, a separate thread send packets and waits according to their delays. + * This is default in normal playback mode. + * + * When in sync mode, no packets will be sent until {@link #sendPacketsTill(int)} is called. + * This is used during path playback and video rendering. + */ + protected boolean asyncMode; + + /** + * Timestamp of the last packet sent in milliseconds since the start. + */ + protected int lastTimeStamp; + + /** + * @see #currentTimeStamp() + */ + protected int currentTimeStamp; + + /** + * The replay file. + */ + protected ReplayFile replayFile; + + /** + * The channel handler context used to send packets to minecraft. + */ + protected ChannelHandlerContext ctx; + + /** + * The replay input stream from which new packets are read. + * When accessing this stream make sure to synchronize on {@code this} as it's used from multiple threads. + */ + protected ReplayInputStream replayIn; + + /** + * The next packet that should be sent. + * This is required as some actions such as jumping to a specified timestamp have to peek at the next packet. + */ + protected PacketData nextPacket; + + /** + * Whether we're currently reading packets from the login phase. + */ + private boolean loginPhase; + + /** + * Whether we need to restart the current replay. E.g. when jumping backwards in time + */ + protected boolean startFromBeginning = true; + + /** + * Whether to terminate the replay. This only has an effect on the async mode and is {@code true} during sync mode. + */ + protected boolean terminate; + + /** + * The speed of the replay. 1 is normal, 2 is twice as fast, 0.5 is half speed and 0 is frozen + */ + protected double replaySpeed = 1f; + + /** + * Whether the world has been loaded and the dirt-screen should go away. + */ + protected boolean hasWorldLoaded; + + /** + * The minecraft instance. + */ + protected Minecraft mc = getMinecraft(); + + /** + * The total length of this replay in milliseconds. + */ + protected final int replayLength; + + /** + * Our actual entity id that the server gave to us. + */ + protected int actualID = -1; + + /** + * Whether to allow (process) the next player movement packet. + */ + protected boolean allowMovement; + + /** + * Directory to which resource packs are extracted. + */ + private final File tempResourcePackFolder = Files.createTempDir(); + + /** + * Create a new replay sender. + * @param file The replay file + * @param asyncMode {@code true} for async mode, {@code false} otherwise + * @see #asyncMode + */ + public FullReplaySender(ReplayHandler replayHandler, ReplayFile file, boolean asyncMode) throws IOException { + this.replayHandler = replayHandler; + this.replayFile = file; + this.asyncMode = asyncMode; + this.replayLength = file.getMetaData().getDuration(); + + MinecraftForge.EVENT_BUS.register(this); + + if (asyncMode) { + new Thread(asyncSender, "replaymod-async-sender").start(); + } + } + + /** + * Set whether this replay sender operates in async mode. + * When in async mode, it will send packets timed from a separate thread. + * When not in async mode, it will send packets when {@link #sendPacketsTill(int)} is called. + * @param asyncMode {@code true} to enable async mode + */ + @Override + public void setAsyncMode(boolean asyncMode) { + if (this.asyncMode == asyncMode) return; + this.asyncMode = asyncMode; + if (asyncMode) { + this.terminate = false; + new Thread(asyncSender, "replaymod-async-sender").start(); + } else { + this.terminate = true; + } + } + + @Override + public boolean isAsyncMode() { + return asyncMode; + } + + /** + * Set whether this replay sender to operate in sync mode. + * When in sync mode, it will send packets when {@link #sendPacketsTill(int)} is called. + * This call will block until the async worker thread has stopped. + */ + @Override + public void setSyncModeAndWait() { + if (!this.asyncMode) return; + this.asyncMode = false; + this.terminate = true; + synchronized (this) { + // This will wait for the worker thread to leave the synchronized code part + } + } + + /** + * Return a fake system tile in milliseconds value that respects slowdown/speedup/pause and works in both, + * sync and async mode. + * Note: For sync mode this returns the last value passed to {@link #sendPacketsTill(int)}. + * @return The timestamp in milliseconds since the start of the replay + */ + @Override + public int currentTimeStamp() { + if (asyncMode) { + int timePassed = (int) (System.currentTimeMillis() - lastPacketSent); + return lastTimeStamp + (int) (timePassed * getReplaySpeed()); + } else { + return lastTimeStamp; + } + } + + /** + * Terminate this replay sender. + */ + public void terminateReplay() { + terminate = true; + MinecraftForge.EVENT_BUS.unregister(this); + try { + channelInactive(ctx); + ctx.channel().pipeline().close(); + FileUtils.deleteDirectory(tempResourcePackFolder); + } catch(Exception e) { + e.printStackTrace(); + } + } + + @SubscribeEvent + public void onWorldTick(TickEvent.ClientTickEvent event) { + // Unfortunately the WorldTickEvent doesn't seem to be emitted on the CLIENT side + if (event.phase != TickEvent.Phase.START) return; + + // Spawning a player into an empty chunk (which we might do with the recording player) + // prevents it from being moved by teleport packets (it essentially gets stuck) because + // Entity#addedToChunk is not set and it is therefore not updated every tick. + // To counteract this, we need to manually update it's position if it hasn't been added + // to any chunk yet. + if (world(mc) != null) { + for (EntityPlayer playerEntity : playerEntities(world(mc))) { + if (!playerEntity.addedToChunk && playerEntity instanceof EntityOtherPlayerMP) { + //#if MC>=11300 + playerEntity.livingTick(); + //#else + //$$ playerEntity.onLivingUpdate(); + //#endif + } + } + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) + throws Exception { + // When in async mode and the replay sender shut down, then don't send packets + if(terminate && asyncMode) { + return; + } + + // When a packet is sent directly, perform no filtering + if(msg instanceof Packet) { + super.channelRead(ctx, msg); + } + + if (msg instanceof byte[]) { + try { + Packet p = deserializePacket((byte[]) msg); + + if (p != null) { + p = processPacket(p); + if (p != null) { + super.channelRead(ctx, p); + } + + // If we do not give minecraft time to tick, there will be dead entity artifacts left in the world + // Therefore we have to remove all loaded, dead entities manually if we are in sync mode. + // We do this after every SpawnX packet and after the destroy entities packet. + if (!asyncMode && world(mc) != null) { + //#if MC>=10904 + if (p instanceof SPacketSpawnPlayer + || p instanceof SPacketSpawnObject + || p instanceof SPacketSpawnMob + || p instanceof SPacketSpawnGlobalEntity + || p instanceof SPacketSpawnPainting + || p instanceof SPacketSpawnExperienceOrb + || p instanceof SPacketDestroyEntities) { + //#else + //$$ if (p instanceof S0CPacketSpawnPlayer + //$$ || p instanceof S0EPacketSpawnObject + //$$ || p instanceof S0FPacketSpawnMob + //$$ || p instanceof S2CPacketSpawnGlobalEntity + //$$ || p instanceof S10PacketSpawnPainting + //$$ || p instanceof S11PacketSpawnExperienceOrb + //$$ || p instanceof S13PacketDestroyEntities) { + //#endif + World world = world(mc); + for (int i = 0; i < world.loadedEntityList.size(); ++i) { + Entity entity = loadedEntityList(world).get(i); + //#if MC>=11300 + if (entity.removed) { + //#else + //$$ if (entity.isDead) { + //#endif + int chunkX = entity.chunkCoordX; + int chunkY = entity.chunkCoordZ; + + //#if MC>=11300 + if (entity.addedToChunk && world.getChunkProvider().provideChunk(chunkX, chunkY, false, false) != null) { + //#else + //#if MC>=10904 + //$$ if (entity.addedToChunk && world.getChunkProvider().getLoadedChunk(chunkX, chunkY) != null) { + //#else + //$$ if (entity.addedToChunk && world.getChunkProvider().chunkExists(chunkX, chunkY)) { + //#endif + //#endif + //#if MC>=11300 + world.getChunk(chunkX, chunkY).removeEntity(entity); + //#else + //$$ world.getChunkFromChunkCoords(chunkX, chunkY).removeEntity(entity); + //#endif + } + + world.loadedEntityList.remove(i--); + world.onEntityRemoved(entity); + } + + } + } + } + } + } catch (Exception e) { + // We'd rather not have a failure parsing one packet screw up the whole replay process + e.printStackTrace(); + } + } + + } + + private Packet deserializePacket(byte[] bytes) throws IOException, IllegalAccessException, InstantiationException { + ByteBuf bb = Unpooled.wrappedBuffer(bytes); + PacketBuffer pb = new PacketBuffer(bb); + + int i = readVarInt(pb); + + EnumConnectionState state = loginPhase ? EnumConnectionState.LOGIN : EnumConnectionState.PLAY; + //#if MC>=10800 + Packet p = state.getPacket(EnumPacketDirection.CLIENTBOUND, i); + //#else + //$$ Packet p = Packet.generatePacket(state.func_150755_b(), i); + //#endif + p.readPacketData(pb); + + return p; + } + + /** + * Process a packet and return the result. + * @param p The packet to process + * @return The processed packet or {@code null} if no packet shall be sent + */ + protected Packet processPacket(Packet p) throws Exception { + //#if MC>=10904 + if (p instanceof SPacketLoginSuccess) { + //#else + //$$ if (p instanceof S02PacketLoginSuccess) { + //#endif + loginPhase = false; + return p; + } + + //#if MC>=10904 + if (p instanceof SPacketCustomPayload) { + SPacketCustomPayload packet = (SPacketCustomPayload) p; + //#else + //$$ if (p instanceof S3FPacketCustomPayload) { + //$$ S3FPacketCustomPayload packet = (S3FPacketCustomPayload) p; + //#endif + //#if MC>=11300 + ResourceLocation channelName = packet.getChannelName(); + //#else + //#if MC>=10800 + //$$ String channelName = packet.getChannelName(); + //#else + //$$ String channelName = packet.func_149169_c(); + //#endif + //#endif + if (Restrictions.PLUGIN_CHANNEL.equals(channelName)) { + final String unknown = replayHandler.getRestrictions().handle(packet); + if (unknown == null) { + return null; + } else { + // Failed to parse options, make sure that under no circumstances further packets are parsed + terminateReplay(); + // Then end replay and show error GUI + mc.addScheduledTask(new Runnable() { + @Override + public void run() { + try { + replayHandler.endReplay(); + } catch (IOException e) { + e.printStackTrace(); + } + mc.displayGuiScreen(new GuiErrorScreen( + I18n.format("replaymod.error.unknownrestriction1"), + I18n.format("replaymod.error.unknownrestriction2", unknown) + )); + } + }); + } + } + } + //#if MC>=10904 + if (p instanceof SPacketDisconnect) { + ITextComponent reason = ((SPacketDisconnect) p).getReason(); + //#else + //$$ if (p instanceof S40PacketDisconnect) { + //#if MC>=10809 + //$$ IChatComponent reason = ((S40PacketDisconnect) p).getReason(); + //#else + //$$ IChatComponent reason = ((S40PacketDisconnect) p).func_149165_c(); + //#endif + //#endif + //#if MC>=11300 + // FIXME or is it getString? + String message = reason.getUnformattedComponentText(); + //#else + //$$ String message = reason.getUnformattedText(); + //#endif + if ("Please update to view this replay.".equals(message)) { + // This version of the mod supports replay restrictions so we are allowed + // to remove this packet. + return null; + } + } + + if(BAD_PACKETS.contains(p.getClass())) return null; + + //#if MC>=10904 + if (p instanceof SPacketCustomPayload) { + SPacketCustomPayload packet = (SPacketCustomPayload) p; + //#else + //$$ if (p instanceof S3FPacketCustomPayload) { + //$$ S3FPacketCustomPayload packet = (S3FPacketCustomPayload) p; + //#endif + //#if MC>=10800 + //#if MC>=11300 + ResourceLocation channelName = packet.getChannelName(); + //#else + //$$ String channelName = packet.getChannelName(); + //#endif + //#else + //$$ String channelName = packet.func_149169_c(); + //#endif + //#if MC>=11300 + if (SPacketCustomPayload.BOOK_OPEN.equals(channelName)) { + //#else + //$$ if ("MC|BOpen".equals(channelName)) { + //#endif + return null; + } + //#if MC>=10800 + } + + //#if MC>=10904 + if(p instanceof SPacketResourcePackSend) { + SPacketResourcePackSend packet = (SPacketResourcePackSend) p; + String url = packet.getURL(); + //#else + //$$ if(p instanceof S48PacketResourcePackSend) { + //$$ S48PacketResourcePackSend packet = (S48PacketResourcePackSend) p; + //#if MC>=10809 + //$$ String url = packet.getURL(); + //#else + //$$ String url = packet.func_179783_a(); + //#endif + //#endif + if (url.startsWith("replay://")) { + //#else + //$$ String url; + //$$ if ("MC|RPack".equals(channelName) && + //$$ (url = new String(packet.func_149168_d(), Charsets.UTF_8)).startsWith("replay://")) { + //#endif + int id = Integer.parseInt(url.substring("replay://".length())); + Map index = replayFile.getResourcePackIndex(); + if (index != null) { + String hash = index.get(id); + if (hash != null) { + File file = new File(tempResourcePackFolder, hash + ".zip"); + if (!file.exists()) { + IOUtils.copy(replayFile.getResourcePack(hash).get(), new FileOutputStream(file)); + } + setServerResourcePack(file); + } + } + return null; + } + } + + //#if MC>=10904 + if(p instanceof SPacketJoinGame) { + SPacketJoinGame packet = (SPacketJoinGame) p; + int entId = packet.getPlayerId(); + //#else + //$$ if(p instanceof S01PacketJoinGame) { + //$$ S01PacketJoinGame packet = (S01PacketJoinGame) p; + //#if MC>=10800 + //$$ int entId = packet.getEntityId(); + //#else + //$$ int entId = packet.func_149197_c(); + //#endif + //#endif + allowMovement = true; + actualID = entId; + entId = -1789435; // Camera entity id should be negative which is an invalid id and can't be used by servers + //#if MC>=10800 + //#if MC>=11300 + DimensionType dimension = packet.func_212642_e(); + //#else + //$$ int dimension = packet.getDimension(); + //#endif + EnumDifficulty difficulty = packet.getDifficulty(); + //#if MC>=11300 + int maxPlayers = packet.maxPlayers; + //#else + //$$ int maxPlayers = packet.getMaxPlayers(); + //#endif + WorldType worldType = packet.getWorldType(); + + //#if MC>=10904 + p = new SPacketJoinGame(entId, GameType.SPECTATOR, false, dimension, + difficulty, maxPlayers, worldType, false); + //#else + //$$ p = new S01PacketJoinGame(entId, GameType.SPECTATOR, false, dimension, + //$$ difficulty, maxPlayers, worldType, false); + //#endif + //#else + //$$ int dimension = packet.func_149194_f(); + //$$ EnumDifficulty difficulty = packet.func_149192_g(); + //$$ int maxPlayers = packet.func_149193_h(); + //$$ WorldType worldType = packet.func_149196_i(); + //$$ + //$$ p = new S01PacketJoinGame(entId, GameType.ADVENTURE, false, dimension, + //$$ difficulty, maxPlayers, worldType); + //#endif + } + + //#if MC>=10904 + if(p instanceof SPacketRespawn) { + SPacketRespawn respawn = (SPacketRespawn) p; + p = new SPacketRespawn( + //#if MC>=11300 + respawn.func_212643_b(), + //#else + //$$ respawn.getDimensionID(), + //#endif + respawn.getDifficulty(), respawn.getWorldType(), GameType.SPECTATOR); + //#else + //$$ if(p instanceof S07PacketRespawn) { + //$$ S07PacketRespawn respawn = (S07PacketRespawn) p; + //#if MC>=10809 + //$$ p = new S07PacketRespawn(respawn.getDimensionID(), + //$$ respawn.getDifficulty(), respawn.getWorldType(), GameType.SPECTATOR); + //#else + //$$ p = new S07PacketRespawn(respawn.func_149082_c(), + //$$ respawn.func_149081_d(), respawn.func_149080_f(), + //#if MC>=10800 + //$$ GameType.SPECTATOR); + //#else + //$$ GameType.ADVENTURE); + //#endif + //#endif + //#endif + + allowMovement = true; + } + + //#if MC>=10904 + if(p instanceof SPacketPlayerPosLook) { + final SPacketPlayerPosLook ppl = (SPacketPlayerPosLook) p; + //#else + //$$ if(p instanceof S08PacketPlayerPosLook) { + //$$ final S08PacketPlayerPosLook ppl = (S08PacketPlayerPosLook) p; + //#endif + if(!hasWorldLoaded) hasWorldLoaded = true; + + if (mc.currentScreen instanceof GuiDownloadTerrain) { + // Close the world loading screen manually in case we swallow the packet + mc.displayGuiScreen(null); + } + + if(replayHandler.shouldSuppressCameraMovements()) return null; + + CameraEntity cent = replayHandler.getCameraEntity(); + + //#if MC>=10800 + //#if MC>=10904 + for (SPacketPlayerPosLook.EnumFlags relative : ppl.getFlags()) { + if (relative == SPacketPlayerPosLook.EnumFlags.X + || relative == SPacketPlayerPosLook.EnumFlags.Y + || relative == SPacketPlayerPosLook.EnumFlags.Z) { + //#else + //$$ for (Object relative : ppl.func_179834_f()) { + //$$ if (relative == S08PacketPlayerPosLook.EnumFlags.X + //$$ || relative == S08PacketPlayerPosLook.EnumFlags.Y + //$$ || relative == S08PacketPlayerPosLook.EnumFlags.Z) { + //#endif + return null; // At least one of the coordinates is relative, so we don't care + } + } + //#endif + + if(cent != null) { + //#if MC>=10809 + if(!allowMovement && !((Math.abs(cent.posX - ppl.getX()) > TP_DISTANCE_LIMIT) || + (Math.abs(cent.posZ - ppl.getZ()) > TP_DISTANCE_LIMIT))) { + //#else + //$$ if(!allowMovement && !((Math.abs(cent.posX - ppl.func_148932_c()) > TP_DISTANCE_LIMIT) || + //$$ (Math.abs(cent.posZ - ppl.func_148933_e()) > TP_DISTANCE_LIMIT))) { + //#endif + return null; + } else { + allowMovement = false; + } + } + + new Runnable() { + @Override + @SuppressWarnings("unchecked") + public void run() { + if (world(mc) == null || !mc.isCallingFromMinecraftThread()) { + ReplayMod.instance.runLater(this); + return; + } + + CameraEntity cent = replayHandler.getCameraEntity(); + //#if MC>=10809 + cent.setCameraPosition(ppl.getX(), ppl.getY(), ppl.getZ()); + //#else + //$$ cent.setCameraPosition(ppl.func_148932_c(), ppl.func_148928_d(), ppl.func_148933_e()); + //#endif + } + }.run(); + } + + //#if MC>=10904 + if(p instanceof SPacketChangeGameState) { + SPacketChangeGameState pg = (SPacketChangeGameState)p; + int reason = pg.getGameState(); + //#else + //$$ if(p instanceof S2BPacketChangeGameState) { + //$$ S2BPacketChangeGameState pg = (S2BPacketChangeGameState)p; + //#if MC>=10809 + //$$ int reason = pg.getGameState(); + //#else + //$$ int reason = pg.func_149138_c(); + //#endif + //#endif + + // only allow the following packets: + // 1 - End raining + // 2 - Begin raining + // + // The following values are to control sky color (e.g. if thunderstorm) + // 7 - Fade value + // 8 - Fade time + if(!(reason == 1 || reason == 2 || reason == 7 || reason == 8)) { + return null; + } + } + + //#if MC>=10904 + if (p instanceof SPacketChat) { + //#else + //$$ if (p instanceof S02PacketChat) { + //#endif + if (!ReplayModReplay.instance.getCore().getSettingsRegistry().get(Setting.SHOW_CHAT)) { + return null; + } + } + + return asyncMode ? processPacketAsync(p) : processPacketSync(p); + } + + @Override + @SuppressWarnings("unchecked") + public void channelActive(ChannelHandlerContext ctx) throws Exception { + this.ctx = ctx; + //#if MC>=10904 + ctx.channel().attr(NetworkManager.PROTOCOL_ATTRIBUTE_KEY).set(EnumConnectionState.PLAY); + //#else + //#if MC>=10800 + //$$ ctx.attr(NetworkManager.attrKeyConnectionState).set(EnumConnectionState.PLAY); + //#endif + //#endif + super.channelActive(ctx); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + // The embedded channel's event loop will consider every thread to be in it and as such provides no + // guarantees that only one thread is using the pipeline at any one time. + // For reading the replay sender (either sync or async) is the only thread ever writing. + // For writing it may very well happen that multiple threads want to use the pipline at the same time. + // It's unclear whether the EmbeddedChannel is supposed to be thread-safe (the behavior of the event loop + // does suggest that). However it seems like it either isn't (likely) or there is a race condition. + // See: https://www.replaymod.com/forum/thread/1752#post8045 (https://paste.replaymod.com/lotacatuwo) + // To work around this issue, we just outright drop all write/flush requests (they aren't needed anyway). + // This still leaves channel handlers upstream with the threading issue but they all seem to cope well with it. + promise.setSuccess(); + } + + @Override + public void flush(ChannelHandlerContext ctx) throws Exception { + // See write method above + } + + /** + * Returns the speed of the replay. 1 being normal speed, 0.5 half and 2 twice as fast. + * If 0 is returned, the replay is paused. + * @return speed multiplier + */ + @Override + public double getReplaySpeed() { + if(!paused()) return replaySpeed; + else return 0; + } + + /** + * Set the speed of the replay. 1 being normal speed, 0.5 half and 2 twice as fast. + * The speed may not be set to 0 nor to negative values. + * @param d Speed multiplier + */ + @Override + public void setReplaySpeed(final double d) { + if(d != 0) this.replaySpeed = d; + //#if MC>=11200 + mc.timer.tickLength = WrappedTimer.DEFAULT_MS_PER_TICK / (float) d; + //#else + //$$ mc.timer.timerSpeed = (float) d; + //#endif + } + + ///////////////////////////////////////////////////////// + // Asynchronous packet processing // + ///////////////////////////////////////////////////////// + + /** + * The real time at which the last packet was sent in milliseconds. + */ + private long lastPacketSent; + + /** + * There is no waiting performed until a packet with at least this timestamp is reached (but not yet sent). + * If this is -1, then timing is normal. + */ + private long desiredTimeStamp = -1; + + /** + * Runnable which performs timed dispatching of packets from the input stream. + */ + private Runnable asyncSender = new Runnable() { + public void run() { + try { + while (ctx == null && !terminate) { + Thread.sleep(10); + } + REPLAY_LOOP: + while (!terminate) { + synchronized (FullReplaySender.this) { + if (replayIn == null) { + replayIn = replayFile.getPacketData(new ReplayStudio(), true); + } + // Packet loop + while (true) { + try { + // When playback is paused and the world has loaded (we don't want any dirt-screens) we sleep + while (paused() && hasWorldLoaded) { + // Unless we are going to terminate, restart or jump + if (terminate || startFromBeginning || desiredTimeStamp != -1) { + break; + } + Thread.sleep(10); + } + + if (terminate) { + break REPLAY_LOOP; + } + + if (startFromBeginning) { + // In case we need to restart from the beginning + // break out of the loop sending all packets which will + // cause the replay to be restarted by the outer loop + break; + } + + // Read the next packet if we don't already have one + if (nextPacket == null) { + nextPacket = new PacketData(replayIn, loginPhase); + } + + int nextTimeStamp = nextPacket.timestamp; + + // If we aren't jumping and the world has already been loaded (no dirt-screens) then wait + // the required amount to get proper packet timing + if (!isHurrying() && hasWorldLoaded) { + // How much time should have passed + int timeWait = (int) Math.round((nextTimeStamp - lastTimeStamp) / replaySpeed); + // How much time did pass + long timeDiff = System.currentTimeMillis() - lastPacketSent; + // How much time we need to wait to make up for the difference + long timeToSleep = Math.max(0, timeWait - timeDiff); + + Thread.sleep(timeToSleep); + lastPacketSent = System.currentTimeMillis(); + } + + // Process packet + channelRead(ctx, nextPacket.bytes); + nextPacket = null; + + lastTimeStamp = nextTimeStamp; + + // In case we finished jumping + // We need to check that we aren't planing to restart so we don't accidentally run this + // code before we actually restarted + if (isHurrying() && lastTimeStamp > desiredTimeStamp && !startFromBeginning) { + desiredTimeStamp = -1; + + replayHandler.moveCameraToTargetPosition(); + + // Pause after jumping + setReplaySpeed(0); + } + } catch (EOFException eof) { + // Reached end of file + // Pause the replay which will cause it to freeze before getting restarted + setReplaySpeed(0); + // Then wait until the user tells us to continue + while (paused() && hasWorldLoaded && desiredTimeStamp == -1 && !terminate) { + Thread.sleep(10); + } + break; + } catch (IOException e) { + e.printStackTrace(); + } + } + + // Restart the replay. + hasWorldLoaded = false; + lastTimeStamp = 0; + loginPhase = true; + startFromBeginning = false; + nextPacket = null; + lastPacketSent = System.currentTimeMillis(); + replayHandler.restartedReplay(); + if (replayIn != null) { + replayIn.close(); + replayIn = null; + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }; + + /** + * Return whether this replay sender is currently rushing. When rushing, all packets are sent without waiting until + * a specified timestamp is passed. + * @return {@code true} if currently rushing, {@code false} otherwise + */ + public boolean isHurrying() { + return desiredTimeStamp != -1; + } + + /** + * Cancels the hurrying. + */ + public void stopHurrying() { + desiredTimeStamp = -1; + } + + /** + * Return the timestamp to which this replay sender is currently rushing. All packets with an lower or equal + * timestamp will be sent out without any sleeping. + * @return The timestamp in milliseconds since the start of the replay + */ + public long getDesiredTimestamp() { + return desiredTimeStamp; + } + + /** + * Jumps to the specified timestamp when in async mode by rushing all packets until one with a timestamp greater + * than the specified timestamp is found. + * If the timestamp has already passed, this causes the replay to restart and then rush all packets. + * @param millis Timestamp in milliseconds since the start of the replay + */ + @Override + public void jumpToTime(int millis) { + Preconditions.checkState(asyncMode, "Can only jump in async mode. Use sendPacketsTill(int) instead."); + if(millis < lastTimeStamp && !isHurrying()) { + startFromBeginning = true; + } + + desiredTimeStamp = millis; + } + + protected Packet processPacketAsync(Packet p) { + //If hurrying, ignore some packets, except for short durations + if(desiredTimeStamp - lastTimeStamp > 1000) { + //#if MC>=10904 + if(p instanceof SPacketParticles) return null; + + if(p instanceof SPacketSpawnObject) { + SPacketSpawnObject pso = (SPacketSpawnObject)p; + int type = pso.getType(); + //#else + //$$ if(p instanceof S2APacketParticles) return null; + //$$ + //$$ if(p instanceof S0EPacketSpawnObject) { + //$$ S0EPacketSpawnObject pso = (S0EPacketSpawnObject)p; + //#if MC>=10809 + //$$ int type = pso.getType(); + //#else + //$$ int type = pso.func_148993_l(); + //#endif + //#endif + if(type == 76) { // Firework rocket + return null; + } + } + } + return p; + } + + ///////////////////////////////////////////////////////// + // Synchronous packet processing // + ///////////////////////////////////////////////////////// + + /** + * Sends all packets until the specified timestamp is reached (inclusive). + * If the timestamp is smaller than the last packet sent, the replay is restarted from the beginning. + * @param timestamp The timestamp in milliseconds since the beginning of this replay + */ + @Override + public void sendPacketsTill(int timestamp) { + Preconditions.checkState(!asyncMode, "This method cannot be used in async mode. Use jumpToTime(int) instead."); + try { + while (ctx == null && !terminate) { // Make sure channel is ready + Thread.sleep(10); + } + + synchronized (this) { + if (timestamp == lastTimeStamp) { // Do nothing if we're already there + return; + } + if (timestamp < lastTimeStamp) { // Restart the replay if we need to go backwards in time + hasWorldLoaded = false; + lastTimeStamp = 0; + if (replayIn != null) { + replayIn.close(); + replayIn = null; + } + loginPhase = true; + startFromBeginning = false; + nextPacket = null; + replayHandler.restartedReplay(); + } + + if (replayIn == null) { + replayIn = replayFile.getPacketData(new ReplayStudio(), true); + } + + while (true) { // Send packets + try { + PacketData pd; + if (nextPacket != null) { + // If there is still a packet left from before, use it first + pd = nextPacket; + nextPacket = null; + } else { + // Otherwise read one from the input stream + pd = new PacketData(replayIn, loginPhase); + } + + int nextTimeStamp = pd.timestamp; + if (nextTimeStamp > timestamp) { + // We are done sending all packets + nextPacket = pd; + break; + } + + // Process packet + channelRead(ctx, pd.bytes); + } catch (EOFException eof) { + // Shit! We hit the end before finishing our job! What shall we do now? + // well, let's just pretend we're done... + replayIn = null; + break; + } catch (IOException e) { + e.printStackTrace(); + } + } + + // This might be required if we change to async mode anytime soon + lastPacketSent = System.currentTimeMillis(); + lastTimeStamp = timestamp; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + protected Packet processPacketSync(Packet p) { + //#if MC>=10904 + if (p instanceof SPacketUnloadChunk) { + SPacketUnloadChunk packet = (SPacketUnloadChunk) p; + int x = packet.getX(); + int z = packet.getZ(); + //#else + //#if MC>=10809 + //$$ if (p instanceof S21PacketChunkData && ((S21PacketChunkData) p).getExtractedSize() == 0) { + //$$ S21PacketChunkData packet = (S21PacketChunkData) p; + //$$ int x = packet.getChunkX(); + //$$ int z = packet.getChunkZ(); + //#else + //$$ if (p instanceof S21PacketChunkData && ((S21PacketChunkData) p).func_149276_g() == 0) { + //$$ S21PacketChunkData packet = (S21PacketChunkData) p; + //$$ int x = packet.func_149273_e(); + //$$ int z = packet.func_149271_f(); + //#endif + //#endif + // If the chunk is getting unloaded, we will have to forcefully update the position of all entities + // within. Otherwise, if there wasn't a game tick recently, there may be entities that have moved + // out of the chunk by now but are still registered in it. If we do not update those, they will get + // unloaded even though they shouldn't. + // Note: This is only half of the truth. Entities may be removed by chunk-unloading, see else-case below. + // To make things worse, it seems like players were never supposed to be unloaded this way because + // they will remain glitched in the World#playerEntities list. + World world = world(mc); + IChunkProvider chunkProvider = world.getChunkProvider(); + // Get the chunk that will be unloaded + //#if MC>=11300 + Chunk chunk = chunkProvider.provideChunk(x, z, false, false); + //#else + //$$ Chunk chunk = chunkProvider.provideChunk(x, z); + //#endif + if (!chunk.isEmpty()) { + List entitiesInChunk = new ArrayList<>(); + // Gather all entities in that chunk + for (Collection entityList : getEntityLists(chunk)) { + entitiesInChunk.addAll(entityList); + } + for (Entity entity : entitiesInChunk) { + // Skip interpolation of position updates coming from server + // (See: newX in EntityLivingBase or otherPlayerMPX in EntityOtherPlayerMP) + // Needs to be called at least 4 times thanks to + // EntityOtherPlayerMP#otherPlayerMPPosRotationIncrements (max vanilla value is 3) + for (int i = 0; i < 4; i++) { + //#if MC>=11300 + entity.tick(); + //#else + //$$ entity.onUpdate(); + //#endif + } + + // Check whether the entity has left the chunk + int chunkX = floor(entity.posX / 16); + int chunkZ = floor(entity.posZ / 16); + if (entity.chunkCoordX != chunkX || entity.chunkCoordZ != chunkZ) { + // Entity has left the chunk + chunk.removeEntityAtIndex(entity, entity.chunkCoordY); + //#if MC>=11300 + Chunk newChunk = chunkProvider.provideChunk(chunkX, chunkZ, false, false); + //#else + //#if MC>=10904 + //$$ Chunk newChunk = chunkProvider.getLoadedChunk(chunkX, chunkZ); + //#else + //$$ Chunk newChunk = chunkProvider.chunkExists(chunkX, chunkZ) + //$$ ? chunkProvider.provideChunk(chunkX, chunkZ) : null; + //#endif + //#endif + if (newChunk != null) { + newChunk.addEntity(entity); + } else { + // Entity has left all loaded chunks + entity.addedToChunk = false; + } + } else { + // When entities remain in a chunk that's to be unloaded, they'll only be added to a unload + // queue and remain loaded as before until the next tick (which during jumping is way off). + // So, if they are re-spawned with the same entity id, MC actually cleans up the old entity and + // then adds the new one but leaves the unload queue as is. + // Finally, on the next tick the legitimate entity will be unloaded because it's part of the + // unload queue (entities .equals based purely on their id). However, the old entity object + // is used to determine the chunk the entity is removed from and in this case that'll allow the + // legitimate entity to remain registered in a loaded chunk, causing them to still be rendered. + // + // The usual removal-due-to-chunk-unload process will, without touching the entityList, call + // onEntityRemoved. In that method WorldClient checks to see whether the entity is still in the + // entityList (which it is) and then adds it to the entitySpawnQueue. + // As the final result the entity will remain loaded. + // To get the same result without ticking, we just remove the entity from the to-be-unloaded + // chunk but keep it loaded otherwise. They won't be rendered because they're not part of any + // chunk and will be removed properly if the server decides to re-spawn the entity. + chunk.removeEntityAtIndex(entity, entity.chunkCoordY); + entity.addedToChunk = false; + } + } + } + } + return p; // During synchronous playback everything is sent normally + } + + private static final class PacketData { + private static final com.github.steveice10.netty.buffer.ByteBuf byteBuf = com.github.steveice10.netty.buffer.Unpooled.buffer(); + private static final StudioCodec codecLoginPhase = new StudioCodec(new StudioSession(new ReplayStudio(), false, true)); + private static final StudioCodec codecPlayPhase = new StudioCodec(new StudioSession(new ReplayStudio(), false, false)); + + private final int timestamp; + private final byte[] bytes; + + PacketData(ReplayInputStream in, boolean loginPhase) throws IOException { + com.replaymod.replaystudio.PacketData data = in.readPacket(); + timestamp = (int) data.getTime(); + // We need to re-encode MCProtocolLib packets, so we can later decode them as NMS packets + // The main reason we aren't reading them as NMS packets is that we want ReplayStudio to be able + // to apply ViaVersion (and potentially other magic) to it. + synchronized (byteBuf) { + byteBuf.markReaderIndex(); // Mark the current reader and writer index (should be at start) + byteBuf.markWriterIndex(); + + try { + (loginPhase ? codecLoginPhase : codecPlayPhase).encode(null, data.getPacket(), byteBuf); + } catch (Exception e) { + throw new IOException(e); + } + + bytes = new byte[byteBuf.readableBytes()]; // Create bytes array of sufficient size + byteBuf.readBytes(bytes); // Read all data into bytes + + byteBuf.resetReaderIndex(); // Reset reader & writer index for next use + byteBuf.resetWriterIndex(); + } + } + } +} diff --git a/src/main/java/com/replaymod/replay/InputReplayTimer.java b/src/main/java/com/replaymod/replay/InputReplayTimer.java index 4baa0f1c..4359da74 100644 --- a/src/main/java/com/replaymod/replay/InputReplayTimer.java +++ b/src/main/java/com/replaymod/replay/InputReplayTimer.java @@ -3,23 +3,25 @@ package com.replaymod.replay; import com.replaymod.core.utils.WrappedTimer; import com.replaymod.replay.camera.CameraController; import com.replaymod.replay.camera.CameraEntity; -import com.replaymod.replay.events.ReplayDispatchKeypressesEvent; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.settings.GameSettings; -import net.minecraft.client.settings.KeyBinding; -import net.minecraft.crash.CrashReport; -import net.minecraft.util.ReportedException; import net.minecraft.util.Timer; -import net.minecraftforge.client.ForgeHooksClient; -import net.minecraftforge.common.MinecraftForge; -import org.lwjgl.input.Keyboard; -import org.lwjgl.input.Mouse; +//#if MC>=11300 +import org.lwjgl.glfw.GLFW; +//#else +//$$ import com.replaymod.replay.events.ReplayDispatchKeypressesEvent; +//$$ import net.minecraft.client.gui.GuiScreen; +//$$ import net.minecraft.client.settings.GameSettings; +//$$ import net.minecraft.client.settings.KeyBinding; +//$$ import net.minecraft.crash.CrashReport; +//$$ import net.minecraftforge.client.ForgeHooksClient; +//$$ import net.minecraftforge.common.MinecraftForge; +//$$ import org.lwjgl.input.Keyboard; +//$$ import org.lwjgl.input.Mouse; //#if MC>=10800 -import net.minecraftforge.fml.common.FMLCommonHandler; - -import java.io.IOException; +//$$ import net.minecraftforge.fml.common.FMLCommonHandler; +//$$ +//$$ import java.io.IOException; //#else //$$ import cpw.mods.fml.common.FMLCommonHandler; //$$ import cpw.mods.fml.common.eventhandler.Event; @@ -27,6 +29,8 @@ import java.io.IOException; //$$ //$$ import static com.replaymod.core.versions.MCVer.FML_BUS; //#endif +//$$ import static com.replaymod.core.versions.MCVer.newReportedException; +//#endif public class InputReplayTimer extends WrappedTimer { private final ReplayModReplay mod; @@ -39,8 +43,13 @@ public class InputReplayTimer extends WrappedTimer { } @Override - public void updateTimer() { - super.updateTimer(); + //#if MC>=11300 + public void updateTimer(long sysClock) { + super.updateTimer(sysClock); + //#else + //$$ public void updateTimer() { + //$$ super.updateTimer(); + //#endif // 1.7.10: We have to run the scheduled executables (ours only) because MC would only run them every tick //#if MC<=10710 @@ -50,43 +59,41 @@ public class InputReplayTimer extends WrappedTimer { // If we are in a replay, we have to manually process key and mouse events as the // tick speed may vary or there may not be any ticks at all (when the replay is paused) if (mod.getReplayHandler() != null) { + //#if MC>=11300 if (mc.currentScreen == null || mc.currentScreen.allowUserInput) { - while (Mouse.next()) { - handleMouseEvent(); - } - - while (Keyboard.next()) { - handleKeyEvent(); - } - } else { + GLFW.glfwPollEvents(); + mc.processKeyBinds(); + } + mc.keyboardListener.tick(); + //#else + //$$ if (mc.currentScreen == null || mc.currentScreen.allowUserInput) { + //$$ while (Mouse.next()) { + //$$ handleMouseEvent(); + //$$ } + //$$ + //$$ while (Keyboard.next()) { + //$$ handleKeyEvent(); + //$$ } + //$$ } else { + //#if MC<11300 //#if MC>=10800 - try { - mc.currentScreen.handleInput(); - } catch (IOException e) { // *SIGH* - e.printStackTrace(); - } + //$$ try { + //$$ mc.currentScreen.handleInput(); + //$$ } catch (IOException e) { // *SIGH* + //$$ e.printStackTrace(); + //$$ } //#else //$$ mc.currentScreen.handleInput(); //#endif - } + //#endif + //$$ } + //#endif } } - protected void handleMouseEvent() { - if (ForgeHooksClient.postMouseEvent()) return; - - int button = Mouse.getEventButton() - 100; - boolean pressed = Mouse.getEventButtonState(); - - // Update key binding states - KeyBinding.setKeyBindState(button, pressed); - if (pressed) { - KeyBinding.onTick(button); - } - - int wheel = Mouse.getEventDWheel(); + public static void handleScroll(int wheel) { if (wheel != 0) { - ReplayHandler replayHandler = mod.getReplayHandler(); + ReplayHandler replayHandler = ReplayModReplay.instance.getReplayHandler(); if (replayHandler != null) { CameraEntity cameraEntity = replayHandler.getCameraEntity(); if (cameraEntity != null) { @@ -102,163 +109,181 @@ public class InputReplayTimer extends WrappedTimer { } } } + } - if (mc.currentScreen == null) { - if (!mc.inGameHasFocus && Mouse.getEventButtonState()) { - // Regrab mouse if the user clicks into the window - mc.setIngameFocus(); - } - } else { + //#if MC<11300 + //$$ protected void handleMouseEvent() { + //$$ if (ForgeHooksClient.postMouseEvent()) return; + //$$ + //$$ int button = Mouse.getEventButton() - 100; + //$$ boolean pressed = Mouse.getEventButtonState(); + //$$ + //$$ // Update key binding states + //$$ KeyBinding.setKeyBindState(button, pressed); + //$$ if (pressed) { + //$$ KeyBinding.onTick(button); + //$$ } + //$$ + //$$ int wheel = Mouse.getEventDWheel(); + //$$ handleScroll(wheel); + //$$ + //$$ if (mc.currentScreen == null) { + //$$ if (!mc.inGameHasFocus && Mouse.getEventButtonState()) { + //$$ // Regrab mouse if the user clicks into the window + //$$ mc.setIngameFocus(); + //$$ } + //$$ } else { //#if MC>=10800 - try { - mc.currentScreen.handleMouseInput(); - } catch (IOException e) { // WHO IS RESPONSIBLE FOR THIS MESS?!? - e.printStackTrace(); - } + //$$ try { + //$$ mc.currentScreen.handleMouseInput(); + //$$ } catch (IOException e) { // WHO IS RESPONSIBLE FOR THIS MESS?!? + //$$ e.printStackTrace(); + //$$ } //#else //$$ mc.currentScreen.handleMouseInput(); //#endif - } - - FMLCommonHandler.instance().fireMouseInput(); - } - - protected void handleKeyEvent() { - // TODO 1.7.10: This might be missing some 1.7.10-only key bindings or implement some of them incorrectly - int key = Keyboard.getEventKey() == 0 ? Keyboard.getEventCharacter() + 256 : Keyboard.getEventKey(); - boolean pressed = Keyboard.getEventKeyState(); - - KeyBinding.setKeyBindState(key, pressed); - if (pressed) { - KeyBinding.onTick(key); - } - - // Still want to be able to create debug crashes ]:D - if (mc.debugCrashKeyPressTime > 0) { - if (Minecraft.getSystemTime() - mc.debugCrashKeyPressTime >= 6000L) { - throw new ReportedException(new CrashReport("Manually triggered debug crash", new Throwable())); - } - - if (!Keyboard.isKeyDown(Keyboard.KEY_F3) || !Keyboard.isKeyDown(Keyboard.KEY_C)) { - mc.debugCrashKeyPressTime = -1; - } - } else if (Keyboard.isKeyDown(Keyboard.KEY_F3) && Keyboard.isKeyDown(Keyboard.KEY_C)) { - mc.debugCrashKeyPressTime = Minecraft.getSystemTime(); - } - - // Twitch, screenshot, fullscreen, etc. (stuff that works everywhere) - if (!MinecraftForge.EVENT_BUS.post(new ReplayDispatchKeypressesEvent.Pre())) { - mc.dispatchKeypresses(); - } - - if (pressed) { - // This might be subject to change as vanilla shaders are still kinda unused in 1.8 - if (key == Keyboard.KEY_F4 && mc.entityRenderer != null) { + //$$ } + //$$ + //$$ FMLCommonHandler.instance().fireMouseInput(); + //$$ } + //$$ + //$$ protected void handleKeyEvent() { + //$$ // TODO 1.7.10: This might be missing some 1.7.10-only key bindings or implement some of them incorrectly + //$$ int key = Keyboard.getEventKey() == 0 ? Keyboard.getEventCharacter() + 256 : Keyboard.getEventKey(); + //$$ boolean pressed = Keyboard.getEventKeyState(); + //$$ + //$$ KeyBinding.setKeyBindState(key, pressed); + //$$ if (pressed) { + //$$ KeyBinding.onTick(key); + //$$ } + //$$ + //$$ // Still want to be able to create debug crashes ]:D + //$$ if (mc.debugCrashKeyPressTime > 0) { + //$$ if (Minecraft.getSystemTime() - mc.debugCrashKeyPressTime >= 6000L) { + //$$ throw newReportedException(new CrashReport("Manually triggered debug crash", new Throwable())); + //$$ } + //$$ + //$$ if (!Keyboard.isKeyDown(Keyboard.KEY_F3) || !Keyboard.isKeyDown(Keyboard.KEY_C)) { + //$$ mc.debugCrashKeyPressTime = -1; + //$$ } + //$$ } else if (Keyboard.isKeyDown(Keyboard.KEY_F3) && Keyboard.isKeyDown(Keyboard.KEY_C)) { + //$$ mc.debugCrashKeyPressTime = Minecraft.getSystemTime(); + //$$ } + //$$ + //$$ // Twitch, screenshot, fullscreen, etc. (stuff that works everywhere) + //$$ if (!MinecraftForge.EVENT_BUS.post(new ReplayDispatchKeypressesEvent.Pre())) { + //$$ mc.dispatchKeypresses(); + //$$ } + //$$ + //$$ if (pressed) { + //$$ // This might be subject to change as vanilla shaders are still kinda unused in 1.8 + //$$ if (key == Keyboard.KEY_F4 && mc.entityRenderer != null) { //#if MC>=10800 - mc.entityRenderer.switchUseShader(); + //$$ mc.entityRenderer.switchUseShader(); //#else //$$ mc.entityRenderer.activateNextShader(); //#endif - } - - if (mc.currentScreen != null) { + //$$ } + //$$ + //$$ if (mc.currentScreen != null) { //#if MC>=10800 - try { - mc.currentScreen.handleKeyboardInput(); - } catch (IOException e) { // AND WHO THOUGHT THIS WAS A GREAT IDEA? - e.printStackTrace(); - } + //$$ try { + //$$ mc.currentScreen.handleKeyboardInput(); + //$$ } catch (IOException e) { // AND WHO THOUGHT THIS WAS A GREAT IDEA? + //$$ e.printStackTrace(); + //$$ } //#else //$$ mc.currentScreen.handleKeyboardInput(); //#endif - } else { - if (key == Keyboard.KEY_ESCAPE) { - mc.displayInGameMenu(); - } - - // Following are a ton of vanilla keyboard shortcuts, some are removed as they're useless in the - // replay viewer as of now - // TODO Update maybe add new key bindings - // TODO: Translate magic values to Keyboard.KEY_ constants - - if (key == 32 && Keyboard.isKeyDown(61) && mc.ingameGUI != null) { + //$$ } else { + //$$ if (key == Keyboard.KEY_ESCAPE) { + //$$ mc.displayInGameMenu(); + //$$ } + //$$ + //$$ // Following are a ton of vanilla keyboard shortcuts, some are removed as they're useless in the + //$$ // replay viewer as of now + //$$ // TODO Update maybe add new key bindings + //$$ // TODO: Translate magic values to Keyboard.KEY_ constants + //$$ + //$$ if (key == 32 && Keyboard.isKeyDown(61) && mc.ingameGUI != null) { //#if MC>=11100 - mc.ingameGUI.getChatGUI().clearChatMessages(false); + //$$ mc.ingameGUI.getChatGUI().clearChatMessages(false); //#else //$$ mc.ingameGUI.getChatGUI().clearChatMessages(); //#endif - } - - if (key == 31 && Keyboard.isKeyDown(61)) { - mc.refreshResources(); - } - - if (key == 20 && Keyboard.isKeyDown(61)) { - mc.refreshResources(); - } - - if (key == 33 && Keyboard.isKeyDown(61)) { - boolean flag1 = Keyboard.isKeyDown(42) | Keyboard.isKeyDown(54); - mc.gameSettings.setOptionValue(GameSettings.Options.RENDER_DISTANCE, flag1 ? -1 : 1); - } - - if (key == 30 && Keyboard.isKeyDown(61)) { - mc.renderGlobal.loadRenderers(); - } - - if (key == 48 && Keyboard.isKeyDown(61)) { + //$$ } + //$$ + //$$ if (key == 31 && Keyboard.isKeyDown(61)) { + //$$ mc.refreshResources(); + //$$ } + //$$ + //$$ if (key == 20 && Keyboard.isKeyDown(61)) { + //$$ mc.refreshResources(); + //$$ } + //$$ + //$$ if (key == 33 && Keyboard.isKeyDown(61)) { + //$$ boolean flag1 = Keyboard.isKeyDown(42) | Keyboard.isKeyDown(54); + //$$ mc.gameSettings.setOptionValue(GameSettings.Options.RENDER_DISTANCE, flag1 ? -1 : 1); + //$$ } + //$$ + //$$ if (key == 30 && Keyboard.isKeyDown(61)) { + //$$ mc.renderGlobal.loadRenderers(); + //$$ } + //$$ + //$$ if (key == 48 && Keyboard.isKeyDown(61)) { //#if MC>=10800 - mc.getRenderManager().setDebugBoundingBox(!mc.getRenderManager().isDebugBoundingBox()); + //$$ mc.getRenderManager().setDebugBoundingBox(!mc.getRenderManager().isDebugBoundingBox()); //#else //$$ RenderManager.debugBoundingBox = !RenderManager.debugBoundingBox; //#endif - } - - if (key == 25 && Keyboard.isKeyDown(61)) { - mc.gameSettings.pauseOnLostFocus = !mc.gameSettings.pauseOnLostFocus; - mc.gameSettings.saveOptions(); - } - - if (key == 59) { - mc.gameSettings.hideGUI = !mc.gameSettings.hideGUI; - } - - if (key == 61) { - mc.gameSettings.showDebugInfo = !mc.gameSettings.showDebugInfo; - mc.gameSettings.showDebugProfilerChart = GuiScreen.isShiftKeyDown(); - } - - if (mc.gameSettings.keyBindTogglePerspective.isPressed()) { - mc.gameSettings.thirdPersonView = (mc.gameSettings.thirdPersonView + 1) % 3; - + //$$ } + //$$ + //$$ if (key == 25 && Keyboard.isKeyDown(61)) { + //$$ mc.gameSettings.pauseOnLostFocus = !mc.gameSettings.pauseOnLostFocus; + //$$ mc.gameSettings.saveOptions(); + //$$ } + //$$ + //$$ if (key == 59) { + //$$ mc.gameSettings.hideGUI = !mc.gameSettings.hideGUI; + //$$ } + //$$ + //$$ if (key == 61) { + //$$ mc.gameSettings.showDebugInfo = !mc.gameSettings.showDebugInfo; + //$$ mc.gameSettings.showDebugProfilerChart = GuiScreen.isShiftKeyDown(); + //$$ } + //$$ + //$$ if (mc.gameSettings.keyBindTogglePerspective.isPressed()) { + //$$ mc.gameSettings.thirdPersonView = (mc.gameSettings.thirdPersonView + 1) % 3; + //$$ //#if MC>=10800 - if (mc.entityRenderer != null) { // Extra check, not in vanilla code - if (mc.gameSettings.thirdPersonView == 0) { - mc.entityRenderer.loadEntityShader(mc.getRenderViewEntity()); - } else if (mc.gameSettings.thirdPersonView == 1) { - mc.entityRenderer.loadEntityShader(null); - } - } + //$$ if (mc.entityRenderer != null) { // Extra check, not in vanilla code + //$$ if (mc.gameSettings.thirdPersonView == 0) { + //$$ mc.entityRenderer.loadEntityShader(mc.getRenderViewEntity()); + //$$ } else if (mc.gameSettings.thirdPersonView == 1) { + //$$ mc.entityRenderer.loadEntityShader(null); + //$$ } + //$$ } //#endif - } - } - - // Navigation in the debug chart - if (mc.gameSettings.showDebugInfo && mc.gameSettings.showDebugProfilerChart) { - if (key == Keyboard.KEY_0) { - mc.updateDebugProfilerName(0); - } - - for (int i = 0; i < 9; ++i) { - if (key == 2 + i) { - mc.updateDebugProfilerName(i + 1); - } - } - } - } - - FMLCommonHandler.instance().fireKeyInput(); - } + //$$ } + //$$ } + //$$ + //$$ // Navigation in the debug chart + //$$ if (mc.gameSettings.showDebugInfo && mc.gameSettings.showDebugProfilerChart) { + //$$ if (key == Keyboard.KEY_0) { + //$$ mc.updateDebugProfilerName(0); + //$$ } + //$$ + //$$ for (int i = 0; i < 9; ++i) { + //$$ if (key == 2 + i) { + //$$ mc.updateDebugProfilerName(i + 1); + //$$ } + //$$ } + //$$ } + //$$ } + //$$ + //$$ FMLCommonHandler.instance().fireKeyInput(); + //$$ } + //#endif //#if MC<=10710 //$$ public static class RunScheduledTasks extends Event {} diff --git a/src/main/java/com/replaymod/replay/NoGuiScreenshot.java b/src/main/java/com/replaymod/replay/NoGuiScreenshot.java index 88a8cc3a..d2c38ec3 100644 --- a/src/main/java/com/replaymod/replay/NoGuiScreenshot.java +++ b/src/main/java/com/replaymod/replay/NoGuiScreenshot.java @@ -38,6 +38,12 @@ public class NoGuiScreenshot { return; } + //#if MC>=11300 + int frameWidth = mc.mainWindow.getFramebufferWidth(), frameHeight = mc.mainWindow.getFramebufferHeight(); + //#else + //$$ int frameWidth = mc.displayWidth, frameHeight = mc.displayHeight; + //#endif + final boolean guiHidden = mc.gameSettings.hideGUI; try { mc.gameSettings.hideGUI = true; @@ -48,16 +54,20 @@ public class NoGuiScreenshot { mc.getFramebuffer().bindFramebuffer(true); GlStateManager.enableTexture2D(); + //#if MC>=11300 + mc.entityRenderer.renderWorld(mc.timer.renderPartialTicks, System.nanoTime()); + //#else //#if MC>=10809 - mc.entityRenderer.updateCameraAndRender(mc.timer.renderPartialTicks, System.nanoTime()); + //$$ mc.entityRenderer.updateCameraAndRender(mc.timer.renderPartialTicks, System.nanoTime()); //#else //$$ mc.entityRenderer.updateCameraAndRender(mc.timer.renderPartialTicks); //#endif + //#endif mc.getFramebuffer().unbindFramebuffer(); GlStateManager.popMatrix(); GlStateManager.pushMatrix(); - mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight); + mc.getFramebuffer().framebufferRender(frameWidth, frameHeight); GlStateManager.popMatrix(); } catch (Throwable t) { future.setException(t); @@ -73,7 +83,12 @@ public class NoGuiScreenshot { // disk for better maintainability File tmpFolder = Files.createTempDir(); try { - ScreenShotHelper.saveScreenshot(tmpFolder, "tmp", mc.displayWidth, mc.displayHeight, mc.getFramebuffer()); + //#if MC>=11300 + ScreenShotHelper.saveScreenshot(tmpFolder, "tmp", frameWidth, frameHeight, mc.getFramebuffer(), (msg) -> + mc.addScheduledTask(() -> mc.ingameGUI.getChatGUI().printChatMessage(msg))); + //#else + //$$ ScreenShotHelper.saveScreenshot(tmpFolder, "tmp", frameWidth, frameHeight, mc.getFramebuffer()); + //#endif File screenshotFile = new File(tmpFolder, "screenshots/tmp"); BufferedImage image = ImageIO.read(screenshotFile); int imageWidth = image.getWidth(); diff --git a/src/main/java/com/replaymod/replay/QuickReplaySender.java b/src/main/java/com/replaymod/replay/QuickReplaySender.java new file mode 100644 index 00000000..2e4f62b2 --- /dev/null +++ b/src/main/java/com/replaymod/replay/QuickReplaySender.java @@ -0,0 +1,1145 @@ +//#if MC>=10904 +package com.replaymod.replay; + +import com.github.steveice10.mc.auth.data.GameProfile; +import com.github.steveice10.mc.protocol.data.game.PlayerListEntry; +import com.github.steveice10.mc.protocol.data.game.PlayerListEntryAction; +import com.github.steveice10.mc.protocol.data.game.chunk.BlockStorage; +import com.github.steveice10.mc.protocol.data.game.chunk.Column; +import com.github.steveice10.mc.protocol.data.game.entity.metadata.Position; +import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode; +import com.github.steveice10.mc.protocol.data.game.setting.Difficulty; +import com.github.steveice10.mc.protocol.data.game.world.WorldType; +import com.github.steveice10.mc.protocol.data.game.world.block.BlockChangeRecord; +import com.github.steveice10.mc.protocol.data.game.world.block.BlockState; +import com.github.steveice10.mc.protocol.data.game.world.notify.ClientNotification; +import com.github.steveice10.mc.protocol.packet.ingame.server.ServerPlayerListEntryPacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.ServerRespawnPacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.entity.ServerEntityDestroyPacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.entity.ServerEntityHeadLookPacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.entity.ServerEntityTeleportPacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.entity.player.ServerPlayerPositionRotationPacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.entity.spawn.ServerSpawnMobPacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.entity.spawn.ServerSpawnObjectPacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.entity.spawn.ServerSpawnPaintingPacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.entity.spawn.ServerSpawnPlayerPacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerBlockChangePacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerChunkDataPacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerMultiBlockChangePacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerNotifyClientPacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerUnloadChunkPacket; +import com.github.steveice10.mc.protocol.packet.ingame.server.world.ServerUpdateTimePacket; +import com.github.steveice10.mc.protocol.packet.login.server.LoginSuccessPacket; +import com.github.steveice10.mc.protocol.util.NetUtil; +import com.github.steveice10.packetlib.io.NetInput; +import com.github.steveice10.packetlib.io.NetOutput; +import com.github.steveice10.packetlib.io.stream.StreamNetInput; +import com.github.steveice10.packetlib.io.stream.StreamNetOutput; +import com.github.steveice10.packetlib.tcp.io.ByteBufNetInput; +import com.github.steveice10.packetlib.tcp.io.ByteBufNetOutput; +import com.google.common.base.Optional; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Multimaps; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import com.replaymod.core.utils.Utils; +import com.replaymod.replaystudio.PacketData; +import com.replaymod.replaystudio.io.ReplayInputStream; +import com.replaymod.replaystudio.io.ReplayOutputStream; +import com.replaymod.replaystudio.replay.ReplayFile; +import com.replaymod.replaystudio.studio.ReplayStudio; +import com.replaymod.replaystudio.util.Location; +import com.replaymod.replaystudio.util.PacketUtils; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerAdapter; +import io.netty.channel.ChannelHandlerContext; +import lombok.SneakyThrows; +import net.minecraft.client.Minecraft; +import net.minecraft.network.EnumConnectionState; +import net.minecraft.network.EnumPacketDirection; +import net.minecraft.network.Packet; +import net.minecraft.network.PacketBuffer; +import net.minecraftforge.fml.common.gameevent.TickEvent; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif + +//#if MC>=11200 +import com.replaymod.core.utils.WrappedTimer; +//#endif + +import static com.replaymod.core.versions.MCVer.FML_BUS; +import static com.replaymod.core.versions.MCVer.getMinecraft; +import static com.replaymod.replay.ReplayModReplay.LOGGER; + +/** + * Sends only chunk updates and entity position updates but tries to do so as quickly as possible. + * To do so, it performs an initial analysis of the replay, scanning all of its packets and storing entity positions + * and chunk states while doing so. + * This allows it to later jump to any time by doing a diff from the current time (including backwards jumping). + */ +@ChannelHandler.Sharable +public class QuickReplaySender extends ChannelHandlerAdapter implements ReplaySender { + private static final String CACHE_ENTRY = "quickModeCache.bin"; + private static final String CACHE_INDEX_ENTRY = "quickModeCacheIndex.bin"; + private static final int CACHE_VERSION = 0; + + private final Minecraft mc = getMinecraft(); + + private final ReplayModReplay mod; + private final ReplayFile replayFile; + private ChannelHandlerContext ctx; + + private int currentTimeStamp; + private double replaySpeed = 1; + + /** + * Whether async mode is enabled. + * Async mode is emulated by registering an event handler on client tick. + */ + private boolean asyncMode; + private long lastAsyncUpdateTime; + + private ListenableFuture initPromise; + + private com.github.steveice10.netty.buffer.ByteBuf buf; + private NetInput bufInput; + private TreeMap> thingSpawnsT = new TreeMap<>(); + private ListMultimap thingSpawns = Multimaps.newListMultimap(thingSpawnsT, ArrayList::new); + private TreeMap> thingDespawnsT = new TreeMap<>(); + private ListMultimap thingDespawns = Multimaps.newListMultimap(thingDespawnsT, ArrayList::new); + private List activeThings = new LinkedList<>(); + private TreeMap> worldTimes = new TreeMap<>(); + private TreeMap> thunderStrengths = new TreeMap<>(); // For some reason, this isn't tied to Weather + + public QuickReplaySender(ReplayModReplay mod, ReplayFile replayFile) { + this.mod = mod; + this.replayFile = replayFile; + } + + public void register() { + FML_BUS.register(this); + } + + public void unregister() { + FML_BUS.unregister(this); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + this.ctx = ctx; + } + + public ListenableFuture getInitializationPromise() { + return initPromise; + } + + public ListenableFuture initialize(Consumer progress) { + if (initPromise != null) { + return initPromise; + } + SettableFuture promise = SettableFuture.create(); + initPromise = promise; + new Thread(() -> { + try { + long start = System.currentTimeMillis(); + if (!tryLoadFromCache(progress)) { + double progressSplit = 0.9; // 90% of progress time for analysing, 10% for loading + analyseReplay(replayFile, d -> progress.accept(d * progressSplit)); + tryLoadFromCache(d -> progress.accept(d * (1 - progressSplit) + progressSplit)); + } + LOGGER.info("Initialized quick replay sender in " + (System.currentTimeMillis() - start) + "ms"); + } catch (Throwable e) { + LOGGER.error("Initializing quick replay sender:", e); + mod.getCore().runLater(() -> { + mod.getCore().printWarningToChat("Error initializing quick replay sender: %s", e.getLocalizedMessage()); + promise.setException(e); + }); + return; + } + mod.getCore().runLater(() -> promise.set(null)); + }).start(); + return promise; + } + + private void ensureInitialized(Runnable body) { + if (initPromise == null) { + LOGGER.warn("QuickReplaySender used without prior initialization!", new Throwable()); + initialize(progress -> {}); + } + Futures.addCallback(initPromise, new FutureCallback() { + @Override + public void onSuccess(@Nullable Void result) { + body.run(); + } + + @Override + public void onFailure(Throwable t) { + // Error already printed by initialize method + } + }); + } + + public void restart() { + activeThings.clear(); + currentTimeStamp = 0; + ctx.fireChannelRead(toMC(new LoginSuccessPacket(new GameProfile(UUID.nameUUIDFromBytes(new byte[0]), "Player")), EnumConnectionState.LOGIN)); + ctx.fireChannelRead(toMC(new ServerRespawnPacket(0, Difficulty.NORMAL, GameMode.SPECTATOR, WorldType.DEFAULT))); + ctx.fireChannelRead(toMC(new ServerPlayerPositionRotationPacket(0, 0, 0, 0, 0, 0))); + } + + @Override + public int currentTimeStamp() { + return currentTimeStamp; + } + + @Override + public void setReplaySpeed(double factor) { + if (factor != 0) { + if (paused() && asyncMode) { + lastAsyncUpdateTime = System.currentTimeMillis(); + } + this.replaySpeed = factor; + } + //#if MC>=11200 + mc.timer.tickLength = WrappedTimer.DEFAULT_MS_PER_TICK / (float) factor; + //#else + //$$ mc.timer.timerSpeed = (float) factor; + //#endif + } + + @Override + public double getReplaySpeed() { + return replaySpeed; + } + + @Override + public boolean isAsyncMode() { + return asyncMode; + } + + @Override + public void setAsyncMode(boolean async) { + if (this.asyncMode == async) return; + ensureInitialized(() -> { + this.asyncMode = async; + if (async) { + lastAsyncUpdateTime = System.currentTimeMillis(); + } + }); + } + + @Override + public void setSyncModeAndWait() { + setAsyncMode(false); + // No waiting required, we emulated async mode via tick events + } + + @Override + public void jumpToTime(int value) { + sendPacketsTill(value); + } + + @SubscribeEvent + public void onTick(TickEvent.ClientTickEvent event) { + if (event.phase != TickEvent.Phase.START) return; + if (!asyncMode) return; + + long now = System.currentTimeMillis(); + long realTimePassed = now - lastAsyncUpdateTime; + lastAsyncUpdateTime = now; + int replayTimePassed = (int) (realTimePassed * replaySpeed); + sendPacketsTill(currentTimeStamp + replayTimePassed); + } + + private boolean tryLoadFromCache(Consumer progress) throws IOException { + boolean success = false; + + Optional cacheIndexOpt = replayFile.getCache(CACHE_INDEX_ENTRY); + if (!cacheIndexOpt.isPresent()) return false; + try (InputStream indexIn = cacheIndexOpt.get()) { + Optional cacheOpt = replayFile.getCache(CACHE_ENTRY); + if (!cacheOpt.isPresent()) return false; + try (InputStream cacheIn = cacheOpt.get()) { + success = loadFromCache(cacheIn, indexIn, progress); + } + } finally { + if (!success) { + buf = null; + bufInput = null; + thingSpawnsT.clear(); + thingDespawnsT.clear(); + worldTimes.clear(); + thunderStrengths.clear(); + } + } + + return success; + } + + private boolean loadFromCache(InputStream cacheIn, InputStream rawIndexIn, Consumer progress) throws IOException { + long sysTimeStart = System.currentTimeMillis(); + + NetInput in = new StreamNetInput(rawIndexIn); + if (in.readVarInt() != CACHE_VERSION) return false; // Incompatible cache version + if (new StreamNetInput(cacheIn).readVarInt() != CACHE_VERSION) return false; // Incompatible cache version + + things: while (true) { + BakedTrackedThing trackedThing; + switch (in.readVarInt()) { + case 0: break things; + case 1: trackedThing = new BakedEntity(in); break; + case 2: trackedThing = new BakedChunk(in); break; + case 3: trackedThing = new BakedWeather(in); break; + default: return false; + } + thingSpawns.put(trackedThing.spawnTime, trackedThing); + thingDespawns.put(trackedThing.despawnTime, trackedThing); + } + + readFromCache(in, worldTimes); + readFromCache(in, thunderStrengths); + int size = in.readVarInt(); + + LOGGER.info("Creating quick mode buffer of size: {}KB", size / 1024); + buf = com.github.steveice10.netty.buffer.Unpooled.buffer(size); + int read = 0; + while (true) { + int len = buf.writeBytes(cacheIn, Math.min(size - read, 4096)); + if (len <= 0) break; + read += len; + progress.accept((double) read / size); + } + bufInput = new ByteBufNetInput(buf); + + LOGGER.info("Loaded quick replay from cache in " + (System.currentTimeMillis() - sysTimeStart) + "ms"); + return true; + } + + private static void analyseReplay(ReplayFile replayFile, Consumer progress) throws IOException { + ReplayStudio studio = new ReplayStudio(); + PacketUtils.registerAllMovementRelated(studio); + studio.setParsing(ServerSpawnMobPacket.class, true); + studio.setParsing(ServerSpawnObjectPacket.class, true); + studio.setParsing(ServerSpawnPaintingPacket.class, true); + studio.setParsing(ServerSpawnPlayerPacket.class, true); + studio.setParsing(ServerEntityDestroyPacket.class, true); + studio.setParsing(ServerChunkDataPacket.class, true); + studio.setParsing(ServerUnloadChunkPacket.class, true); + studio.setParsing(ServerBlockChangePacket.class, true); + studio.setParsing(ServerMultiBlockChangePacket.class, true); + studio.setParsing(ServerPlayerListEntryPacket.class, true); + studio.setParsing(ServerUpdateTimePacket.class, true); + studio.setParsing(ServerNotifyClientPacket.class, true); + + TreeMap worldTimes = new TreeMap<>(); + TreeMap thunderStrengths = new TreeMap<>(); + Map playerListEntries = new HashMap<>(); + Map activeEntities = new HashMap<>(); + Map activeChunks = new HashMap<>(); + Weather activeWeather = null; + + double sysTimeStart = System.currentTimeMillis(); + double duration; + try (ReplayInputStream in = replayFile.getPacketData(studio); + OutputStream cacheOut = replayFile.writeCache(CACHE_ENTRY); + OutputStream cacheIndexOut = replayFile.writeCache(CACHE_INDEX_ENTRY)) { + NetOutput out = new StreamNetOutput(cacheOut); + out.writeVarInt(CACHE_VERSION); + NetOutput indexOut = new StreamNetOutput(cacheIndexOut); + indexOut.writeVarInt(CACHE_VERSION); + + int index = 0; + int time = 0; + duration = replayFile.getMetaData().getDuration(); + PacketData packetData; + while ((packetData = in.readPacket()) != null) { + com.github.steveice10.packetlib.packet.Packet packet = packetData.getPacket(); + time = (int) packetData.getTime(); + progress.accept(time / duration); + Integer entityId = PacketUtils.getEntityId(packet); + if (packet instanceof ServerSpawnMobPacket + || packet instanceof ServerSpawnObjectPacket + || packet instanceof ServerSpawnPaintingPacket) { + Entity entity = new Entity(entityId, Collections.singletonList(packet)); + entity.spawnTime = time; + Entity prev = activeEntities.put(entityId, entity); + if (prev != null) { + index = prev.writeToCache(indexOut, out, time, index); + } + } else if (packet instanceof ServerSpawnPlayerPacket) { + ServerPlayerListEntryPacket listEntryPacket = new ServerPlayerListEntryPacket( + PlayerListEntryAction.ADD_PLAYER, + new PlayerListEntry[]{ + playerListEntries.get(((ServerSpawnPlayerPacket) packet).getUUID()) + } + ); + Entity entity = new Entity(entityId, Arrays.asList(listEntryPacket, packet)); + entity.spawnTime = time; + Entity prev = activeEntities.put(entityId, entity); + if (prev != null) { + index = prev.writeToCache(indexOut, out, time, index); + } + } else if (packet instanceof ServerEntityDestroyPacket) { + for (int id : ((ServerEntityDestroyPacket) packet).getEntityIds()) { + Entity entity = activeEntities.remove(id); + if (entity != null) { + index = entity.writeToCache(indexOut, out, time, index); + } + } + } else if (packet instanceof ServerChunkDataPacket) { + Column column = ((ServerChunkDataPacket) packet).getColumn(); + if (column.hasBiomeData()) { + Chunk chunk = new Chunk(column); + chunk.spawnTime = time; + Chunk prev = activeChunks.put(coordToLong(column.getX(), column.getZ()), chunk); + if (prev != null) { + index = prev.writeToCache(indexOut, out, time, index); + } + } else { + Chunk chunk = activeChunks.get(coordToLong(column.getX(), column.getZ())); + if (chunk != null) { + int sectionY = 0; + for (com.github.steveice10.mc.protocol.data.game.chunk.Chunk section : column.getChunks()) { + if (section == null) { + sectionY++; + continue; + } + BlockStorage toBlocks = section.getBlocks(); + BlockStorage fromBlocks = chunk.currentBlockState[sectionY]; + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + BlockState fromState = fromBlocks.get(x, y, z); + BlockState toState = toBlocks.get(x, y, z); + if (!fromState.equals(toState)) { + Position pos = new Position(column.getX() << 4 | x, sectionY << 4 | y, column.getZ() << 4 | z); + chunk.blocks.put(time, new BlockChange(pos, fromState, toState)); + } + } + } + } + chunk.currentBlockState[sectionY] = toBlocks; + sectionY++; + } + } + } + } else if (packet instanceof ServerUnloadChunkPacket) { + ServerUnloadChunkPacket p = (ServerUnloadChunkPacket) packet; + Chunk prev = activeChunks.remove(coordToLong(p.getX(), p.getZ())); + if (prev != null) { + index = prev.writeToCache(indexOut, out, time, index); + } + } else if (packet instanceof ServerBlockChangePacket || packet instanceof ServerMultiBlockChangePacket) { + for (BlockChangeRecord record : + packet instanceof ServerBlockChangePacket + ? new BlockChangeRecord[]{ ((ServerBlockChangePacket) packet).getRecord() } + : ((ServerMultiBlockChangePacket) packet).getRecords()) { + Position pos = record.getPosition(); + Chunk chunk = activeChunks.get(coordToLong(pos.getX() >> 4, pos.getZ() >> 4)); + if (chunk != null) { + BlockStorage blockStorage = chunk.currentBlockState[pos.getY() >> 4]; + int x = pos.getX() & 15, y = pos.getY() & 15, z = pos.getZ() & 15; + BlockState prevState = blockStorage.get(x, y, z); + BlockState newState = record.getBlock(); + blockStorage.set(x, y, z, newState); + chunk.blocks.put(time, new BlockChange(pos, prevState, newState)); + } + } + } else if (packet instanceof ServerPlayerListEntryPacket) { + ServerPlayerListEntryPacket p = (ServerPlayerListEntryPacket) packet; + if (p.getAction() == PlayerListEntryAction.ADD_PLAYER) { + for (PlayerListEntry entry : p.getEntries()) { + playerListEntries.put(entry.getProfile().getId(), entry); + } + } + } else if (packet instanceof ServerRespawnPacket) { + for (Entity entity : activeEntities.values()) { + index = entity.writeToCache(indexOut, out, time, index); + } + activeEntities.clear(); + for (Chunk chunk : activeChunks.values()) { + index = chunk.writeToCache(indexOut, out, time, index); + } + activeChunks.clear(); + if (activeWeather != null) { + index = activeWeather.writeToCache(indexOut, out, time, index); + } + activeWeather = null; + } else if (packet instanceof ServerUpdateTimePacket) { + worldTimes.put(time, packet); + } else if (packet instanceof ServerNotifyClientPacket) { + ServerNotifyClientPacket p = (ServerNotifyClientPacket) packet; + switch (p.getNotification()) { + case START_RAIN: + if (activeWeather != null) { + index = activeWeather.writeToCache(indexOut, out, time, index); + } + activeWeather = new Weather(); + activeWeather.spawnTime = time; + break; + case STOP_RAIN: + if (activeWeather != null) { + index = activeWeather.writeToCache(indexOut, out, time, index); + activeWeather = null; + } + break; + case RAIN_STRENGTH: + if (activeWeather != null) { + activeWeather.rainStrengths.put(time, packet); + } + break; + case THUNDER_STRENGTH: + thunderStrengths.put(time, packet); + break; + default: break; + } + } + if (entityId != null) { + Entity entity = activeEntities.get(entityId); + if (entity != null) { + Location current = entity.locations.isEmpty() ? null : entity.locations.lastEntry().getValue(); + Location updated = PacketUtils.updateLocation(current, packet); + if (updated != null) { + entity.locations.put(time, updated); + } + } + } + } + + for (Entity entity : activeEntities.values()) { + index = entity.writeToCache(indexOut, out, time, index); + } + for (Chunk chunk : activeChunks.values()) { + index = chunk.writeToCache(indexOut, out, time, index); + } + if (activeWeather != null) { + index = activeWeather.writeToCache(indexOut, out, time, index); + } + + indexOut.writeByte(0); + writeToCache(indexOut, worldTimes); + writeToCache(indexOut, thunderStrengths); + + indexOut.writeVarInt(index); + } + LOGGER.info("Analysed replay in " + (System.currentTimeMillis() - sysTimeStart) + "ms"); + } + + @Override + public void sendPacketsTill(int replayTime) { + ensureInitialized(() -> { + if (replayTime > currentTimeStamp) { + activeThings.removeIf(thing -> { + if (thing.despawnTime <= replayTime) { + thing.despawn(this, ctx::fireChannelRead); + return true; + } else { + return false; + } + }); + thingSpawnsT.subMap(currentTimeStamp, false, replayTime, true).values() + .forEach(things -> things.forEach(thing -> { + if (thing.despawnTime > replayTime) { + thing.spawn(this, ctx::fireChannelRead); + activeThings.add(thing); + } + })); + activeThings.forEach(thing -> thing.play(currentTimeStamp, replayTime, ctx::fireChannelRead)); + playMap(worldTimes, currentTimeStamp, replayTime, ctx::fireChannelRead); + playMap(thunderStrengths, currentTimeStamp, replayTime, ctx::fireChannelRead); + } else { + activeThings.removeIf(thing -> { + if (thing.spawnTime > replayTime) { + thing.despawn(this, ctx::fireChannelRead); + return true; + } else { + return false; + } + }); + thingDespawnsT.subMap(replayTime, false, currentTimeStamp, true).values() + .forEach(things -> things.forEach(thing -> { + if (thing.spawnTime <= replayTime) { + thing.spawn(this, ctx::fireChannelRead); + activeThings.add(thing); + } + })); + activeThings.forEach(thing -> thing.rewind(currentTimeStamp, replayTime, ctx::fireChannelRead)); + rewindMap(worldTimes, currentTimeStamp, replayTime, ctx::fireChannelRead); + rewindMap(thunderStrengths, currentTimeStamp, replayTime, ctx::fireChannelRead); + } + currentTimeStamp = replayTime; + }); + } + + private static final com.github.steveice10.netty.buffer.ByteBuf mcplByteBuf = com.github.steveice10.netty.buffer.Unpooled.buffer(); + private static final ByteBufNetInput byteBufNetInput = new ByteBufNetInput(mcplByteBuf); + private static final ByteBufNetOutput byteBufNetOutput = new ByteBufNetOutput(mcplByteBuf); + + private static BlockStorage copy(BlockStorage of) { + int readerIndex = byteBuf.readerIndex(); // Mark the current reader and writer index (should be at start) + int writerIndex = byteBuf.writerIndex(); + try { + of.write(byteBufNetOutput); + return new BlockStorage(byteBufNetInput); + } catch (Exception e) { + Utils.throwIfUnchecked(e); + throw new RuntimeException(e); + } finally { + byteBuf.readerIndex(readerIndex); // Reset reader & writer index for next use + byteBuf.writerIndex(writerIndex); + } + } + + private static final ByteBuf byteBuf = Unpooled.buffer(); + private static final ByteBufOutputStream byteBufOut = new ByteBufOutputStream(byteBuf); + private static final PacketBuffer packetBuf = new PacketBuffer(byteBuf); + private static final ReplayOutputStream encoder = new ReplayOutputStream(new ReplayStudio(), byteBufOut); + private static final Inflater inflater = new Inflater(); + private static final Deflater deflater = new Deflater(); + + private static Packet toMC(com.github.steveice10.packetlib.packet.Packet packet) { + return toMC(packet, EnumConnectionState.PLAY); + } + + private static Packet toMC(com.github.steveice10.packetlib.packet.Packet packet, EnumConnectionState state) { + // We need to re-encode MCProtocolLib packets, so we can then decode them as NMS packets + // The main reason we aren't reading them as NMS packets is that we want ReplayStudio to be able + // to apply ViaVersion (and potentially other magic) to it. + synchronized (encoder) { + int readerIndex = byteBuf.readerIndex(); // Mark the current reader and writer index (should be at start) + int writerIndex = byteBuf.writerIndex(); + try { + encoder.write(0, packet); // Re-encode packet, data will end up in byteBuf + encoder.flush(); + + byteBuf.skipBytes(8); // Skip packet length & timestamp + + int packetId = + //#if MC>=11102 + packetBuf.readVarInt(); + //#else + //$$ packetBuf.readVarIntFromBuffer(); + //#endif + Packet mcPacket = state.getPacket(EnumPacketDirection.CLIENTBOUND, packetId); + mcPacket.readPacketData(packetBuf); + return mcPacket; + } catch (Exception e) { + Utils.throwIfUnchecked(e); + throw new RuntimeException(e); + } finally { + byteBuf.readerIndex(readerIndex); // Reset reader & writer index for next use + byteBuf.writerIndex(writerIndex); + } + } + } + + private static Packet readPacketFromCache(NetInput in) throws IOException { + int readerIndex = byteBuf.readerIndex(); // Mark the current reader and writer index (should be at start) + int writerIndex = byteBuf.writerIndex(); + try { + int prefix = in.readVarInt(); + int len = prefix >> 1; + if ((prefix & 1) == 1) { + int fullLen = in.readVarInt(); + byteBuf.writeBytes(in.readBytes(len)); + byteBuf.capacity(byteBuf.writerIndex() + fullLen); + + inflater.setInput(byteBuf.array(), byteBuf.arrayOffset() + byteBuf.readerIndex(), len); + inflater.inflate(byteBuf.array(), byteBuf.arrayOffset() + byteBuf.writerIndex(), fullLen); + + byteBuf.readerIndex(byteBuf.readerIndex() + len); + byteBuf.writerIndex(byteBuf.writerIndex() + fullLen); + } else { + byteBuf.writeBytes(in.readBytes(len)); + } + + int packetId = + //#if MC>=11102 + packetBuf.readVarInt(); + //#else + //$$ packetBuf.readVarIntFromBuffer(); + //#endif + Packet mcPacket = EnumConnectionState.PLAY.getPacket(EnumPacketDirection.CLIENTBOUND, packetId); + mcPacket.readPacketData(packetBuf); + return mcPacket; + } catch (Exception e) { + Utils.throwIfInstanceOf(e, IOException.class); + Utils.throwIfUnchecked(e); + throw new RuntimeException(e); + } finally { + byteBuf.readerIndex(readerIndex); // Reset reader & writer index for next use + byteBuf.writerIndex(writerIndex); + inflater.reset(); + } + } + + @SneakyThrows(IOException.class) + private static List> readPacketsFromCache(NetInput in) { + int size = in.readVarInt(); + List> packets = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + packets.add(readPacketFromCache(in)); + } + return packets; + } + + private static void readFromCache(NetInput in, SortedMap> packets) throws IOException { + int time = 0; + for (int i = in.readVarInt(); i > 0; i--) { + time += in.readVarInt(); + packets.put(time, readPacketFromCache(in)); + } + } + + private static int writeToCache(NetOutput out, com.github.steveice10.packetlib.packet.Packet packet) throws IOException { + int readerIndex = byteBuf.readerIndex(); // Mark the current reader and writer index (should be at start) + int writerIndex = byteBuf.writerIndex(); + try { + encoder.write(0, packet); // Re-encode packet, data will end up in byteBuf + encoder.flush(); + + byteBuf.skipBytes(8); // Skip packet length & timestamp + + int rawIndex = byteBuf.readerIndex(); + int size = byteBuf.readableBytes(); + + byteBuf.ensureWritable(size); + deflater.setInput(byteBuf.array(), byteBuf.arrayOffset() + byteBuf.readerIndex(), size); + deflater.finish(); + int compressedSize = 0; + while (!deflater.finished() && compressedSize < size) { + compressedSize += deflater.deflate( + byteBuf.array(), + byteBuf.arrayOffset() + byteBuf.writerIndex() + compressedSize, + size - compressedSize + ); + } + + int len = 0; + if (compressedSize < size) { + byteBuf.readerIndex(rawIndex + size); + byteBuf.writerIndex(rawIndex + size + compressedSize); + len += writeVarInt(out, compressedSize << 1 | 1); + len += writeVarInt(out, size); + } else { + byteBuf.readerIndex(rawIndex); + byteBuf.writerIndex(rawIndex + size); + len += writeVarInt(out, size << 1); + } + while (byteBuf.isReadable()) { + out.writeByte(byteBuf.readByte()); + len += 1; + } + return len; + } catch (Exception e) { + Utils.throwIfInstanceOf(e, IOException.class); + Utils.throwIfUnchecked(e); + throw new RuntimeException(e); + } finally { + byteBuf.readerIndex(readerIndex); // Reset reader & writer index for next use + byteBuf.writerIndex(writerIndex); + deflater.reset(); + } + } + + private static int writeToCache(NetOutput out, Collection packets) throws IOException { + int len = writeVarInt(out, packets.size()); + for (com.github.steveice10.packetlib.packet.Packet packet : packets) { + len += writeToCache(out, packet); + } + return len; + } + + private static int writeToCache(NetOutput out, SortedMap packets) throws IOException { + int len = 0; + len += writeVarInt(out, packets.size()); + int lastTime = 0; + for (Map.Entry entry : packets.entrySet()) { + int time = entry.getKey(); + len += writeVarInt(out, time - lastTime); + lastTime = time; + + len += writeToCache(out, entry.getValue()); + } + return len; + } + + private static int writeVarInt(NetOutput out, int i) throws IOException { + int len = 1; + while ((i & -128) != 0) { + out.writeByte(i & 127 | 128); + i >>>= 7; + len++; + } + out.writeByte(i); + return len; + } + + private static long coordToLong(int x, int z) { + return (long)x << 32 | (long)z & 0xFFFFFFFFL; + } + + private static void playMap(NavigableMap updates, int currentTimeStamp, int replayTime, Consumer update) { + Map.Entry lastUpdate = updates.floorEntry(replayTime); + if (lastUpdate != null && lastUpdate.getKey() > currentTimeStamp) { + update.accept(lastUpdate.getValue()); + } + } + + private static void rewindMap(NavigableMap updates, int currentTimeStamp, int replayTime, Consumer update) { + Map.Entry lastUpdate = updates.floorEntry(replayTime); + if (lastUpdate != null && !lastUpdate.getKey().equals(updates.floorKey(currentTimeStamp))) { + update.accept(lastUpdate.getValue()); + } + } + + private static abstract class TrackedThing { + List spawnPackets; + List despawnPackets; + int spawnTime; + + private TrackedThing(List spawnPackets, + List despawnPackets) { + this.spawnPackets = spawnPackets; + this.despawnPackets = despawnPackets; + } + + public int writeToCache(NetOutput indexOut, NetOutput cacheOut, int despawnTime, int index) throws IOException { + indexOut.writeVarInt(spawnTime); + indexOut.writeVarInt(despawnTime); + indexOut.writeVarInt(index); + index += QuickReplaySender.writeToCache(cacheOut, spawnPackets); + indexOut.writeVarInt(index); + index += QuickReplaySender.writeToCache(cacheOut, despawnPackets); + return index; + } + } + + // For memory efficiency we store raw packets (we might even want to consider compression or memory mapped files). + // During analysis we use TrackedThing which we then serialize (we'd have to do that anyway for caching) + // and afterwards deserialize in BakedTrackedThing as MC packets on demand for replaying. + private static abstract class BakedTrackedThing { + int indexSpawnPackets; + int indexDespawnPackets; + int spawnTime; + int despawnTime; + + private BakedTrackedThing(NetInput in) throws IOException { + spawnTime = in.readVarInt(); + despawnTime = in.readVarInt(); + indexSpawnPackets = in.readVarInt(); + indexDespawnPackets = in.readVarInt(); + } + + public void spawn(QuickReplaySender sender, Consumer> send) { + sender.buf.readerIndex(indexSpawnPackets); + readPacketsFromCache(sender.bufInput).forEach(send); + } + + public void despawn(QuickReplaySender sender, Consumer> send) { + sender.buf.readerIndex(indexDespawnPackets); + readPacketsFromCache(sender.bufInput).forEach(send); + } + + public abstract void play(int currentTimeStamp, int replayTime, Consumer> send); + public abstract void rewind(int currentTimeStamp, int replayTime, Consumer> send); + } + + private static class Entity extends TrackedThing { + private int id; + private NavigableMap locations = new TreeMap<>(); + + private Entity(int entityId, List spawnPackets) { + super(spawnPackets, Collections.singletonList(new ServerEntityDestroyPacket(entityId))); + this.id = entityId; + } + + @Override + public int writeToCache(NetOutput indexOut, NetOutput cacheOut, int despawnTime, int index) throws IOException { + indexOut.writeByte(1); + index = super.writeToCache(indexOut, cacheOut, despawnTime, index); + + indexOut.writeVarInt(id); + indexOut.writeVarInt(index); + index += writeVarInt(cacheOut, locations.size()); + int lastTime = 0; + for (Map.Entry entry : locations.entrySet()) { + int time = entry.getKey(); + Location loc = entry.getValue(); + index += writeVarInt(cacheOut, time - lastTime); + lastTime = time; + cacheOut.writeDouble(loc.getX()); + cacheOut.writeDouble(loc.getY()); + cacheOut.writeDouble(loc.getZ()); + cacheOut.writeFloat(loc.getYaw()); + cacheOut.writeFloat(loc.getPitch()); + index += 32; + } + + return index; + } + } + + private static class BakedEntity extends BakedTrackedThing { + private int id; + private int index; + private NavigableMap locations; + + private BakedEntity(NetInput in) throws IOException { + super(in); + + id = in.readVarInt(); + index = in.readVarInt(); + } + + @Override + @SneakyThrows(IOException.class) + public void spawn(QuickReplaySender sender, Consumer> send) { + super.spawn(sender, send); + + sender.buf.readerIndex(index); + NetInput in = sender.bufInput; + + locations = new TreeMap<>(); + + int time = 0; + for (int i = in.readVarInt(); i > 0; i--) { + time += in.readVarInt(); + locations.put(time, new Location(in.readDouble(), in.readDouble(), in.readDouble(), in.readFloat(), in.readFloat())); + } + } + + @Override + public void despawn(QuickReplaySender sender, Consumer> send) { + super.despawn(sender, send); + locations = null; + } + + @Override + public void play(int currentTimeStamp, int replayTime, Consumer> send) { + playMap(locations, currentTimeStamp, replayTime, l -> { + send.accept(toMC(new ServerEntityTeleportPacket(id, l.getX(), l.getY(), l.getZ(), l.getYaw(), l.getPitch(), false))); + send.accept(toMC(new ServerEntityHeadLookPacket(id, l.getYaw()))); + }); + } + + @Override + public void rewind(int currentTimeStamp, int replayTime, Consumer> send) { + rewindMap(locations, currentTimeStamp, replayTime, l -> { + send.accept(toMC(new ServerEntityTeleportPacket(id, l.getX(), l.getY(), l.getZ(), l.getYaw(), l.getPitch(), false))); + send.accept(toMC(new ServerEntityHeadLookPacket(id, l.getYaw()))); + }); + } + } + + private static class Chunk extends TrackedThing { + private TreeMap> blocksT = new TreeMap<>(); + private ListMultimap blocks = Multimaps.newListMultimap(blocksT, LinkedList::new); // LinkedList to allow .descendingIterator + private BlockStorage[] currentBlockState = new BlockStorage[16]; + + private Chunk(Column column) { + super(Collections.singletonList(new ServerChunkDataPacket(column)), + Collections.singletonList(new ServerUnloadChunkPacket(column.getX(), column.getZ()))); + com.github.steveice10.mc.protocol.data.game.chunk.Chunk[] chunks = column.getChunks(); + for (int i = 0; i < currentBlockState.length; i++) { + currentBlockState[i] = chunks[i] == null ? new BlockStorage() : copy(chunks[i].getBlocks()); + } + } + + @Override + public int writeToCache(NetOutput indexOut, NetOutput cacheOut, int despawnTime, int index) throws IOException { + indexOut.writeByte(2); + index = super.writeToCache(indexOut, cacheOut, despawnTime, index); + + indexOut.writeVarInt(index); + index += writeVarInt(cacheOut, blocksT.size()); + int lastTime = 0; + for (Map.Entry> entry : blocksT.entrySet()) { + int time = entry.getKey(); + index += writeVarInt(cacheOut, time - lastTime); + lastTime = time; + + Collection blockChanges = entry.getValue(); + index += writeVarInt(cacheOut, blockChanges.size()); + for (BlockChange blockChange : blockChanges) { + NetUtil.writePosition(cacheOut, blockChange.pos); + index += 8; + //#if MC>=11300 + index += writeVarInt(cacheOut, blockChange.from.getId()); + index += writeVarInt(cacheOut, blockChange.to.getId()); + //#else + //$$ index += writeVarInt(cacheOut, blockChange.from.getId() << 4 | blockChange.from.getData() & 15); + //$$ index += writeVarInt(cacheOut, blockChange.to.getId() << 4 | blockChange.to.getData() & 15); + //#endif + } + } + + return index; + } + } + + private static class BakedChunk extends BakedTrackedThing { + private int index; + private TreeMap> blocksT; + + private BakedChunk(NetInput in) throws IOException { + super(in); + + index = in.readVarInt(); + } + + @Override + @SneakyThrows(IOException.class) + public void spawn(QuickReplaySender sender, Consumer> send) { + super.spawn(sender, send); + + sender.buf.readerIndex(index); + NetInput in = sender.bufInput; + + blocksT = new TreeMap<>(); + ListMultimap blocks = Multimaps.newListMultimap(blocksT, LinkedList::new); // LinkedList to allow .descendingIterator + + int time = 0; + for (int i = in.readVarInt(); i > 0; i--) { + time += in.readVarInt(); + + for (int j = in.readVarInt(); j > 0; j--) { + blocks.put(time, new BlockChange( + NetUtil.readPosition(in), + NetUtil.readBlockState(in), + NetUtil.readBlockState(in) + )); + } + } + } + + @Override + public void despawn(QuickReplaySender sender, Consumer> send) { + super.despawn(sender, send); + + blocksT = null; + } + + @Override + public void play(int currentTimeStamp, int replayTime, Consumer> send) { + blocksT.subMap(currentTimeStamp, false, replayTime, true).values() + .forEach(updates -> updates.forEach(update -> { + send.accept(toMC(new ServerBlockChangePacket(new BlockChangeRecord(update.pos, update.to)))); + })); + } + + @Override + public void rewind(int currentTimeStamp, int replayTime, Consumer> send) { + if (currentTimeStamp >= despawnTime) { + play(spawnTime, replayTime, send); + return; + } + blocksT.subMap(replayTime, false, currentTimeStamp, true).descendingMap().values() + .forEach(updates -> + ((LinkedList) updates).descendingIterator().forEachRemaining(update -> + send.accept(toMC(new ServerBlockChangePacket(new BlockChangeRecord(update.pos, update.from)))))); + } + } + + private static class BlockChange { + private Position pos; + private BlockState from; + private BlockState to; + + private BlockChange(Position pos, BlockState from, BlockState to) { + this.pos = pos; + this.from = from; + this.to = to; + } + } + + private static class Weather extends TrackedThing { + private TreeMap rainStrengths = new TreeMap<>(); + + private Weather() { + super(Collections.singletonList(new ServerNotifyClientPacket(ClientNotification.START_RAIN, null)), + Collections.singletonList(new ServerNotifyClientPacket(ClientNotification.STOP_RAIN, null))); + } + + @Override + public int writeToCache(NetOutput indexOut, NetOutput cacheOut, int despawnTime, int index) throws IOException { + indexOut.writeByte(3); + index = super.writeToCache(indexOut, cacheOut, despawnTime, index); + + indexOut.writeVarInt(index); + index += QuickReplaySender.writeToCache(cacheOut, rainStrengths); + + return index; + } + } + + private static class BakedWeather extends BakedTrackedThing { + private int index; + private TreeMap> rainStrengths; + + private BakedWeather(NetInput in) throws IOException { + super(in); + + index = in.readVarInt(); + } + + @Override + @SneakyThrows(IOException.class) + public void spawn(QuickReplaySender sender, Consumer> send) { + super.spawn(sender, send); + + sender.buf.readerIndex(index); + + rainStrengths = new TreeMap<>(); + + readFromCache(sender.bufInput, rainStrengths); + } + + @Override + public void despawn(QuickReplaySender sender, Consumer> send) { + super.despawn(sender, send); + + rainStrengths = null; + } + + @Override + public void play(int currentTimeStamp, int replayTime, Consumer> send) { + playMap(rainStrengths, currentTimeStamp, replayTime, send); + } + + @Override + public void rewind(int currentTimeStamp, int replayTime, Consumer> send) { + rewindMap(rainStrengths, currentTimeStamp, replayTime, send); + } + } +} +//#endif diff --git a/src/main/java/com/replaymod/replay/ReplayHandler.java b/src/main/java/com/replaymod/replay/ReplayHandler.java index 6052a8c7..b3cb2fc7 100755 --- a/src/main/java/com/replaymod/replay/ReplayHandler.java +++ b/src/main/java/com/replaymod/replay/ReplayHandler.java @@ -1,7 +1,11 @@ package com.replaymod.replay; import com.google.common.base.Preconditions; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import com.replaymod.core.utils.Restrictions; +import com.replaymod.core.utils.Utils; import com.replaymod.core.utils.WrappedTimer; import com.replaymod.replay.camera.CameraEntity; import com.replaymod.replay.camera.SpectatorCameraController; @@ -11,28 +15,48 @@ import com.replaymod.replay.gui.overlay.GuiReplayOverlay; import com.replaymod.replaystudio.data.Marker; import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.util.Location; +import de.johni0702.minecraft.gui.container.GuiContainer; +import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar; +import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.embedded.EmbeddedChannel; import net.minecraft.client.Minecraft; -import net.minecraft.client.entity.EntityOtherPlayerMP; import net.minecraft.client.gui.GuiScreen; -import net.minecraft.client.gui.ScaledResolution; import net.minecraft.client.resources.I18n; +import net.minecraft.crash.CrashReport; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.network.NetworkManager; -import org.lwjgl.opengl.Display; import java.io.IOException; import java.util.*; +//#if MC>=11300 +import net.minecraft.client.MainWindow; +import net.minecraft.entity.EntityLivingBase; +//#else +//$$ import net.minecraft.client.entity.EntityOtherPlayerMP; +//$$ import net.minecraft.client.gui.ScaledResolution; +//$$ import org.lwjgl.opengl.Display; +//#endif + +//#if MC<10904 +//$$ import de.johni0702.minecraft.gui.element.GuiLabel; +//$$ import de.johni0702.minecraft.gui.popup.GuiInfoPopup; +//$$ import de.johni0702.minecraft.gui.utils.Colors; +//#endif + //#if MC>=10800 -import com.mojang.authlib.GameProfile; -import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.network.EnumPacketDirection; -import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.fml.client.FMLClientHandler; -import net.minecraftforge.fml.common.network.handshake.NetworkDispatcher; +//#if MC>=11300 +import net.minecraftforge.fml.network.NetworkHooks; +//#else +//$$ import com.mojang.authlib.GameProfile; +//$$ import net.minecraft.client.network.NetHandlerPlayClient; +//$$ import net.minecraftforge.fml.client.FMLClientHandler; +//$$ import net.minecraftforge.fml.common.network.handshake.NetworkDispatcher; +//#endif +import net.minecraft.client.network.NetHandlerLoginClient; import static net.minecraft.client.renderer.GlStateManager.*; //#else @@ -48,16 +72,19 @@ import static net.minecraft.client.renderer.GlStateManager.*; //$$ import java.net.SocketAddress; //$$ //$$ import static com.replaymod.core.versions.MCVer.GlStateManager.*; -//$$ import static com.replaymod.replay.ReplayModReplay.LOGGER; //#endif +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + import static com.replaymod.core.versions.MCVer.*; +import static com.replaymod.replay.ReplayModReplay.LOGGER; import static org.lwjgl.opengl.GL11.GL_COLOR_BUFFER_BIT; import static org.lwjgl.opengl.GL11.GL_DEPTH_BUFFER_BIT; public class ReplayHandler { - private static Minecraft mc = Minecraft.getMinecraft(); + private static Minecraft mc = getMinecraft(); /** * The file currently being played. @@ -67,7 +94,13 @@ public class ReplayHandler { /** * Decodes and sends packets into channel. */ - private final ReplaySender replaySender; + private final FullReplaySender fullReplaySender; + //#if MC>=10904 + private final QuickReplaySender quickReplaySender; + private boolean quickMode = false; + //#else + //$$ private static final String QUICK_MODE_MIN_MC = "1.9.4"; + //#endif /** * Currently active replay restrictions. @@ -79,12 +112,14 @@ public class ReplayHandler { */ private boolean suppressCameraMovements; - private final List markers; + private Set markers; private final GuiReplayOverlay overlay; private EmbeddedChannel channel; + private int replayDuration; + /** * The position at which the camera should be located after the next jump. */ @@ -96,11 +131,16 @@ public class ReplayHandler { Preconditions.checkState(mc.isCallingFromMinecraftThread(), "Must be called from Minecraft thread."); this.replayFile = replayFile; + replayDuration = replayFile.getMetaData().getDuration(); + FML_BUS.post(new ReplayOpenEvent.Pre(this)); - markers = new ArrayList<>(replayFile.getMarkers().or(Collections.emptySet())); + markers = replayFile.getMarkers().or(Collections.emptySet()); - replaySender = new ReplaySender(this, replayFile, false); + fullReplaySender = new FullReplaySender(this, replayFile, false); + //#if MC>=10904 + quickReplaySender = new QuickReplaySender(ReplayModReplay.instance, replayFile); + //#endif setup(); @@ -109,7 +149,7 @@ public class ReplayHandler { FML_BUS.post(new ReplayOpenEvent.Post(this)); - replaySender.setAsyncMode(asyncMode); + fullReplaySender.setAsyncMode(asyncMode); } void restartedReplay() { @@ -117,7 +157,11 @@ public class ReplayHandler { // Force re-creation of camera entity by unloading the previous world mc.addScheduledTask(() -> { - mc.setIngameNotInFocus(); + //#if MC>=11300 + mc.mouseHelper.ungrabMouse(); + //#else + //$$ mc.setIngameNotInFocus(); + //#endif mc.loadWorld(null); }); @@ -131,7 +175,10 @@ public class ReplayHandler { FML_BUS.post(new ReplayCloseEvent.Pre(this)); - replaySender.terminateReplay(); + fullReplaySender.terminateReplay(); + //#if MC>=10904 + quickReplaySender.unregister(); + //#endif replayFile.save(); replayFile.close(); @@ -139,7 +186,11 @@ public class ReplayHandler { channel.close().awaitUninterruptibly(); if (player(mc) instanceof CameraEntity) { - player(mc).setDead(); + //#if MC>=11300 + player(mc).remove(); + //#else + //$$ player(mc).setDead(); + //#endif } if (world(mc) != null) { @@ -175,29 +226,38 @@ public class ReplayHandler { t.printStackTrace(); } }; - NetHandlerPlayClient netHandlerPlayClient = - new NetHandlerPlayClient(mc, null, networkManager, new GameProfile(UUID.randomUUID(), "Player")); - networkManager.setNetHandler(netHandlerPlayClient); - FMLClientHandler.instance().setPlayClient(netHandlerPlayClient); - - //#if MC>=11200 + //#if MC>=11300 channel = new EmbeddedChannel(); - NetworkDispatcher networkDispatcher = new NetworkDispatcher(networkManager); - channel.attr(NetworkDispatcher.FML_DISPATCHER).set(networkDispatcher); - channel.pipeline().addFirst("ReplayModReplay_replaySender", replaySender); + channel.pipeline().addFirst("ReplayModReplay_replaySender", fullReplaySender); + channel.pipeline().addFirst("ReplayModReplay_quickReplaySender", quickReplaySender); channel.pipeline().addLast("packet_handler", networkManager); channel.pipeline().fireChannelActive(); - networkDispatcher.clientToServerHandshake(); + + networkManager.setNetHandler(new NetHandlerLoginClient(networkManager, mc, null, it -> {})); + NetworkHooks.registerClientLoginChannel(networkManager); + // FIXME make this work (with vanilla and mods) on all other versions again, now that login phase is included + // probably have to change some of the forge handshake calls + //#else + //$$ NetHandlerLoginClient netHandlerLoginClient = + //$$ new NetHandlerLoginClient(networkManager, mc, null); + //$$ networkManager.setNetHandler(netHandlerLoginClient); + //$$ + //#if MC>=11200 + //$$ channel = new EmbeddedChannel(); + //$$ channel.pipeline().addFirst("ReplayModReplay_replaySender", fullReplaySender); + //$$ channel.pipeline().addFirst("ReplayModReplay_quickReplaySender", quickReplaySender); + //$$ channel.pipeline().addLast("packet_handler", networkManager); + //$$ channel.pipeline().fireChannelActive(); //#else //$$ channel = new EmbeddedChannel(networkManager); - //$$ NetworkDispatcher networkDispatcher = new NetworkDispatcher(networkManager); - //$$ channel.attr(NetworkDispatcher.FML_DISPATCHER).set(networkDispatcher); - //$$ - //$$ channel.pipeline().addFirst("ReplayModReplay_replaySender", replaySender); - //$$ channel.pipeline().addAfter("ReplayModReplay_replaySender", "fml:packet_handler", networkDispatcher); + //$$ channel.pipeline().addFirst("ReplayModReplay_replaySender", fullReplaySender); + //#if MC>=10904 + //$$ channel.pipeline().addFirst("ReplayModReplay_quickReplaySender", quickReplaySender); + //#endif //$$ channel.pipeline().fireChannelActive(); //#endif + //#endif //#else //$$ NetworkManager networkManager = new NetworkManager(true) { //$$ @Override @@ -225,16 +285,9 @@ public class ReplayHandler { //$$ ChannelOutboundHandlerAdapter dummyHandler = new ChannelOutboundHandlerAdapter(); //$$ channel = new EmbeddedChannel(dummyHandler); //$$ channel.pipeline().remove(dummyHandler); - //$$ channel.pipeline().addFirst("ReplayModReplay_replaySender", replaySender); + //$$ channel.pipeline().addFirst("ReplayModReplay_replaySender", fullReplaySender); //$$ channel.pipeline().addAfter("ReplayModReplay_replaySender", "packet_handler", networkManager); //$$ channel.pipeline().fireChannelActive(); - //$$ - //$$ // Call twice to force-overwrite the NetworkManager's internal state - //$$ networkManager.setConnectionState(EnumConnectionState.PLAY); - //$$ networkManager.getNetHandler().onConnectionStateTransition(EnumConnectionState.LOGIN, EnumConnectionState.PLAY); - //$$ networkManager.setConnectionState(EnumConnectionState.PLAY); - //$$ - //$$ FMLNetworkHandler.fmlClientHandshake(networkManager); //#endif } @@ -247,13 +300,122 @@ public class ReplayHandler { } public ReplaySender getReplaySender() { - return replaySender; + //#if MC>=10904 + return quickMode ? quickReplaySender : fullReplaySender; + //#else + //$$ return fullReplaySender; + //#endif } public GuiReplayOverlay getOverlay() { return overlay; } + //#if MC>=10904 + public void ensureQuickModeInitialized(Runnable andThen) { + ListenableFuture future = quickReplaySender.getInitializationPromise(); + if (future == null) { + InitializingQuickModePopup popup = new InitializingQuickModePopup(overlay); + future = quickReplaySender.initialize(progress -> popup.progressBar.setProgress(progress.floatValue())); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable Void result) { + popup.close(); + } + + @Override + public void onFailure(@Nonnull Throwable t) { + String message = "Failed to initialize quick mode. It will not be available."; + Utils.error(LOGGER, overlay, CrashReport.makeCrashReport(t, message), popup::close); + } + }); + } + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable Void result) { + andThen.run(); + } + + @Override + public void onFailure(@Nonnull Throwable t) { + // Exception already printed in callback added above + } + }); + } + + private class InitializingQuickModePopup extends AbstractGuiPopup { + private final GuiProgressBar progressBar = new GuiProgressBar(popup).setSize(300, 20) + .setI18nLabel("replaymod.gui.loadquickmode"); + + public InitializingQuickModePopup(GuiContainer container) { + super(container); + open(); + } + + @Override + public void close() { + super.close(); + } + + @Override + protected InitializingQuickModePopup getThis() { + return this; + } + } + + public void setQuickMode(boolean quickMode) { + if (quickMode == this.quickMode) return; + if (quickMode && fullReplaySender.isAsyncMode()) { + // If this method is called via runLater, then it cannot switch to sync mode by itself as there might be + // some rogue packets in the task queue after it. Instead the caller must switch to sync mode first and + // use runLater until all packets have been processed (when using setAsyncModeAndWait, one runLater should + // be sufficient). + throw new IllegalStateException("Cannot switch to quick mode while in async mode."); + } + this.quickMode = quickMode; + + CameraEntity cam = getCameraEntity(); + if (cam != null) { + targetCameraPosition = new Location(cam.posX, cam.posY, cam.posZ, cam.rotationYaw, cam.rotationPitch); + } else { + targetCameraPosition = null; + } + + if (quickMode) { + quickReplaySender.register(); + quickReplaySender.restart(); + quickReplaySender.sendPacketsTill(fullReplaySender.currentTimeStamp()); + } else { + quickReplaySender.unregister(); + fullReplaySender.sendPacketsTill(0); + fullReplaySender.sendPacketsTill(quickReplaySender.currentTimeStamp()); + } + + moveCameraToTargetPosition(); + } + + public boolean isQuickMode() { + return quickMode; + } + //#else + //$$ public void ensureQuickModeInitialized(@SuppressWarnings("unused") Runnable andThen) { + //$$ GuiInfoPopup.open(overlay, + //$$ new GuiLabel().setI18nText("replaymod.gui.noquickmode", QUICK_MODE_MIN_MC).setColor(Colors.BLACK)); + //$$ } + //$$ + //$$ public void setQuickMode(@SuppressWarnings("unused") boolean quickMode) { + //$$ throw new UnsupportedOperationException("Quick Mode not supported on this version."); + //$$ } + //$$ + //$$ public boolean isQuickMode() { + //$$ return false; + //$$ } + //#endif + + public int getReplayDuration() { + return replayDuration; + } + /** * Return whether camera movement by user inputs and/or server packets should be suppressed. * @return {@code true} if these kinds of movement should be suppressed @@ -270,26 +432,6 @@ public class ReplayHandler { this.suppressCameraMovements = suppressCameraMovements; } - /** - * Returns all markers. - * When changed, {@link #saveMarkers()} should be called to save the changes. - * @return Collection of markers in no particular order - */ - public Collection getMarkers() { - return markers; - } - - /** - * Saves all markers. - */ - public void saveMarkers() { - try { - replayFile.writeMarkers(new HashSet<>(markers)); - } catch (IOException e) { - e.printStackTrace(); - } - } - /** * Spectate the specified entity. * When the entity is {@code null} or the camera entity, the camera becomes the view entity. @@ -363,6 +505,49 @@ public class ReplayHandler { } public void doJump(int targetTime, boolean retainCameraPosition) { + //#if MC>=10904 + if (getReplaySender() == quickReplaySender) { + // Always round to full tick + targetTime = targetTime + targetTime % 50; + + if (targetTime >= 50) { + // Jump to time of previous tick first + quickReplaySender.sendPacketsTill(targetTime - 50); + } + + // Update all entity positions (especially prev/lastTick values) + for (Entity entity : loadedEntityList(world(mc))) { + skipTeleportInterpolation(entity); + entity.lastTickPosX = entity.prevPosX = entity.posX; + entity.lastTickPosY = entity.prevPosY = entity.posY; + entity.lastTickPosZ = entity.prevPosZ = entity.posZ; + entity.prevRotationYaw = entity.rotationYaw; + entity.prevRotationPitch = entity.rotationPitch; + } + + // Run previous tick + //#if MC>=11300 + mc.runTick(); + //#else + //$$ try { + //$$ mc.runTick(); + //$$ } catch (IOException e) { + //$$ throw new RuntimeException(e); + //$$ } + //#endif + + // Jump to target tick + quickReplaySender.sendPacketsTill(targetTime); + + // Immediately apply player teleport interpolation + for (Entity entity : loadedEntityList(world(mc))) { + skipTeleportInterpolation(entity); + } + return; + } + //#endif + FullReplaySender replaySender = fullReplaySender; + if (replaySender.isHurrying()) { return; // When hurrying, no Timeline jumping etc. is possible } @@ -389,7 +574,11 @@ public class ReplayHandler { // Render our please-wait-screen GuiScreen guiScreen = new GuiScreen() { @Override - public void drawScreen(int mouseX, int mouseY, float partialTicks) { + //#if MC>=11300 + public void render(int mouseX, int mouseY, float partialTicks) { + //#else + //$$ public void drawScreen(int mouseX, int mouseY, float partialTicks) { + //#endif drawBackground(0); drawCenteredString(getFontRenderer(mc), I18n.format("replaymod.gui.pleasewait"), width / 2, height / 2, 0xffffffff); @@ -404,47 +593,68 @@ public class ReplayHandler { clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); enableTexture2D(); mc.getFramebuffer().bindFramebuffer(true); - mc.entityRenderer.setupOverlayRendering(); + //#if MC>=11300 + mc.mainWindow.setupOverlayRendering(); + //#else + //$$ mc.entityRenderer.setupOverlayRendering(); + //#endif - ScaledResolution resolution = newScaledResolution(mc); + //#if MC>=11300 + MainWindow + //#else + //$$ ScaledResolution + //#endif + resolution = newScaledResolution(mc); guiScreen.setWorldAndResolution(mc, resolution.getScaledWidth(), resolution.getScaledHeight()); - guiScreen.drawScreen(0, 0, 0); + //#if MC>=11300 + guiScreen.render(0, 0, 0); + //#else + //$$ guiScreen.drawScreen(0, 0, 0); + //#endif mc.getFramebuffer().unbindFramebuffer(); popMatrix(); pushMatrix(); - mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight); + //#if MC>=11300 + mc.getFramebuffer().framebufferRender(mc.mainWindow.getFramebufferWidth(), mc.mainWindow.getFramebufferHeight()); + //#else + //$$ mc.getFramebuffer().framebufferRender(mc.displayWidth, mc.displayHeight); + //#endif popMatrix(); - Display.update(); + //#if MC>=11300 + mc.mainWindow.update(true); + //#else + //$$ Display.update(); + //#endif // Send the packets replaySender.sendPacketsTill(targetTime); replaySender.setAsyncMode(true); replaySender.setReplaySpeed(0); - getConnection(mc).getNetworkManager().processReceivedPackets(); + getConnection(mc).getNetworkManager() + //#if MC>=11300 + .tick(); + //#else + //$$ .processReceivedPackets(); + //#endif for (Entity entity : loadedEntityList(world(mc))) { - if (entity instanceof EntityOtherPlayerMP) { - EntityOtherPlayerMP e = (EntityOtherPlayerMP) entity; - e.setPosition(e.otherPlayerMPX, e.otherPlayerMPY, e.otherPlayerMPZ); - e.rotationYaw = (float) e.otherPlayerMPYaw; - e.rotationPitch = (float) e.otherPlayerMPPitch; - } + skipTeleportInterpolation(entity); entity.lastTickPosX = entity.prevPosX = entity.posX; entity.lastTickPosY = entity.prevPosY = entity.posY; entity.lastTickPosZ = entity.prevPosZ = entity.posZ; entity.prevRotationYaw = entity.rotationYaw; entity.prevRotationPitch = entity.rotationPitch; } - //#if MC>=10800 - try { - mc.runTick(); - } catch (IOException e) { - e.printStackTrace(); // This should never be thrown but whatever - } + //#if MC>=10800 && MC<11300 + //$$ try { + //$$ mc.runTick(); + //$$ } catch (IOException e) { + //$$ e.printStackTrace(); // This should never be thrown but whatever + //$$ } //#else - //$$ mc.runTick(); + mc.runTick(); //#endif //finally, updating the camera's position (which is not done by the sync jumping) @@ -455,4 +665,22 @@ public class ReplayHandler { } } } + + private void skipTeleportInterpolation(Entity entity) { + //#if MC>=11300 + if (entity instanceof EntityLivingBase) { + EntityLivingBase e = (EntityLivingBase) entity; + e.setPosition(e.interpTargetX, e.interpTargetY, e.interpTargetZ); + e.rotationYaw = (float) e.interpTargetYaw; + e.rotationPitch = (float) e.interpTargetPitch; + } + //#else + //$$ if (entity instanceof EntityOtherPlayerMP) { + //$$ EntityOtherPlayerMP e = (EntityOtherPlayerMP) entity; + //$$ e.setPosition(e.otherPlayerMPX, e.otherPlayerMPY, e.otherPlayerMPZ); + //$$ e.rotationYaw = (float) e.otherPlayerMPYaw; + //$$ e.rotationPitch = (float) e.otherPlayerMPPitch; + //$$ } + //#endif + } } diff --git a/src/main/java/com/replaymod/replay/ReplayModReplay.java b/src/main/java/com/replaymod/replay/ReplayModReplay.java index 48d73d59..4ece897f 100644 --- a/src/main/java/com/replaymod/replay/ReplayModReplay.java +++ b/src/main/java/com/replaymod/replay/ReplayModReplay.java @@ -4,14 +4,16 @@ import com.google.common.base.Function; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import com.replaymod.core.KeyBindingRegistry; +import com.replaymod.core.Module; import com.replaymod.core.ReplayMod; import com.replaymod.core.utils.ModCompat; +import com.replaymod.core.versions.MCVer; import com.replaymod.replay.camera.CameraController; import com.replaymod.replay.camera.CameraControllerRegistry; import com.replaymod.replay.camera.CameraEntity; import com.replaymod.replay.camera.ClassicCameraController; import com.replaymod.replay.camera.VanillaCameraController; -import com.replaymod.replay.gui.overlay.GuiMarkerTimeline; import com.replaymod.replay.gui.screen.GuiModCompatWarning; import com.replaymod.replay.handler.GuiHandler; import com.replaymod.replaystudio.data.Marker; @@ -19,46 +21,30 @@ import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ZipReplayFile; import com.replaymod.replaystudio.studio.ReplayStudio; import net.minecraft.client.Minecraft; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.lwjgl.input.Keyboard; -//#if MC>=10800 -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.event.FMLInitializationEvent; -import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; +//#if MC>=11300 +import com.replaymod.core.versions.MCVer.Keyboard; //#else -//$$ import cpw.mods.fml.common.Mod; -//$$ import cpw.mods.fml.common.event.FMLInitializationEvent; -//$$ import cpw.mods.fml.common.event.FMLPostInitializationEvent; -//$$ import cpw.mods.fml.common.event.FMLPreInitializationEvent; +//$$ import org.lwjgl.input.Keyboard; //#endif import javax.annotation.Nullable; import java.io.File; import java.io.IOException; -import java.util.ArrayList; import java.util.Optional; -@Mod(modid = ReplayModReplay.MOD_ID, - version = "@MOD_VERSION@", - acceptedMinecraftVersions = "@MC_VERSION@", - acceptableRemoteVersions = "*", - //#if MC>=10800 - clientSideOnly = true, - //#endif - useMetadata = true) -public class ReplayModReplay { - public static final String MOD_ID = "replaymod-replay"; +public class ReplayModReplay implements Module { - @Mod.Instance(MOD_ID) + { instance = this; } public static ReplayModReplay instance; private ReplayMod core; private final CameraControllerRegistry cameraControllerRegistry = new CameraControllerRegistry(); - public static Logger LOGGER; + public static Logger LOGGER = LogManager.getLogger(); protected ReplayHandler replayHandler; @@ -66,14 +52,15 @@ public class ReplayModReplay { return replayHandler; } - @Mod.EventHandler - public void preInit(FMLPreInitializationEvent event) { - LOGGER = event.getModLog(); - core = ReplayMod.instance; + public ReplayModReplay(ReplayMod core) { + this.core = core; core.getSettingsRegistry().register(Setting.class); + } - core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.marker", Keyboard.KEY_M, new Runnable() { + @Override + public void registerKeyBindings(KeyBindingRegistry registry) { + registry.registerKeyBinding("replaymod.input.marker", Keyboard.KEY_M, new Runnable() { @Override public void run() { if (replayHandler != null ) { @@ -87,29 +74,17 @@ public class ReplayModReplay { marker.setYaw(camera.rotationYaw); marker.setPitch(camera.rotationPitch); marker.setRoll(camera.roll); - replayHandler.getMarkers().add(marker); - replayHandler.saveMarkers(); + replayHandler.getOverlay().timeline.addMarker(marker); } } } }); - core.getKeyBindingRegistry().registerRaw(Keyboard.KEY_DELETE, () -> { - if (replayHandler != null) { - GuiMarkerTimeline timeline = replayHandler.getOverlay().timeline; - if (timeline.getSelectedMarker() != null) { - replayHandler.getMarkers().remove(timeline.getSelectedMarker()); - replayHandler.saveMarkers(); - timeline.setSelectedMarker(null); - } - } - }); - - core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.thumbnail", Keyboard.KEY_N, new Runnable() { + registry.registerKeyBinding("replaymod.input.thumbnail", Keyboard.KEY_N, new Runnable() { @Override public void run() { if (replayHandler != null) { - Minecraft mc = Minecraft.getMinecraft(); + Minecraft mc = MCVer.getMinecraft(); ListenableFuture future = NoGuiScreenshot.take(mc, 1280, 720); Futures.addCallback(future, new FutureCallback() { @Override @@ -133,7 +108,7 @@ public class ReplayModReplay { } }); - core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.playpause", Keyboard.KEY_P, new Runnable() { + registry.registerKeyBinding("replaymod.input.playpause", Keyboard.KEY_P, new Runnable() { @Override public void run() { if (replayHandler != null) { @@ -142,6 +117,17 @@ public class ReplayModReplay { } }); + registry.registerKeyBinding("replaymod.input.quickmode", Keyboard.KEY_Q, () -> { + if (replayHandler != null) { + replayHandler.getReplaySender().setSyncModeAndWait(); + core.runLater(() -> + replayHandler.ensureQuickModeInitialized(() -> { + replayHandler.setQuickMode(!replayHandler.isQuickMode()); + replayHandler.getReplaySender().setAsyncMode(true); + })); + } + }); + core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.rollclockwise", Keyboard.KEY_L, () -> { // Noop, actual handling logic in CameraEntity#update }); @@ -153,7 +139,10 @@ public class ReplayModReplay { core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.resettilt", Keyboard.KEY_K, () -> { Optional.ofNullable(replayHandler).map(ReplayHandler::getCameraEntity).ifPresent(c -> c.roll = 0); }); + } + @Override + public void initClient() { cameraControllerRegistry.register("replaymod.camera.classic", new Function() { @Nullable @Override @@ -168,21 +157,13 @@ public class ReplayModReplay { return new VanillaCameraController(core.getMinecraft(), cameraEntity); } }); - } - @Mod.EventHandler - public void init(FMLInitializationEvent event) { Minecraft mc = core.getMinecraft(); mc.timer = new InputReplayTimer(mc.timer, this); new GuiHandler(this).register(); } - @Mod.EventHandler - public void postInit(FMLPostInitializationEvent event) { - Setting.CAMERA.setChoices(new ArrayList<>(cameraControllerRegistry.getControllers())); - } - public void startReplay(File file) throws IOException { startReplay(new ZipReplayFile(new ReplayStudio(), file)); } diff --git a/src/main/java/com/replaymod/replay/ReplaySender.java b/src/main/java/com/replaymod/replay/ReplaySender.java old mode 100755 new mode 100644 index 481a61e2..53a23c62 --- a/src/main/java/com/replaymod/replay/ReplaySender.java +++ b/src/main/java/com/replaymod/replay/ReplaySender.java @@ -1,740 +1,18 @@ package com.replaymod.replay; -import com.google.common.base.Preconditions; -import com.google.common.io.Files; -import com.replaymod.core.ReplayMod; -import com.replaymod.core.utils.Restrictions; -import com.replaymod.replay.camera.CameraEntity; -import com.replaymod.replaystudio.io.ReplayInputStream; -import com.replaymod.replaystudio.io.ReplayOutputStream; -import com.replaymod.replaystudio.replay.ReplayFile; -import com.replaymod.replaystudio.studio.ReplayStudio; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufOutputStream; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelDuplexHandler; -import io.netty.channel.ChannelHandler.Sharable; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; import net.minecraft.client.Minecraft; -import net.minecraft.client.entity.EntityOtherPlayerMP; -import net.minecraft.client.gui.GuiDownloadTerrain; -import net.minecraft.client.gui.GuiErrorScreen; -import net.minecraft.client.resources.I18n; -import net.minecraft.entity.Entity; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.network.*; -import net.minecraft.network.play.server.*; -import net.minecraft.world.EnumDifficulty; -import net.minecraft.world.World; -import net.minecraft.world.WorldType; -import net.minecraft.world.chunk.Chunk; -import net.minecraft.world.chunk.IChunkProvider; -import net.minecraftforge.common.MinecraftForge; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; -//#if MC>=11200 -import com.replaymod.core.utils.WrappedTimer; -//#endif -//#if MC>=11002 -import net.minecraft.world.GameType; -//#else -//$$ import net.minecraft.world.WorldSettings.GameType; -//#endif -//#if MC>=10904 -import net.minecraft.util.text.ITextComponent; -//#else -//$$ import net.minecraft.util.IChatComponent; -//#endif +import static com.replaymod.core.versions.MCVer.getMinecraft; -//#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; -import net.minecraftforge.fml.common.gameevent.TickEvent; -//#else -//$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; -//$$ import cpw.mods.fml.common.gameevent.TickEvent; -//$$ import org.apache.commons.io.Charsets; -//#endif - -import java.io.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; - -import static com.replaymod.core.versions.MCVer.*; - -/** - * Sends replay packets to netty channels. - * Even though {@link Sharable}, this should never be added to multiple pipes at once, it may however be re-added when - * the replay restart from the beginning. - */ -@Sharable -public class ReplaySender extends ChannelDuplexHandler { - /** - * These packets are ignored completely during replay. - */ - private static final List BAD_PACKETS = Arrays.asList( - //#if MC>=11200 - SPacketRecipeBook.class, - SPacketAdvancementInfo.class, - SPacketSelectAdvancementsTab.class, - //#endif - //#if MC>=10904 - // TODO Update possibly more? - SPacketUpdateHealth.class, - SPacketOpenWindow.class, - SPacketCloseWindow.class, - SPacketSetSlot.class, - SPacketWindowItems.class, - SPacketSignEditorOpen.class, - SPacketStatistics.class, - SPacketSetExperience.class, - SPacketCamera.class, - SPacketPlayerAbilities.class, - SPacketTitle.class - //#else - //#if MC>=10800 - //$$ S43PacketCamera.class, - //$$ S45PacketTitle.class, - //#endif - //$$ S06PacketUpdateHealth.class, - //$$ S2DPacketOpenWindow.class, - //$$ S2EPacketCloseWindow.class, - //$$ S2FPacketSetSlot.class, - //$$ S30PacketWindowItems.class, - //$$ S36PacketSignEditorOpen.class, - //$$ S37PacketStatistics.class, - //$$ S1FPacketSetExperience.class, - //$$ S39PacketPlayerAbilities.class - //#endif - ); - - private static int TP_DISTANCE_LIMIT = 128; - - /** - * The replay handler responsible for the current replay. - */ - private final ReplayHandler replayHandler; - - /** - * Whether to work in async mode. - * - * When in async mode, a separate thread send packets and waits according to their delays. - * This is default in normal playback mode. - * - * When in sync mode, no packets will be sent until {@link #sendPacketsTill(int)} is called. - * This is used during path playback and video rendering. - */ - protected boolean asyncMode; - - /** - * Timestamp of the last packet sent in milliseconds since the start. - */ - protected int lastTimeStamp; - - /** - * @see #currentTimeStamp() - */ - protected int currentTimeStamp; - - /** - * The replay file. - */ - protected ReplayFile replayFile; - - /** - * The channel handler context used to send packets to minecraft. - */ - protected ChannelHandlerContext ctx; - - /** - * The replay input stream from which new packets are read. - * When accessing this stream make sure to synchronize on {@code this} as it's used from multiple threads. - */ - protected ReplayInputStream replayIn; - - /** - * The next packet that should be sent. - * This is required as some actions such as jumping to a specified timestamp have to peek at the next packet. - */ - protected PacketData nextPacket; - - /** - * Whether we need to restart the current replay. E.g. when jumping backwards in time - */ - protected boolean startFromBeginning = true; - - /** - * Whether to terminate the replay. This only has an effect on the async mode and is {@code true} during sync mode. - */ - protected boolean terminate; - - /** - * The speed of the replay. 1 is normal, 2 is twice as fast, 0.5 is half speed and 0 is frozen - */ - protected double replaySpeed = 1f; - - /** - * Whether the world has been loaded and the dirt-screen should go away. - */ - protected boolean hasWorldLoaded; - - /** - * The minecraft instance. - */ - protected Minecraft mc = Minecraft.getMinecraft(); - - /** - * The total length of this replay in milliseconds. - */ - protected final int replayLength; - - /** - * Our actual entity id that the server gave to us. - */ - protected int actualID = -1; - - /** - * Whether to allow (process) the next player movement packet. - */ - protected boolean allowMovement; - - /** - * Directory to which resource packs are extracted. - */ - private final File tempResourcePackFolder = Files.createTempDir(); - - /** - * Create a new replay sender. - * @param file The replay file - * @param asyncMode {@code true} for async mode, {@code false} otherwise - * @see #asyncMode - */ - public ReplaySender(ReplayHandler replayHandler, ReplayFile file, boolean asyncMode) throws IOException { - this.replayHandler = replayHandler; - this.replayFile = file; - this.asyncMode = asyncMode; - this.replayLength = file.getMetaData().getDuration(); - - MinecraftForge.EVENT_BUS.register(this); - - if (asyncMode) { - new Thread(asyncSender, "replaymod-async-sender").start(); - } - } - - /** - * Set whether this replay sender operates in async mode. - * When in async mode, it will send packets timed from a separate thread. - * When not in async mode, it will send packets when {@link #sendPacketsTill(int)} is called. - * @param asyncMode {@code true} to enable async mode - */ - public void setAsyncMode(boolean asyncMode) { - if (this.asyncMode == asyncMode) return; - this.asyncMode = asyncMode; - if (asyncMode) { - this.terminate = false; - new Thread(asyncSender, "replaymod-async-sender").start(); - } else { - this.terminate = true; - } - } - - public boolean isAsyncMode() { - return asyncMode; - } - - /** - * Set whether this replay sender to operate in sync mode. - * When in sync mode, it will send packets when {@link #sendPacketsTill(int)} is called. - * This call will block until the async worker thread has stopped. - */ - public void setSyncModeAndWait() { - if (!this.asyncMode) return; - this.asyncMode = false; - this.terminate = true; - synchronized (this) { - // This will wait for the worker thread to leave the synchronized code part - } - } - - /** - * Return a fake {@link Minecraft#getSystemTime()} value that respects slowdown/speedup/pause and works in both, - * sync and async mode. - * Note: For sync mode this returns the last value passed to {@link #sendPacketsTill(int)}. - * @return The timestamp in milliseconds since the start of the replay - */ - public int currentTimeStamp() { - if (asyncMode) { - int timePassed = (int) (System.currentTimeMillis() - lastPacketSent); - return lastTimeStamp + (int) (timePassed * getReplaySpeed()); - } else { - return lastTimeStamp; - } - } - - /** - * Return the total length of the replay played. - * @return Total length in milliseconds - */ - public int replayLength() { - return replayLength; - } - - /** - * Terminate this replay sender. - */ - public void terminateReplay() { - terminate = true; - MinecraftForge.EVENT_BUS.unregister(this); - try { - channelInactive(ctx); - ctx.channel().pipeline().close(); - FileUtils.deleteDirectory(tempResourcePackFolder); - } catch(Exception e) { - e.printStackTrace(); - } - } - - @SubscribeEvent - public void onWorldTick(TickEvent.ClientTickEvent event) { - // Unfortunately the WorldTickEvent doesn't seem to be emitted on the CLIENT side - if (event.phase != TickEvent.Phase.START) return; - - // Spawning a player into an empty chunk (which we might do with the recording player) - // prevents it from being moved by teleport packets (it essentially gets stuck) because - // Entity#addedToChunk is not set and it is therefore not updated every tick. - // To counteract this, we need to manually update it's position if it hasn't been added - // to any chunk yet. - if (world(mc) != null) { - for (EntityPlayer playerEntity : playerEntities(world(mc))) { - if (!playerEntity.addedToChunk && playerEntity instanceof EntityOtherPlayerMP) { - playerEntity.onLivingUpdate(); - } - } - } - } - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) - throws Exception { - // When in async mode and the replay sender shut down, then don't send packets - if(terminate && asyncMode) { - return; - } - - // When a packet is sent directly, perform no filtering - if(msg instanceof Packet) { - super.channelRead(ctx, msg); - } - - if (msg instanceof byte[]) { - try { - Packet p = deserializePacket((byte[]) msg); - - if (p != null) { - p = processPacket(p); - if (p != null) { - super.channelRead(ctx, p); - } - - // If we do not give minecraft time to tick, there will be dead entity artifacts left in the world - // Therefore we have to remove all loaded, dead entities manually if we are in sync mode. - // We do this after every SpawnX packet and after the destroy entities packet. - if (!asyncMode && world(mc) != null) { - //#if MC>=10904 - if (p instanceof SPacketSpawnPlayer - || p instanceof SPacketSpawnObject - || p instanceof SPacketSpawnMob - || p instanceof SPacketSpawnGlobalEntity - || p instanceof SPacketSpawnPainting - || p instanceof SPacketSpawnExperienceOrb - || p instanceof SPacketDestroyEntities) { - //#else - //$$ if (p instanceof S0CPacketSpawnPlayer - //$$ || p instanceof S0EPacketSpawnObject - //$$ || p instanceof S0FPacketSpawnMob - //$$ || p instanceof S2CPacketSpawnGlobalEntity - //$$ || p instanceof S10PacketSpawnPainting - //$$ || p instanceof S11PacketSpawnExperienceOrb - //$$ || p instanceof S13PacketDestroyEntities) { - //#endif - World world = world(mc); - for (int i = 0; i < world.loadedEntityList.size(); ++i) { - Entity entity = loadedEntityList(world).get(i); - if (entity.isDead) { - int chunkX = entity.chunkCoordX; - int chunkY = entity.chunkCoordZ; - - //#if MC>=10904 - if (entity.addedToChunk && world.getChunkProvider().getLoadedChunk(chunkX, chunkY) != null) { - //#else - //$$ if (entity.addedToChunk && world.getChunkProvider().chunkExists(chunkX, chunkY)) { - //#endif - world.getChunkFromChunkCoords(chunkX, chunkY).removeEntity(entity); - } - - world.loadedEntityList.remove(i--); - world.onEntityRemoved(entity); - } - - } - } - } - } - } catch (Exception e) { - // We'd rather not have a failure parsing one packet screw up the whole replay process - e.printStackTrace(); - } - } - - } - - private Packet deserializePacket(byte[] bytes) throws IOException, IllegalAccessException, InstantiationException { - ByteBuf bb = Unpooled.wrappedBuffer(bytes); - PacketBuffer pb = new PacketBuffer(bb); - - int i = readVarInt(pb); - - //#if MC>=10800 - Packet p = EnumConnectionState.PLAY.getPacket(EnumPacketDirection.CLIENTBOUND, i); - //#else - //$$ Packet p = Packet.generatePacket(EnumConnectionState.PLAY.func_150755_b(), i); - //#endif - p.readPacketData(pb); - - return p; - } - - /** - * Process a packet and return the result. - * @param p The packet to process - * @return The processed packet or {@code null} if no packet shall be sent - */ - protected Packet processPacket(Packet p) throws Exception { - //#if MC>=10904 - if (p instanceof SPacketCustomPayload) { - SPacketCustomPayload packet = (SPacketCustomPayload) p; - //#else - //$$ if (p instanceof S3FPacketCustomPayload) { - //$$ S3FPacketCustomPayload packet = (S3FPacketCustomPayload) p; - //#endif - //#if MC>=10800 - String channelName = packet.getChannelName(); - //#else - //$$ String channelName = packet.func_149169_c(); - //#endif - if (Restrictions.PLUGIN_CHANNEL.equals(channelName)) { - final String unknown = replayHandler.getRestrictions().handle(packet); - if (unknown == null) { - return null; - } else { - // Failed to parse options, make sure that under no circumstances further packets are parsed - terminateReplay(); - // Then end replay and show error GUI - mc.addScheduledTask(new Runnable() { - @Override - public void run() { - try { - replayHandler.endReplay(); - } catch (IOException e) { - e.printStackTrace(); - } - mc.displayGuiScreen(new GuiErrorScreen( - I18n.format("replaymod.error.unknownrestriction1"), - I18n.format("replaymod.error.unknownrestriction2", unknown) - )); - } - }); - } - } - } - //#if MC>=10904 - if (p instanceof SPacketDisconnect) { - ITextComponent reason = ((SPacketDisconnect) p).getReason(); - //#else - //$$ if (p instanceof S40PacketDisconnect) { - //#if MC>=10809 - //$$ IChatComponent reason = ((S40PacketDisconnect) p).getReason(); - //#else - //$$ IChatComponent reason = ((S40PacketDisconnect) p).func_149165_c(); - //#endif - //#endif - if ("Please update to view this replay.".equals(reason.getUnformattedText())) { - // This version of the mod supports replay restrictions so we are allowed - // to remove this packet. - return null; - } - } - - if(BAD_PACKETS.contains(p.getClass())) return null; - - //#if MC>=10904 - if (p instanceof SPacketCustomPayload) { - SPacketCustomPayload packet = (SPacketCustomPayload) p; - //#else - //$$ if (p instanceof S3FPacketCustomPayload) { - //$$ S3FPacketCustomPayload packet = (S3FPacketCustomPayload) p; - //#endif - //#if MC>=10800 - String channelName = packet.getChannelName(); - //#else - //$$ String channelName = packet.func_149169_c(); - //#endif - if ("MC|BOpen".equals(channelName)) { - return null; - } - //#if MC>=10800 - } - - //#if MC>=10904 - if(p instanceof SPacketResourcePackSend) { - SPacketResourcePackSend packet = (SPacketResourcePackSend) p; - String url = packet.getURL(); - //#else - //$$ if(p instanceof S48PacketResourcePackSend) { - //$$ S48PacketResourcePackSend packet = (S48PacketResourcePackSend) p; - //#if MC>=10809 - //$$ String url = packet.getURL(); - //#else - //$$ String url = packet.func_179783_a(); - //#endif - //#endif - if (url.startsWith("replay://")) { - //#else - //$$ String url; - //$$ if ("MC|RPack".equals(channelName) && - //$$ (url = new String(packet.func_149168_d(), Charsets.UTF_8)).startsWith("replay://")) { - //#endif - int id = Integer.parseInt(url.substring("replay://".length())); - Map index = replayFile.getResourcePackIndex(); - if (index != null) { - String hash = index.get(id); - if (hash != null) { - File file = new File(tempResourcePackFolder, hash + ".zip"); - if (!file.exists()) { - IOUtils.copy(replayFile.getResourcePack(hash).get(), new FileOutputStream(file)); - } - setServerResourcePack(mc.getResourcePackRepository(), file); - } - } - return null; - } - } - - //#if MC>=10904 - if(p instanceof SPacketJoinGame) { - SPacketJoinGame packet = (SPacketJoinGame) p; - int entId = packet.getPlayerId(); - //#else - //$$ if(p instanceof S01PacketJoinGame) { - //$$ S01PacketJoinGame packet = (S01PacketJoinGame) p; - //#if MC>=10800 - //$$ int entId = packet.getEntityId(); - //#else - //$$ int entId = packet.func_149197_c(); - //#endif - //#endif - allowMovement = true; - actualID = entId; - entId = -1789435; // Camera entity id should be negative which is an invalid id and can't be used by servers - //#if MC>=10800 - int dimension = packet.getDimension(); - EnumDifficulty difficulty = packet.getDifficulty(); - int maxPlayers = packet.getMaxPlayers(); - WorldType worldType = packet.getWorldType(); - - //#if MC>=10904 - p = new SPacketJoinGame(entId, GameType.SPECTATOR, false, dimension, - difficulty, maxPlayers, worldType, false); - //#else - //$$ p = new S01PacketJoinGame(entId, GameType.SPECTATOR, false, dimension, - //$$ difficulty, maxPlayers, worldType, false); - //#endif - //#else - //$$ int dimension = packet.func_149194_f(); - //$$ EnumDifficulty difficulty = packet.func_149192_g(); - //$$ int maxPlayers = packet.func_149193_h(); - //$$ WorldType worldType = packet.func_149196_i(); - //$$ - //$$ p = new S01PacketJoinGame(entId, GameType.ADVENTURE, false, dimension, - //$$ difficulty, maxPlayers, worldType); - //#endif - } - - //#if MC>=10904 - if(p instanceof SPacketRespawn) { - SPacketRespawn respawn = (SPacketRespawn) p; - p = new SPacketRespawn(respawn.getDimensionID(), - respawn.getDifficulty(), respawn.getWorldType(), GameType.SPECTATOR); - //#else - //$$ if(p instanceof S07PacketRespawn) { - //$$ S07PacketRespawn respawn = (S07PacketRespawn) p; - //#if MC>=10809 - //$$ p = new S07PacketRespawn(respawn.getDimensionID(), - //$$ respawn.getDifficulty(), respawn.getWorldType(), GameType.SPECTATOR); - //#else - //$$ p = new S07PacketRespawn(respawn.func_149082_c(), - //$$ respawn.func_149081_d(), respawn.func_149080_f(), - //#if MC>=10800 - //$$ GameType.SPECTATOR); - //#else - //$$ GameType.ADVENTURE); - //#endif - //#endif - //#endif - - allowMovement = true; - } - - //#if MC>=10904 - if(p instanceof SPacketPlayerPosLook) { - final SPacketPlayerPosLook ppl = (SPacketPlayerPosLook) p; - //#else - //$$ if(p instanceof S08PacketPlayerPosLook) { - //$$ final S08PacketPlayerPosLook ppl = (S08PacketPlayerPosLook) p; - //#endif - if(!hasWorldLoaded) hasWorldLoaded = true; - - if (mc.currentScreen instanceof GuiDownloadTerrain) { - // Close the world loading screen manually in case we swallow the packet - mc.displayGuiScreen(null); - } - - if(replayHandler.shouldSuppressCameraMovements()) return null; - - CameraEntity cent = replayHandler.getCameraEntity(); - - //#if MC>=10800 - //#if MC>=10904 - for (SPacketPlayerPosLook.EnumFlags relative : ppl.getFlags()) { - if (relative == SPacketPlayerPosLook.EnumFlags.X - || relative == SPacketPlayerPosLook.EnumFlags.Y - || relative == SPacketPlayerPosLook.EnumFlags.Z) { - //#else - //$$ for (Object relative : ppl.func_179834_f()) { - //$$ if (relative == S08PacketPlayerPosLook.EnumFlags.X - //$$ || relative == S08PacketPlayerPosLook.EnumFlags.Y - //$$ || relative == S08PacketPlayerPosLook.EnumFlags.Z) { - //#endif - return null; // At least one of the coordinates is relative, so we don't care - } - } - //#endif - - if(cent != null) { - //#if MC>=10809 - if(!allowMovement && !((Math.abs(cent.posX - ppl.getX()) > TP_DISTANCE_LIMIT) || - (Math.abs(cent.posZ - ppl.getZ()) > TP_DISTANCE_LIMIT))) { - //#else - //$$ if(!allowMovement && !((Math.abs(cent.posX - ppl.func_148932_c()) > TP_DISTANCE_LIMIT) || - //$$ (Math.abs(cent.posZ - ppl.func_148933_e()) > TP_DISTANCE_LIMIT))) { - //#endif - return null; - } else { - allowMovement = false; - } - } - - new Runnable() { - @Override - @SuppressWarnings("unchecked") - public void run() { - if (world(mc) == null || !mc.isCallingFromMinecraftThread()) { - ReplayMod.instance.runLater(this); - return; - } - - CameraEntity cent = replayHandler.getCameraEntity(); - //#if MC>=10809 - cent.setCameraPosition(ppl.getX(), ppl.getY(), ppl.getZ()); - //#else - //$$ cent.setCameraPosition(ppl.func_148932_c(), ppl.func_148928_d(), ppl.func_148933_e()); - //#endif - } - }.run(); - } - - //#if MC>=10904 - if(p instanceof SPacketChangeGameState) { - SPacketChangeGameState pg = (SPacketChangeGameState)p; - int reason = pg.getGameState(); - //#else - //$$ if(p instanceof S2BPacketChangeGameState) { - //$$ S2BPacketChangeGameState pg = (S2BPacketChangeGameState)p; - //#if MC>=10809 - //$$ int reason = pg.getGameState(); - //#else - //$$ int reason = pg.func_149138_c(); - //#endif - //#endif - - // only allow the following packets: - // 1 - End raining - // 2 - Begin raining - // - // The following values are to control sky color (e.g. if thunderstorm) - // 7 - Fade value - // 8 - Fade time - if(!(reason == 1 || reason == 2 || reason == 7 || reason == 8)) { - return null; - } - } - - //#if MC>=10904 - if (p instanceof SPacketChat) { - //#else - //$$ if (p instanceof S02PacketChat) { - //#endif - if (!ReplayModReplay.instance.getCore().getSettingsRegistry().get(Setting.SHOW_CHAT)) { - return null; - } - } - - return asyncMode ? processPacketAsync(p) : processPacketSync(p); - } - - @Override - @SuppressWarnings("unchecked") - public void channelActive(ChannelHandlerContext ctx) throws Exception { - this.ctx = ctx; - //#if MC>=10904 - ctx.channel().attr(NetworkManager.PROTOCOL_ATTRIBUTE_KEY).set(EnumConnectionState.PLAY); - //#else - //#if MC>=10800 - //$$ ctx.attr(NetworkManager.attrKeyConnectionState).set(EnumConnectionState.PLAY); - //#endif - //#endif - super.channelActive(ctx); - } - - @Override - public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { - // The embedded channel's event loop will consider every thread to be in it and as such provides no - // guarantees that only one thread is using the pipeline at any one time. - // For reading the replay sender (either sync or async) is the only thread ever writing. - // For writing it may very well happen that multiple threads want to use the pipline at the same time. - // It's unclear whether the EmbeddedChannel is supposed to be thread-safe (the behavior of the event loop - // does suggest that). However it seems like it either isn't (likely) or there is a race condition. - // See: https://www.replaymod.com/forum/thread/1752#post8045 (https://paste.replaymod.com/lotacatuwo) - // To work around this issue, we just outright drop all write/flush requests (they aren't needed anyway). - // This still leaves channel handlers upstream with the threading issue but they all seem to cope well with it. - promise.setSuccess(); - } - - @Override - public void flush(ChannelHandlerContext ctx) throws Exception { - // See write method above - } +public interface ReplaySender { + int currentTimeStamp(); /** * Whether the replay is currently paused. * @return {@code true} if it is paused, {@code false} otherwise */ - public boolean paused() { + public default boolean paused() { + Minecraft mc = getMinecraft(); //#if MC>=11200 return mc.timer.tickLength == Float.POSITIVE_INFINITY; //#else @@ -742,414 +20,13 @@ public class ReplaySender extends ChannelDuplexHandler { //#endif } - /** - * Returns the speed of the replay. 1 being normal speed, 0.5 half and 2 twice as fast. - * If 0 is returned, the replay is paused. - * @return speed multiplier - */ - public double getReplaySpeed() { - if(!paused()) return replaySpeed; - else return 0; - } + void setReplaySpeed(double factor); + double getReplaySpeed(); - /** - * Set the speed of the replay. 1 being normal speed, 0.5 half and 2 twice as fast. - * The speed may not be set to 0 nor to negative values. - * @param d Speed multiplier - */ - public void setReplaySpeed(final double d) { - if(d != 0) this.replaySpeed = d; - //#if MC>=11200 - mc.timer.tickLength = WrappedTimer.DEFAULT_MS_PER_TICK / (float) d; - //#else - //$$ mc.timer.timerSpeed = (float) d; - //#endif - } + boolean isAsyncMode(); + void setAsyncMode(boolean async); + void setSyncModeAndWait(); - ///////////////////////////////////////////////////////// - // Asynchronous packet processing // - ///////////////////////////////////////////////////////// - - /** - * The real time at which the last packet was sent in milliseconds. - */ - private long lastPacketSent; - - /** - * There is no waiting performed until a packet with at least this timestamp is reached (but not yet sent). - * If this is -1, then timing is normal. - */ - private long desiredTimeStamp = -1; - - /** - * Runnable which performs timed dispatching of packets from the input stream. - */ - private Runnable asyncSender = new Runnable() { - public void run() { - try { - while (ctx == null && !terminate) { - Thread.sleep(10); - } - REPLAY_LOOP: - while (!terminate) { - synchronized (ReplaySender.this) { - if (replayIn == null) { - replayIn = replayFile.getPacketData(); - } - // Packet loop - while (true) { - try { - // When playback is paused and the world has loaded (we don't want any dirt-screens) we sleep - while (paused() && hasWorldLoaded) { - // Unless we are going to terminate, restart or jump - if (terminate || startFromBeginning || desiredTimeStamp != -1) { - break; - } - Thread.sleep(10); - } - - if (terminate) { - break REPLAY_LOOP; - } - - if (startFromBeginning) { - // In case we need to restart from the beginning - // break out of the loop sending all packets which will - // cause the replay to be restarted by the outer loop - break; - } - - // Read the next packet if we don't already have one - if (nextPacket == null) { - nextPacket = new PacketData(replayIn); - } - - int nextTimeStamp = nextPacket.timestamp; - - // If we aren't jumping and the world has already been loaded (no dirt-screens) then wait - // the required amount to get proper packet timing - if (!isHurrying() && hasWorldLoaded) { - // How much time should have passed - int timeWait = (int) Math.round((nextTimeStamp - lastTimeStamp) / replaySpeed); - // How much time did pass - long timeDiff = System.currentTimeMillis() - lastPacketSent; - // How much time we need to wait to make up for the difference - long timeToSleep = Math.max(0, timeWait - timeDiff); - - Thread.sleep(timeToSleep); - lastPacketSent = System.currentTimeMillis(); - } - - // Process packet - channelRead(ctx, nextPacket.bytes); - nextPacket = null; - - lastTimeStamp = nextTimeStamp; - - // In case we finished jumping - // We need to check that we aren't planing to restart so we don't accidentally run this - // code before we actually restarted - if (isHurrying() && lastTimeStamp > desiredTimeStamp && !startFromBeginning) { - desiredTimeStamp = -1; - - replayHandler.moveCameraToTargetPosition(); - - // Pause after jumping - setReplaySpeed(0); - } - } catch (EOFException eof) { - // Reached end of file - // Pause the replay which will cause it to freeze before getting restarted - setReplaySpeed(0); - // Then wait until the user tells us to continue - while (paused() && hasWorldLoaded && desiredTimeStamp == -1 && !terminate) { - Thread.sleep(10); - } - break; - } catch (IOException e) { - e.printStackTrace(); - } - } - - // Restart the replay. - hasWorldLoaded = false; - lastTimeStamp = 0; - startFromBeginning = false; - nextPacket = null; - lastPacketSent = System.currentTimeMillis(); - replayHandler.restartedReplay(); - if (replayIn != null) { - replayIn.close(); - replayIn = null; - } - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - }; - - /** - * Return whether this replay sender is currently rushing. When rushing, all packets are sent without waiting until - * a specified timestamp is passed. - * @return {@code true} if currently rushing, {@code false} otherwise - */ - public boolean isHurrying() { - return desiredTimeStamp != -1; - } - - /** - * Cancels the hurrying. - */ - public void stopHurrying() { - desiredTimeStamp = -1; - } - - /** - * Return the timestamp to which this replay sender is currently rushing. All packets with an lower or equal - * timestamp will be sent out without any sleeping. - * @return The timestamp in milliseconds since the start of the replay - */ - public long getDesiredTimestamp() { - return desiredTimeStamp; - } - - /** - * Jumps to the specified timestamp when in async mode by rushing all packets until one with a timestamp greater - * than the specified timestamp is found. - * If the timestamp has already passed, this causes the replay to restart and then rush all packets. - * @param millis Timestamp in milliseconds since the start of the replay - */ - public void jumpToTime(int millis) { - Preconditions.checkState(asyncMode, "Can only jump in async mode. Use sendPacketsTill(int) instead."); - if(millis < lastTimeStamp && !isHurrying()) { - startFromBeginning = true; - } - - desiredTimeStamp = millis; - } - - protected Packet processPacketAsync(Packet p) { - //If hurrying, ignore some packets, except for short durations - if(desiredTimeStamp - lastTimeStamp > 1000) { - //#if MC>=10904 - if(p instanceof SPacketParticles) return null; - - if(p instanceof SPacketSpawnObject) { - SPacketSpawnObject pso = (SPacketSpawnObject)p; - int type = pso.getType(); - //#else - //$$ if(p instanceof S2APacketParticles) return null; - //$$ - //$$ if(p instanceof S0EPacketSpawnObject) { - //$$ S0EPacketSpawnObject pso = (S0EPacketSpawnObject)p; - //#if MC>=10809 - //$$ int type = pso.getType(); - //#else - //$$ int type = pso.func_148993_l(); - //#endif - //#endif - if(type == 76) { // Firework rocket - return null; - } - } - } - return p; - } - - ///////////////////////////////////////////////////////// - // Synchronous packet processing // - ///////////////////////////////////////////////////////// - - /** - * Sends all packets until the specified timestamp is reached (inclusive). - * If the timestamp is smaller than the last packet sent, the replay is restarted from the beginning. - * @param timestamp The timestamp in milliseconds since the beginning of this replay - */ - public void sendPacketsTill(int timestamp) { - Preconditions.checkState(!asyncMode, "This method cannot be used in async mode. Use jumpToTime(int) instead."); - try { - while (ctx == null && !terminate) { // Make sure channel is ready - Thread.sleep(10); - } - - synchronized (this) { - if (timestamp == lastTimeStamp) { // Do nothing if we're already there - return; - } - if (timestamp < lastTimeStamp) { // Restart the replay if we need to go backwards in time - hasWorldLoaded = false; - lastTimeStamp = 0; - if (replayIn != null) { - replayIn.close(); - replayIn = null; - } - startFromBeginning = false; - nextPacket = null; - replayHandler.restartedReplay(); - } - - if (replayIn == null) { - replayIn = replayFile.getPacketData(); - } - - while (true) { // Send packets - try { - PacketData pd; - if (nextPacket != null) { - // If there is still a packet left from before, use it first - pd = nextPacket; - nextPacket = null; - } else { - // Otherwise read one from the input stream - pd = new PacketData(replayIn); - } - - int nextTimeStamp = pd.timestamp; - if (nextTimeStamp > timestamp) { - // We are done sending all packets - nextPacket = pd; - break; - } - - // Process packet - channelRead(ctx, pd.bytes); - } catch (EOFException eof) { - // Shit! We hit the end before finishing our job! What shall we do now? - // well, let's just pretend we're done... - replayIn = null; - break; - } catch (IOException e) { - e.printStackTrace(); - } - } - - // This might be required if we change to async mode anytime soon - lastPacketSent = System.currentTimeMillis(); - lastTimeStamp = timestamp; - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - protected Packet processPacketSync(Packet p) { - //#if MC>=10904 - if (p instanceof SPacketUnloadChunk) { - SPacketUnloadChunk packet = (SPacketUnloadChunk) p; - int x = packet.getX(); - int z = packet.getZ(); - //#else - //#if MC>=10809 - //$$ if (p instanceof S21PacketChunkData && ((S21PacketChunkData) p).getExtractedSize() == 0) { - //$$ S21PacketChunkData packet = (S21PacketChunkData) p; - //$$ int x = packet.getChunkX(); - //$$ int z = packet.getChunkZ(); - //#else - //$$ if (p instanceof S21PacketChunkData && ((S21PacketChunkData) p).func_149276_g() == 0) { - //$$ S21PacketChunkData packet = (S21PacketChunkData) p; - //$$ int x = packet.func_149273_e(); - //$$ int z = packet.func_149271_f(); - //#endif - //#endif - // If the chunk is getting unloaded, we will have to forcefully update the position of all entities - // within. Otherwise, if there wasn't a game tick recently, there may be entities that have moved - // out of the chunk by now but are still registered in it. If we do not update those, they will get - // unloaded even though they shouldn't. - // Note: This is only half of the truth. Entities may be removed by chunk-unloading, see else-case below. - // To make things worse, it seems like players were never supposed to be unloaded this way because - // they will remain glitched in the World#playerEntities list. - World world = world(mc); - IChunkProvider chunkProvider = world.getChunkProvider(); - // Get the chunk that will be unloaded - Chunk chunk = chunkProvider.provideChunk(x, z); - if (!chunk.isEmpty()) { - List entitiesInChunk = new ArrayList<>(); - // Gather all entities in that chunk - for (Collection entityList : getEntityLists(chunk)) { - entitiesInChunk.addAll(entityList); - } - for (Entity entity : entitiesInChunk) { - // Skip interpolation of position updates coming from server - // (See: newX in EntityLivingBase or otherPlayerMPX in EntityOtherPlayerMP) - // Needs to be called at least 4 times thanks to - // EntityOtherPlayerMP#otherPlayerMPPosRotationIncrements (max vanilla value is 3) - for (int i = 0; i < 4; i++) { - entity.onUpdate(); - } - - // Check whether the entity has left the chunk - int chunkX = floor(entity.posX / 16); - int chunkZ = floor(entity.posZ / 16); - if (entity.chunkCoordX != chunkX || entity.chunkCoordZ != chunkZ) { - // Entity has left the chunk - chunk.removeEntityAtIndex(entity, entity.chunkCoordY); - //#if MC>=10904 - Chunk newChunk = chunkProvider.getLoadedChunk(chunkX, chunkZ); - //#else - //$$ Chunk newChunk = chunkProvider.chunkExists(chunkX, chunkZ) - //$$ ? chunkProvider.provideChunk(chunkX, chunkZ) : null; - //#endif - if (newChunk != null) { - newChunk.addEntity(entity); - } else { - // Entity has left all loaded chunks - entity.addedToChunk = false; - } - } else { - // When entities remain in a chunk that's to be unloaded, they'll only be added to a unload - // queue and remain loaded as before until the next tick (which during jumping is way off). - // So, if they are re-spawned with the same entity id, MC actually cleans up the old entity and - // then adds the new one but leaves the unload queue as is. - // Finally, on the next tick the legitimate entity will be unloaded because it's part of the - // unload queue (entities .equals based purely on their id). However, the old entity object - // is used to determine the chunk the entity is removed from and in this case that'll allow the - // legitimate entity to remain registered in a loaded chunk, causing them to still be rendered. - // - // The usual removal-due-to-chunk-unload process will, without touching the entityList, call - // onEntityRemoved. In that method WorldClient checks to see whether the entity is still in the - // entityList (which it is) and then adds it to the entitySpawnQueue. - // As the final result the entity will remain loaded. - // To get the same result without ticking, we just remove the entity from the to-be-unloaded - // chunk but keep it loaded otherwise. They won't be rendered because they're not part of any - // chunk and will be removed properly if the server decides to re-spawn the entity. - chunk.removeEntityAtIndex(entity, entity.chunkCoordY); - entity.addedToChunk = false; - } - } - } - } - return p; // During synchronous playback everything is sent normally - } - - private static final class PacketData { - private static final ByteBuf byteBuf = Unpooled.buffer(); - private static final ByteBufOutputStream byteBufOut = new ByteBufOutputStream(byteBuf); - private static final ReplayOutputStream encoder = new ReplayOutputStream(new ReplayStudio(), byteBufOut); - private final int timestamp; - private final byte[] bytes; - - public PacketData(ReplayInputStream in) throws IOException { - com.replaymod.replaystudio.PacketData data = in.readPacket(); - timestamp = (int) data.getTime(); - // We need to re-encode MCProtocolLib packets, so we can later decode them as NMS packets - // The main reason we aren't reading them as NMS packets is that we want ReplayStudio to be able - // to apply ViaVersion (and potentially other magic) to it. - synchronized (encoder) { - byteBuf.markReaderIndex(); // Mark the current reader and writer index (should be at start) - byteBuf.markWriterIndex(); - - encoder.write(data); // Re-encode packet, data will end up in byteBuf - encoder.flush(); - - byteBuf.skipBytes(8); // Skip packet length & timestamp - bytes = new byte[byteBuf.readableBytes()]; // Create bytes array of sufficient size - byteBuf.readBytes(bytes); // Read all data into bytes - - byteBuf.resetReaderIndex(); // Reset reader & writer index for next use - byteBuf.resetWriterIndex(); - } - } - } + void jumpToTime(int value); // async + void sendPacketsTill(int replayTime); // sync } diff --git a/src/main/java/com/replaymod/replay/camera/CameraControllerRegistry.java b/src/main/java/com/replaymod/replay/camera/CameraControllerRegistry.java index 1a3a31df..fcb937f3 100644 --- a/src/main/java/com/replaymod/replay/camera/CameraControllerRegistry.java +++ b/src/main/java/com/replaymod/replay/camera/CameraControllerRegistry.java @@ -2,7 +2,9 @@ package com.replaymod.replay.camera; import com.google.common.base.Function; import com.google.common.base.Preconditions; +import com.replaymod.replay.Setting; +import java.util.ArrayList; import java.util.Collections; import java.util.Map; import java.util.Set; @@ -14,6 +16,7 @@ public class CameraControllerRegistry { public void register(String name, Function constructor) { Preconditions.checkState(!constructors.containsKey(name), "Controller " + name + " is already registered."); constructors.put(name, constructor); + Setting.CAMERA.setChoices(new ArrayList<>(getControllers())); } public Set getControllers() { diff --git a/src/main/java/com/replaymod/replay/camera/CameraEntity.java b/src/main/java/com/replaymod/replay/camera/CameraEntity.java index fecd2ef0..9fd861e4 100755 --- a/src/main/java/com/replaymod/replay/camera/CameraEntity.java +++ b/src/main/java/com/replaymod/replay/camera/CameraEntity.java @@ -3,20 +3,19 @@ package com.replaymod.replay.camera; import com.replaymod.core.ReplayMod; import com.replaymod.core.events.SettingsChangedEvent; import com.replaymod.core.utils.Utils; +import com.replaymod.core.versions.MCVer; import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.Setting; import com.replaymod.replay.events.ReplayChatMessageEvent; import com.replaymod.replaystudio.util.Location; import lombok.Getter; import lombok.Setter; -import net.minecraft.block.material.Material; import net.minecraft.client.Minecraft; import net.minecraft.client.entity.AbstractClientPlayer; import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.client.settings.KeyBinding; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLiving; -import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.item.EntityItemFrame; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.ResourceLocation; @@ -26,10 +25,21 @@ import net.minecraftforge.client.event.RenderGameOverlayEvent; import net.minecraftforge.client.event.RenderHandEvent; import net.minecraftforge.common.MinecraftForge; +//#if MC>=11300 +import net.minecraft.util.math.RayTraceFluidMode; +//#else +//$$ import net.minecraft.block.material.Material; +//$$ import net.minecraft.entity.EntityLivingBase; +//#endif + //#if MC>=10904 import net.minecraft.inventory.EntityEquipmentSlot; //#if MC>=11200 -import net.minecraft.stats.RecipeBook; +//#if MC>=11300 +import net.minecraft.client.util.RecipeBookClient; +//#else +//$$ import net.minecraft.stats.RecipeBook; +//#endif //#endif import net.minecraft.stats.StatisticsManager; import net.minecraft.util.EnumHand; @@ -45,9 +55,14 @@ import net.minecraft.util.text.ITextComponent; //#if MC>=10800 import net.minecraft.client.entity.EntityPlayerSP; -import net.minecraftforge.fml.common.eventhandler.EventPriority; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.EventPriority; +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.EventPriority; //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; @@ -97,9 +112,13 @@ public class CameraEntity */ private final EventHandler eventHandler = new EventHandler(); + //#if MC>=11300 + public CameraEntity(Minecraft mcIn, World worldIn, NetHandlerPlayClient netHandlerPlayClient, StatisticsManager statisticsManager, RecipeBookClient recipeBookClient) { + super(mcIn, worldIn, netHandlerPlayClient, statisticsManager, recipeBookClient); + //#else //#if MC>=11200 - public CameraEntity(Minecraft mcIn, World worldIn, NetHandlerPlayClient netHandlerPlayClient, StatisticsManager statisticsManager, RecipeBook recipeBook) { - super(mcIn, worldIn, netHandlerPlayClient, statisticsManager, recipeBook); + //$$ public CameraEntity(Minecraft mcIn, World worldIn, NetHandlerPlayClient netHandlerPlayClient, StatisticsManager statisticsManager, RecipeBook recipeBook) { + //$$ super(mcIn, worldIn, netHandlerPlayClient, statisticsManager, recipeBook); //#else //#if MC>=10904 //$$ public CameraEntity(Minecraft mcIn, World worldIn, NetHandlerPlayClient netHandlerPlayClient, StatisticsManager statisticsManager) { @@ -113,6 +132,7 @@ public class CameraEntity //$$ super(mcIn, worldIn, session, netHandlerPlayClient, statFileWriter); //#endif //#endif + //#endif //#endif FORGE_BUS.register(eventHandler); FML_BUS.register(eventHandler); @@ -196,7 +216,11 @@ public class CameraEntity private void updateBoundingBox() { //#if MC>=10800 - setEntityBoundingBox(new AxisAlignedBB( + //#if MC>=11300 + setBoundingBox(new AxisAlignedBB( + //#else + //$$ setEntityBoundingBox(new AxisAlignedBB( + //#endif //#else //$$ this.boundingBox.setBB(AxisAlignedBB.getBoundingBox( //#endif @@ -205,7 +229,11 @@ public class CameraEntity } @Override - public void onUpdate() { + //#if MC>=11300 + public void tick() { + //#else + //$$ public void onUpdate() { + //#endif //#if MC>=10800 Entity view = getRenderViewEntity(mc); //#else @@ -262,10 +290,12 @@ public class CameraEntity return falseUnlessSpectating(Entity::isEntityInsideOpaqueBlock); // Make sure no suffocation overlay is rendered } - @Override - public boolean isInsideOfMaterial(Material materialIn) { - return falseUnlessSpectating(e -> e.isInsideOfMaterial(materialIn)); // Make sure no overlays are rendered - } + //#if MC<11300 + //$$ @Override + //$$ public boolean isInsideOfMaterial(Material materialIn) { + //$$ return falseUnlessSpectating(e -> e.isInsideOfMaterial(materialIn)); // Make sure no overlays are rendered + //$$ } + //#endif //#if MC>=10800 @Override @@ -405,19 +435,33 @@ public class CameraEntity return super.isHandActive(); } + //#if MC>=11300 @Override - public RayTraceResult rayTrace(double p_174822_1_, float p_174822_3_) { - RayTraceResult pos = super.rayTrace(p_174822_1_, 1f); + public RayTraceResult rayTrace(double blockReachDistance, float partialTicks, RayTraceFluidMode p_174822_4_) { + RayTraceResult pos = super.rayTrace(blockReachDistance, partialTicks, p_174822_4_); // Make sure we can never look at blocks (-> no outline) - if(pos != null && pos.typeOfHit == RayTraceResult.Type.BLOCK) { - pos.typeOfHit = RayTraceResult.Type.MISS; + if(pos != null && pos.type == RayTraceResult.Type.BLOCK) { + pos.type = RayTraceResult.Type.MISS; } return pos; } //#else //$$ @Override + //$$ public RayTraceResult rayTrace(double p_174822_1_, float p_174822_3_) { + //$$ RayTraceResult pos = super.rayTrace(p_174822_1_, 1f); + //$$ + //$$ // Make sure we can never look at blocks (-> no outline) + //$$ if(pos != null && pos.typeOfHit == RayTraceResult.Type.BLOCK) { + //$$ pos.typeOfHit = RayTraceResult.Type.MISS; + //$$ } + //$$ + //$$ return pos; + //$$ } + //#endif + //#else + //$$ @Override //$$ public MovingObjectPosition rayTrace(double p_174822_1_, float p_174822_3_) { //$$ MovingObjectPosition pos = super.rayTrace(p_174822_1_, 1f); //$$ @@ -430,15 +474,22 @@ public class CameraEntity //$$ } //#endif - @Override - public void openGui(Object mod, int modGuiId, World world, int x, int y, int z) { - // Do not open any block GUIs for the camera entities - // Note: Vanilla GUIs are filtered out on a packet level, this only applies to mod GUIs - } + //#if MC<11300 + //$$ @Override + //$$ public void openGui(Object mod, int modGuiId, World world, int x, int y, int z) { + //$$ // Do not open any block GUIs for the camera entities + //$$ // Note: Vanilla GUIs are filtered out on a packet level, this only applies to mod GUIs + //$$ } + //#endif @Override - public void setDead() { - super.setDead(); + //#if MC>=11300 + public void remove() { + super.remove(); + //#else + //$$ public void setDead() { + //$$ super.setDead(); + //#endif FORGE_BUS.unregister(eventHandler); FML_BUS.unregister(eventHandler); } @@ -530,11 +581,11 @@ public class CameraEntity @SubscribeEvent public void preCrosshairRender(RenderGameOverlayEvent.Pre event) { // The crosshair should only render if targeted entity can actually be spectated - if (getType(event) == RenderGameOverlayEvent.ElementType.CROSSHAIRS) { + if (MCVer.getType(event) == RenderGameOverlayEvent.ElementType.CROSSHAIRS) { event.setCanceled(!canSpectate(mc.pointedEntity)); } // Hotbar should never be rendered - if (getType(event) == RenderGameOverlayEvent.ElementType.HOTBAR) { + if (MCVer.getType(event) == RenderGameOverlayEvent.ElementType.HOTBAR) { event.setCanceled(true); } } @@ -605,7 +656,7 @@ public class CameraEntity @SubscribeEvent public void preRenderGameOverlay(RenderGameOverlayEvent.Pre event) { - switch (getType(event)) { + switch (MCVer.getType(event)) { case ALL: heldItemTooltipsWasTrue = mc.gameSettings.heldItemTooltips; mc.gameSettings.heldItemTooltips = false; @@ -641,7 +692,7 @@ public class CameraEntity @SubscribeEvent public void postRenderGameOverlay(RenderGameOverlayEvent.Post event) { - if (getType(event) != RenderGameOverlayEvent.ElementType.ALL) return; + if (MCVer.getType(event) != RenderGameOverlayEvent.ElementType.ALL) return; mc.gameSettings.heldItemTooltips = heldItemTooltipsWasTrue; } } diff --git a/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java b/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java index 621d1cbe..e9b609cd 100644 --- a/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java +++ b/src/main/java/com/replaymod/replay/camera/ClassicCameraController.java @@ -1,9 +1,8 @@ package com.replaymod.replay.camera; -import net.minecraft.client.Minecraft; import net.minecraft.client.settings.KeyBinding; -import org.lwjgl.Sys; -import org.lwjgl.util.vector.Vector3f; + +import javax.vecmath.Vector3f; import static com.replaymod.core.versions.MCVer.*; @@ -22,7 +21,7 @@ public class ClassicCameraController implements CameraController { private Vector3f direction; private Vector3f dirBefore; private double motion; - private long lastCall = Sys.getTime(); + private long lastCall = System.currentTimeMillis(); private boolean speedup = false; @@ -34,7 +33,7 @@ public class ClassicCameraController implements CameraController { public void update(float partialTicksPassed) { boolean forward = false, backward = false, left = false, right = false, up = false, down = false; speedup = false; - for(KeyBinding kb : Minecraft.getMinecraft().gameSettings.keyBindings) { + for(KeyBinding kb : getMinecraft().gameSettings.keyBindings) { if(!isKeyDown(kb)) continue; if(kb.getKeyDescription().equals("key.forward")) { forward = true; @@ -109,7 +108,7 @@ public class ClassicCameraController implements CameraController { } private void updateMovement() { - long frac = Sys.getTime() - lastCall; + long frac = System.currentTimeMillis() - lastCall; if(frac == 0) return; @@ -124,13 +123,14 @@ public class ClassicCameraController implements CameraController { motion = Math.min(motion, MAX_SPEED); - lastCall = Sys.getTime(); + lastCall = System.currentTimeMillis(); - if(direction == null || motion < THRESHOLD) { + if(direction == null || direction.lengthSquared() == 0 || motion < THRESHOLD) { return; } - Vector3f movement = direction.normalise(null); + Vector3f movement = new Vector3f(direction); + movement.normalize(); double factor = motion * (frac / 1000D); camera.moveCamera(movement.x * factor, movement.y * factor, movement.z * factor); @@ -162,7 +162,9 @@ public class ClassicCameraController implements CameraController { Vector3f dbf = direction; if(dirBefore != null) { - direction = Vector3f.add(direction, dirBefore.normalise(null), null); + dirBefore.normalize(); + dirBefore.add(direction); + direction = dirBefore; } dirBefore = dbf; diff --git a/src/main/java/com/replaymod/replay/camera/SpectatorCameraController.java b/src/main/java/com/replaymod/replay/camera/SpectatorCameraController.java index d66f954e..256440f7 100644 --- a/src/main/java/com/replaymod/replay/camera/SpectatorCameraController.java +++ b/src/main/java/com/replaymod/replay/camera/SpectatorCameraController.java @@ -6,7 +6,12 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.settings.KeyBinding; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; -import org.lwjgl.input.Mouse; + +//#if MC>=11300 +// FIXME +//#else +//$$ import org.lwjgl.input.Mouse; +//#endif import java.util.Arrays; @@ -18,7 +23,7 @@ public class SpectatorCameraController implements CameraController { @Override public void update(float partialTicksPassed) { - Minecraft mc = Minecraft.getMinecraft(); + Minecraft mc = getMinecraft(); if (mc.gameSettings.keyBindSneak.isPressed()) { ReplayModReplay.instance.getReplayHandler().spectateCamera(); } @@ -31,7 +36,11 @@ public class SpectatorCameraController implements CameraController { } // Prevent mouse movement - Mouse.updateCursor(); + //#if MC>=11300 + // FIXME + //#else + //$$ Mouse.updateCursor(); + //#endif // Always make sure the camera is in the exact same spot as the spectated entity // This is necessary as some rendering code for the hand doesn't respect the view entity diff --git a/src/main/java/com/replaymod/replay/camera/VanillaCameraController.java b/src/main/java/com/replaymod/replay/camera/VanillaCameraController.java index 7e1085e1..803322f8 100644 --- a/src/main/java/com/replaymod/replay/camera/VanillaCameraController.java +++ b/src/main/java/com/replaymod/replay/camera/VanillaCameraController.java @@ -1,9 +1,15 @@ package com.replaymod.replay.camera; import net.minecraft.client.Minecraft; -import net.minecraft.client.settings.GameSettings; import net.minecraft.client.settings.KeyBinding; -import org.lwjgl.util.vector.Vector3f; + +//#if MC>=11300 +import net.minecraft.client.GameSettings; +//#else +//$$ import net.minecraft.client.settings.GameSettings; +//#endif + +import javax.vecmath.Vector3f; import static com.replaymod.core.versions.MCVer.*; @@ -42,11 +48,11 @@ public class VanillaCameraController implements CameraController { Vector3f direction = new Vector3f(0, 0, 0); for (int i = 0; i < 6; i++) { // First, get movement direction depending on keys pressed if (isKeyDown(bindings[i])) { - Vector3f.add(direction, DIRECTIONS[i], direction); + direction.add(DIRECTIONS[i]); } } if (direction.length() == 0) return; - direction.normalise(direction); // Normalize, so we don't move quicker if we hold down multiple keys + direction.normalize(); // Normalize, so we don't move quicker if we hold down multiple keys double yawRadians = Math.toRadians(camera.rotationYaw); float yawSin = (float) Math.sin(yawRadians), yawCos = (float) Math.cos(yawRadians); // Rotate by yaw diff --git a/src/main/java/com/replaymod/replay/events/ReplayChatMessageEvent.java b/src/main/java/com/replaymod/replay/events/ReplayChatMessageEvent.java index 9ad07adf..2c096a45 100644 --- a/src/main/java/com/replaymod/replay/events/ReplayChatMessageEvent.java +++ b/src/main/java/com/replaymod/replay/events/ReplayChatMessageEvent.java @@ -5,8 +5,13 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.Cancelable; -import net.minecraftforge.fml.common.eventhandler.Event; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.Cancelable; +import net.minecraftforge.eventbus.api.Event; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.Cancelable; +//$$ import net.minecraftforge.fml.common.eventhandler.Event; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.Cancelable; //$$ import cpw.mods.fml.common.eventhandler.Event; diff --git a/src/main/java/com/replaymod/replay/events/ReplayCloseEvent.java b/src/main/java/com/replaymod/replay/events/ReplayCloseEvent.java index b228c675..23c0fc1d 100644 --- a/src/main/java/com/replaymod/replay/events/ReplayCloseEvent.java +++ b/src/main/java/com/replaymod/replay/events/ReplayCloseEvent.java @@ -5,7 +5,11 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.Event; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.Event; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.Event; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.Event; //#endif diff --git a/src/main/java/com/replaymod/replay/events/ReplayDispatchKeypressesEvent.java b/src/main/java/com/replaymod/replay/events/ReplayDispatchKeypressesEvent.java index 5d972056..d769747d 100644 --- a/src/main/java/com/replaymod/replay/events/ReplayDispatchKeypressesEvent.java +++ b/src/main/java/com/replaymod/replay/events/ReplayDispatchKeypressesEvent.java @@ -1,8 +1,13 @@ package com.replaymod.replay.events; //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.Cancelable; -import net.minecraftforge.fml.common.eventhandler.Event; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.Cancelable; +import net.minecraftforge.eventbus.api.Event; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.Cancelable; +//$$ import net.minecraftforge.fml.common.eventhandler.Event; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.Cancelable; //$$ import cpw.mods.fml.common.eventhandler.Event; diff --git a/src/main/java/com/replaymod/replay/events/ReplayOpenEvent.java b/src/main/java/com/replaymod/replay/events/ReplayOpenEvent.java index 0e305532..6aef11ce 100644 --- a/src/main/java/com/replaymod/replay/events/ReplayOpenEvent.java +++ b/src/main/java/com/replaymod/replay/events/ReplayOpenEvent.java @@ -5,7 +5,11 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.Event; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.Event; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.Event; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.Event; //#endif diff --git a/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java b/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java index cc94f3cc..12332b05 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiEditMarkerPopup.java @@ -1,7 +1,6 @@ package com.replaymod.replay.gui.overlay; import com.google.common.base.Strings; -import com.replaymod.replay.ReplayHandler; import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.element.*; @@ -12,16 +11,22 @@ import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.utils.Colors; import com.replaymod.replaystudio.data.Marker; -import org.lwjgl.input.Keyboard; -import org.lwjgl.util.ReadablePoint; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint; + +//#if MC>=11300 +import com.replaymod.core.versions.MCVer.Keyboard; +//#else +//$$ import org.lwjgl.input.Keyboard; +//#endif + +import java.util.function.Consumer; public class GuiEditMarkerPopup extends AbstractGuiPopup implements Typeable { private static GuiNumberField newGuiNumberField() { return new GuiNumberField().setSize(150, 20).setValidateOnFocusChange(true); } - private final ReplayHandler replayHandler; - private final Marker marker; + private final Consumer onSave; public final GuiLabel title = new GuiLabel().setI18nText("replaymod.gui.editkeyframe.title.marker"); @@ -60,6 +65,7 @@ public class GuiEditMarkerPopup extends AbstractGuiPopup imp public final GuiButton saveButton = new GuiButton().onClick(new Runnable() { @Override public void run() { + Marker marker = new Marker(); marker.setName(Strings.emptyToNull(nameField.getText())); marker.setTime(timeField.getInteger()); marker.setX(xField.getDouble()); @@ -68,7 +74,7 @@ public class GuiEditMarkerPopup extends AbstractGuiPopup imp marker.setYaw(yawField.getFloat()); marker.setPitch(pitchField.getFloat()); marker.setRoll(rollField.getFloat()); - replayHandler.saveMarkers(); + onSave.accept(marker); close(); } }).setSize(150, 20).setI18nLabel("replaymod.gui.save"); @@ -84,10 +90,9 @@ public class GuiEditMarkerPopup extends AbstractGuiPopup imp .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(7)) .addElements(new HorizontalLayout.Data(0.5), saveButton, cancelButton); - public GuiEditMarkerPopup(ReplayHandler replayHandler, GuiContainer container, Marker marker) { + public GuiEditMarkerPopup(GuiContainer container, Marker marker, Consumer onSave) { super(container); - this.replayHandler = replayHandler; - this.marker = marker; + this.onSave = onSave; setBackgroundColor(Colors.DARK_TRANSPARENT); diff --git a/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java b/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java index 0a376fbf..0cf96d05 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiMarkerTimeline.java @@ -2,27 +2,46 @@ package com.replaymod.replay.gui.overlay; import com.replaymod.core.ReplayMod; import com.replaymod.replay.ReplayHandler; +import com.replaymod.replay.ReplayModReplay; import com.replaymod.replaystudio.data.Marker; import com.replaymod.replaystudio.util.Location; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.element.advanced.AbstractGuiTimeline; import de.johni0702.minecraft.gui.function.Draggable; +import de.johni0702.minecraft.gui.function.Typeable; +import de.johni0702.minecraft.gui.utils.lwjgl.Point; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint; import net.minecraft.client.resources.I18n; -import org.lwjgl.util.Point; -import org.lwjgl.util.ReadableDimension; -import org.lwjgl.util.ReadablePoint; + +//#if MC>=11300 +import com.replaymod.core.versions.MCVer.Keyboard; +//#else +//$$ import org.lwjgl.input.Keyboard; +//#endif + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; import static de.johni0702.minecraft.gui.utils.Utils.clamp; -public class GuiMarkerTimeline extends AbstractGuiTimeline implements Draggable { +public class GuiMarkerTimeline extends AbstractGuiTimeline implements Draggable, Typeable { protected static final int TEXTURE_MARKER_X = 109; protected static final int TEXTURE_MARKER_Y = 20; protected static final int TEXTURE_MARKER_SELECTED_X = 114; protected static final int TEXTURE_MARKER_SELECTED_Y = 20; protected static final int MARKER_SIZE = 5; + @Nullable private final ReplayHandler replayHandler; + private final Consumer> saveMarkers; + protected Set markers; private ReadableDimension lastSize; @@ -32,8 +51,27 @@ public class GuiMarkerTimeline extends AbstractGuiTimeline im private boolean dragging; private long lastClickTime; - public GuiMarkerTimeline(ReplayHandler replayHandler) { + public GuiMarkerTimeline(@Nonnull ReplayHandler replayHandler) { this.replayHandler = replayHandler; + try { + this.markers = replayHandler.getReplayFile().getMarkers().or(HashSet::new); + } catch (IOException e) { + ReplayModReplay.LOGGER.error("Failed to get markers from replay", e); + this.markers = new HashSet<>(); + } + this.saveMarkers = (markers) -> { + try { + replayHandler.getReplayFile().writeMarkers(markers); + } catch (IOException e) { + ReplayModReplay.LOGGER.error("Failed to save markers to replay", e); + } + }; + } + + public GuiMarkerTimeline(Set markers, Consumer> saveMarkers) { + this.replayHandler = null; + this.markers = markers; + this.saveMarkers = saveMarkers; } @Override @@ -52,7 +90,7 @@ public class GuiMarkerTimeline extends AbstractGuiTimeline im protected void drawMarkers(GuiRenderer renderer, ReadableDimension size) { renderer.bindTexture(ReplayMod.TEXTURE); - for (Marker marker : replayHandler.getMarkers()) { + for (Marker marker : markers) { drawMarker(renderer, size, marker); } } @@ -63,7 +101,10 @@ public class GuiMarkerTimeline extends AbstractGuiTimeline im double positionInVisible = markerPos - getOffset(); double fractionOfVisible = positionInVisible / visibleLength; int markerX = (int) (BORDER_LEFT + fractionOfVisible * (size.getWidth() - BORDER_LEFT - BORDER_RIGHT)); + drawMarker(renderer, size, marker, markerX); + } + protected void drawMarker(GuiRenderer renderer, ReadableDimension size, Marker marker, int markerX) { int textureX, textureY; if (marker.equals(selectedMarker)) { textureX = TEXTURE_MARKER_SELECTED_X; @@ -99,7 +140,7 @@ public class GuiMarkerTimeline extends AbstractGuiTimeline im int visibleLength = (int) (getLength() * getZoom()); int contentWidth = lastSize.getWidth() - BORDER_LEFT - BORDER_RIGHT; - for (Marker marker : replayHandler.getMarkers()) { + for (Marker marker : markers) { int markerPos = clamp(marker.getTime(), getOffset(), getOffset() + visibleLength); double positionInVisible = markerPos - getOffset(); double fractionOfVisible = positionInVisible / visibleLength; @@ -122,16 +163,24 @@ public class GuiMarkerTimeline extends AbstractGuiTimeline im draggingStartX = position.getX(); draggingTimeDelta = marker.getTime() - getTimeAt(position.getX(), position.getY()); } else { // Double click - new GuiEditMarkerPopup(replayHandler, getContainer(), marker).open(); + new GuiEditMarkerPopup(getContainer(), marker, (updatedMarker) -> { + markers.remove(marker); + markers.add(updatedMarker); + saveMarkers.accept(markers); + }).open(); } lastClickTime = now; } else if (button == 1) { // Right click selectedMarker = null; - replayHandler.setTargetPosition(new Location( - marker.getX(), marker.getY(), marker.getZ(), - marker.getPitch(), marker.getYaw() - )); - replayHandler.doJump(marker.getTime(), false); + if (replayHandler != null) { + replayHandler.setTargetPosition(new Location( + marker.getX(), marker.getY(), marker.getZ(), + marker.getPitch(), marker.getYaw() + )); + replayHandler.doJump(marker.getTime(), false); + } else { + setCursorPosition(marker.getTime()); + } } return true; } else { @@ -164,7 +213,7 @@ public class GuiMarkerTimeline extends AbstractGuiTimeline im mouseDrag(position, button, 0); if (dragging) { dragging = false; - replayHandler.saveMarkers(); + saveMarkers.accept(markers); return true; } } @@ -187,4 +236,19 @@ public class GuiMarkerTimeline extends AbstractGuiTimeline im public Marker getSelectedMarker() { return selectedMarker; } + + @Override + public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) { + if (keyCode == Keyboard.KEY_DELETE && selectedMarker != null) { + markers.remove(selectedMarker); + saveMarkers.accept(markers); + return true; + } + return false; + } + + public void addMarker(Marker marker) { + markers.add(marker); + saveMarkers.accept(markers); + } } diff --git a/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java b/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java index d27379a0..ec9f5e1a 100644 --- a/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java +++ b/src/main/java/com/replaymod/replay/gui/overlay/GuiReplayOverlay.java @@ -14,15 +14,24 @@ import de.johni0702.minecraft.gui.element.GuiTooltip; import de.johni0702.minecraft.gui.element.advanced.IGuiTimeline; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint; +import de.johni0702.minecraft.gui.utils.lwjgl.WritablePoint; import net.minecraft.client.resources.I18n; -import net.minecraft.client.settings.GameSettings; -import org.lwjgl.input.Keyboard; -import org.lwjgl.util.ReadableDimension; -import org.lwjgl.util.ReadablePoint; -import org.lwjgl.util.WritablePoint; + +//#if MC>=11300 +import net.minecraft.client.GameSettings; +//#else +//$$ import net.minecraft.client.settings.GameSettings; +//$$ import org.lwjgl.input.Keyboard; +//#endif //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif import net.minecraftforge.fml.common.gameevent.InputEvent; //#else //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; @@ -128,7 +137,7 @@ public class GuiReplayOverlay extends AbstractGuiOverlay { public void run(int time) { replayHandler.doJump(time, true); } - }).setLength(replayHandler.getReplaySender().replayLength()); + }).setLength(replayHandler.getReplayDuration()); } public double getSpeedSliderValue() { @@ -160,12 +169,14 @@ public class GuiReplayOverlay extends AbstractGuiOverlay { setMouseVisible(true); } } + /* FIXME if (Keyboard.getEventKeyState()) { // Handle the F1 key binding while the overlay is opened as a gui screen if (isMouseVisible() && Keyboard.getEventKey() == Keyboard.KEY_F1) { gameSettings.hideGUI = !gameSettings.hideGUI; } } + */ } @Override diff --git a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java index cd83f75a..7243e9d1 100755 --- a/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java +++ b/src/main/java/com/replaymod/replay/gui/screen/GuiReplayViewer.java @@ -4,11 +4,13 @@ import com.google.common.base.Optional; import com.google.common.base.Supplier; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.SettableFuture; import com.mojang.realmsclient.gui.ChatFormatting; import com.replaymod.core.ReplayMod; import com.replaymod.core.SettingsRegistry; import com.replaymod.core.gui.GuiReplaySettings; import com.replaymod.core.utils.Utils; +import com.replaymod.core.versions.MCVer; import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.Setting; import com.replaymod.replaystudio.replay.ReplayFile; @@ -20,97 +22,65 @@ import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.container.GuiScreen; import de.johni0702.minecraft.gui.element.*; -import de.johni0702.minecraft.gui.element.advanced.GuiResourceLoadingList; +import de.johni0702.minecraft.gui.element.advanced.AbstractGuiResourceLoadingList; import de.johni0702.minecraft.gui.function.Typeable; import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.GridLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.VerticalLayout; +import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.popup.GuiYesNoPopup; import de.johni0702.minecraft.gui.utils.Colors; import de.johni0702.minecraft.gui.utils.Consumer; +import de.johni0702.minecraft.gui.utils.lwjgl.Dimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint; +import lombok.Getter; import net.minecraft.client.gui.GuiErrorScreen; import net.minecraft.client.resources.I18n; -import net.minecraft.util.Util; +import net.minecraft.crash.CrashReport; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOCase; import org.apache.commons.io.filefilter.SuffixFileFilter; import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.lwjgl.Sys; -import org.lwjgl.input.Keyboard; -import org.lwjgl.util.Dimension; -import org.lwjgl.util.ReadableDimension; -import org.lwjgl.util.ReadablePoint; -import java.awt.*; +//#if MC>=11300 +import com.replaymod.core.versions.MCVer.Keyboard; +//#else +//$$ import org.lwjgl.input.Keyboard; +//#endif + import java.awt.image.BufferedImage; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; +import java.util.List; import static com.replaymod.replay.ReplayModReplay.LOGGER; +import static com.replaymod.core.versions.MCVer.*; -public class GuiReplayViewer extends GuiScreen implements Typeable { +public class GuiReplayViewer extends GuiScreen { private final ReplayModReplay mod; - public final GuiResourceLoadingList list = new GuiResourceLoadingList(this).onSelectionChanged(new Runnable() { + public final GuiReplayList list = new GuiReplayList(this).onSelectionChanged(new Runnable() { @Override public void run() { - replayButtonPanel.forEach(IGuiButton.class).setEnabled(list.getSelected() != null); + replaySpecificButtons.forEach(b -> b.setEnabled(list.getSelected() != null)); if (list.getSelected() != null && list.getSelected().incompatible) { loadButton.setDisabled(); } } - }).onLoad(new Consumer>> () { - @Override - public void consume(Consumer> obj) { - try { - File folder = mod.getCore().getReplayFolder(); - for (final File file : folder.listFiles((FileFilter) new SuffixFileFilter(".mcpr", IOCase.INSENSITIVE))) { - if (Thread.interrupted()) break; - try (ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), file)) { - - Optional thumb = replayFile.getThumb(); - // Make sure that to int[] conversion doesn't have to occur in main thread - final BufferedImage theThumb; - if (thumb.isPresent()) { - BufferedImage buf = thumb.get(); - // This is the same way minecraft calls this method, we cache the result and hand - // minecraft a BufferedImage with way simpler logic using the precomputed values - final int[] theIntArray = buf.getRGB(0, 0, buf.getWidth(), buf.getHeight(), null, 0, buf.getWidth()); - theThumb = new BufferedImage(buf.getWidth(), buf.getHeight(), BufferedImage.TYPE_INT_ARGB) { - @Override - public int[] getRGB(int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize) { - System.arraycopy(theIntArray, 0, rgbArray, 0, theIntArray.length); - return null; // Minecraft doesn't use the return value - } - }; - } else { - theThumb = null; - } - final ReplayMetaData metaData = replayFile.getMetaData(); - - if (metaData != null) { - obj.consume(() -> new GuiReplayEntry(file, metaData, theThumb)); - } - } catch (Exception e) { - LOGGER.error("Could not load Replay File {}", file.getName(), e); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } }).onSelectionDoubleClicked(() -> { if (this.loadButton.isEnabled()) { this.loadButton.onClick(); // Disable load button to prevent the player from opening the replay twice at the same time this.loadButton.setDisabled(); } - }).setDrawShadow(true).setDrawSlider(true); + }); public final GuiButton loadButton = new GuiButton().onClick(new Runnable() { @Override @@ -121,36 +91,15 @@ public class GuiReplayViewer extends GuiScreen implements Typeable { e.printStackTrace(); } } - }).setSize(73, 20).setI18nLabel("replaymod.gui.load").setDisabled(); + }).setSize(150, 20).setI18nLabel("replaymod.gui.load").setDisabled(); public final GuiButton folderButton = new GuiButton().onClick(new Runnable() { @Override public void run() { try { File folder = mod.getCore().getReplayFolder(); - String path = folder.getAbsolutePath(); - // First try OS specific methods - try { - switch (Util.getOSType()) { - case WINDOWS: - Runtime.getRuntime().exec(String.format("cmd.exe /C start \"Open file\" \"%s\"", path)); - return; - case OSX: - Runtime.getRuntime().exec(new String[]{"/usr/bin/open", path}); - return; - } - } catch (IOException e) { - LogManager.getLogger().error("Cannot open file", e); - } - - // Otherwise try to java way - try { - Desktop.getDesktop().browse(folder.toURI()); - } catch (Throwable throwable) { - // And if all fails, lwjgl - Sys.openURL("file://" + path); - } + MCVer.openFile(folder); } catch (IOException e) { mod.getLogger().error("Cannot open file", e); } @@ -253,17 +202,27 @@ public class GuiReplayViewer extends GuiScreen implements Typeable { } }).setSize(73, 20).setI18nLabel("replaymod.gui.cancel"); - public final GuiPanel replayButtonPanel = new GuiPanel().setLayout(new GridLayout().setSpacingX(5).setSpacingY(5) - .setColumns(2)).addElements(null, loadButton, new GuiPanel() /* Upload */, renameButton, deleteButton); - public final GuiPanel generalButtonPanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(5)) - .addElements(null, folderButton, new GuiPanel().setLayout(new HorizontalLayout().setSpacing(5)) - .addElements(null, settingsButton, cancelButton)); - public final GuiPanel buttonPanel = new GuiPanel(this).setLayout(new HorizontalLayout().setSpacing(6)) - .addElements(null, replayButtonPanel, generalButtonPanel); + public final List replaySpecificButtons = new ArrayList<>(); + { replaySpecificButtons.addAll(Arrays.asList(loadButton, renameButton, deleteButton)); } + public final GuiPanel uploadButton = new GuiPanel(); + public final GuiPanel editorButton = new GuiPanel(); + + public final GuiPanel upperButtonPanel = new GuiPanel().setLayout(new HorizontalLayout().setSpacing(5)) + .addElements(null, loadButton, editorButton, uploadButton); + public final GuiPanel lowerButtonPanel = new GuiPanel().setLayout(new HorizontalLayout().setSpacing(5)) + .addElements(null, renameButton, deleteButton, settingsButton, cancelButton); + public final GuiPanel buttonPanel = new GuiPanel(this).setLayout(new VerticalLayout().setSpacing(5)) + .addElements(null, upperButtonPanel, lowerButtonPanel); public GuiReplayViewer(ReplayModReplay mod) { this.mod = mod; + try { + list.setFolder(mod.getCore().getReplayFolder()); + } catch (IOException e) { + throw newReportedException(CrashReport.makeCrashReport(e, "Getting replay folder")); + } + setTitle(new GuiLabel().setI18nText("replaymod.gui.replayviewer")); setLayout(new CustomLayout() { @@ -277,20 +236,140 @@ public class GuiReplayViewer extends GuiScreen implements Typeable { }); } - private final GuiImage defaultThumbnail = new GuiImage().setTexture(Utils.DEFAULT_THUMBNAIL); + private static final GuiImage DEFAULT_THUMBNAIL = new GuiImage().setTexture(Utils.DEFAULT_THUMBNAIL); - @Override - public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) { - if (keyCode == Keyboard.KEY_F1) { - SettingsRegistry reg = ReplayMod.instance.getSettingsRegistry(); - reg.set(Setting.SHOW_SERVER_IPS, !reg.get(Setting.SHOW_SERVER_IPS)); - reg.save(); - list.load(); + public static class GuiSelectReplayPopup extends AbstractGuiPopup { + public static GuiSelectReplayPopup openGui(GuiContainer container, File folder) { + GuiSelectReplayPopup popup = new GuiSelectReplayPopup(container, folder); + popup.list.load(); + popup.open(); + return popup; + } + + @Getter + private final SettableFuture future = SettableFuture.create(); + + @Getter + private final GuiReplayList list = new GuiReplayList(popup); + + @Getter + private final GuiButton acceptButton = new GuiButton(popup).setI18nLabel("gui.done").setSize(50, 20).setDisabled(); + + @Getter + private final GuiButton cancelButton = new GuiButton(popup).setI18nLabel("gui.cancel").setSize(50, 20); + + + public GuiSelectReplayPopup(GuiContainer container, File folder) { + super(container); + + list.setFolder(folder); + + list.onSelectionChanged(() -> { + acceptButton.setEnabled(list.getSelected() != null); + }).onSelectionDoubleClicked(() -> { + close(); + future.set(list.getSelected().file); + }); + acceptButton.onClick(() -> { + future.set(list.getSelected().file); + close(); + }); + cancelButton.onClick(() -> { + future.set(null); + close(); + }); + + popup.setLayout(new CustomLayout() { + @Override + protected void layout(GuiPanel container, int width, int height) { + pos(cancelButton, width - width(cancelButton), height - height(cancelButton)); + pos(acceptButton, x(cancelButton) - 5 - width(acceptButton), y(cancelButton)); + pos(list, 0, 5); + size(list, width, height - height(cancelButton) - 10); + } + + @Override + public ReadableDimension calcMinSize(GuiContainer container) { + return new Dimension(330, 200); + } + }); + } + + @Override + protected GuiSelectReplayPopup getThis() { + return this; } - return false; } - public class GuiReplayEntry extends AbstractGuiContainer implements Comparable { + public static class GuiReplayList extends AbstractGuiResourceLoadingList implements Typeable { + private File folder = null; + + public GuiReplayList(GuiContainer container) { + super(container); + } + + { + onLoad((Consumer> results) -> { + File[] files = folder.listFiles((FileFilter) new SuffixFileFilter(".mcpr", IOCase.INSENSITIVE)); + if (files == null) { + LOGGER.warn("Failed to list files in {}", folder); + return; + } + for (final File file : files) { + if (Thread.interrupted()) break; + try (ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), file)) { + Optional thumb = replayFile.getThumb(); + // Make sure that to int[] conversion doesn't have to occur in main thread + final BufferedImage theThumb; + if (thumb.isPresent()) { + BufferedImage buf = thumb.get(); + // This is the same way minecraft calls this method, we cache the result and hand + // minecraft a BufferedImage with way simpler logic using the precomputed values + final int[] theIntArray = buf.getRGB(0, 0, buf.getWidth(), buf.getHeight(), null, 0, buf.getWidth()); + theThumb = new BufferedImage(buf.getWidth(), buf.getHeight(), BufferedImage.TYPE_INT_ARGB) { + @Override + public int[] getRGB(int startX, int startY, int w, int h, int[] rgbArray, int offset, int scansize) { + System.arraycopy(theIntArray, 0, rgbArray, 0, theIntArray.length); + return null; // Minecraft doesn't use the return value + } + }; + } else { + theThumb = null; + } + final ReplayMetaData metaData = replayFile.getMetaData(); + + if (metaData != null) { + results.consume(() -> new GuiReplayEntry(file, metaData, theThumb)); + } + } catch (Exception e) { + LOGGER.error("Could not load Replay File {}", file.getName(), e); + } + } + }).setDrawShadow(true).setDrawSlider(true); + } + + public void setFolder(File folder) { + this.folder = folder; + } + + @Override + public boolean typeKey(ReadablePoint mousePosition, int keyCode, char keyChar, boolean ctrlDown, boolean shiftDown) { + if (keyCode == Keyboard.KEY_F1) { + SettingsRegistry reg = ReplayMod.instance.getSettingsRegistry(); + reg.set(Setting.SHOW_SERVER_IPS, !reg.get(Setting.SHOW_SERVER_IPS)); + reg.save(); + load(); + } + return false; + } + + @Override + protected GuiReplayList getThis() { + return this; + } + } + + public static class GuiReplayEntry extends AbstractGuiContainer implements Comparable { public final File file; public final GuiLabel name = new GuiLabel(); public final GuiLabel server = new GuiLabel().setColor(Colors.LIGHT_GRAY); @@ -327,14 +406,14 @@ public class GuiReplayViewer extends GuiScreen implements Typeable { } else { server.setText(metaData.getServerName()); } - incompatible = !new ReplayStudio().isCompatible(metaData.getFileFormatVersion()); + incompatible = !new ReplayStudio().isCompatible(metaData.getFileFormatVersion(), metaData.getProtocolVersion()); if (incompatible) { version.setText("Minecraft " + metaData.getMcVersion()); } dateMillis = metaData.getDate(); date.setText(new SimpleDateFormat().format(new Date(dateMillis))); if (thumbImage == null) { - thumbnail = new GuiImage(defaultThumbnail).setSize(30 * 16 / 9, 30); + thumbnail = new GuiImage(DEFAULT_THUMBNAIL).setSize(30 * 16 / 9, 30); addElements(null, thumbnail); } else { thumbnail = new GuiImage(this).setTexture(thumbImage).setSize(30 * 16 / 9, 30); diff --git a/src/main/java/com/replaymod/replay/handler/GuiHandler.java b/src/main/java/com/replaymod/replay/handler/GuiHandler.java index 4a98b72b..1bc1bc28 100644 --- a/src/main/java/com/replaymod/replay/handler/GuiHandler.java +++ b/src/main/java/com/replaymod/replay/handler/GuiHandler.java @@ -1,16 +1,22 @@ package com.replaymod.replay.handler; +import com.replaymod.core.versions.MCVer; import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.gui.screen.GuiReplayViewer; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiIngameMenu; import net.minecraft.client.gui.GuiMainMenu; +import net.minecraft.client.gui.GuiMultiplayer; import net.minecraft.client.resources.I18n; import net.minecraftforge.client.event.GuiScreenEvent; //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif //#else //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; //#endif @@ -23,6 +29,9 @@ import static com.replaymod.core.versions.MCVer.*; import static com.replaymod.replay.ReplayModReplay.LOGGER; public class GuiHandler { + //#if MC>=11300 + private static final int BUTTON_OPTIONS = 0; + //#endif private static final int BUTTON_EXIT_SERVER = 1; private static final int BUTTON_ADVANCEMENTS = 5; private static final int BUTTON_STATS = 6; @@ -31,7 +40,7 @@ public class GuiHandler { private static final int BUTTON_REPLAY_VIEWER = 17890234; private static final int BUTTON_EXIT_REPLAY = 17890235; - private static final Minecraft mc = Minecraft.getMinecraft(); + private static final Minecraft mc = MCVer.getMinecraft(); private final ReplayModReplay mod; @@ -60,29 +69,44 @@ public class GuiHandler { switch (b.id) { // Replace "Exit Server" button with "Exit Replay" button case BUTTON_EXIT_SERVER: - b.displayString = I18n.format("replaymod.gui.exit"); - b.id = BUTTON_EXIT_REPLAY; + removeButton(event, b); + addButton(event, new GuiButton(BUTTON_EXIT_REPLAY, x(b), y(b), b.width, b.height, I18n.format("replaymod.gui.exit")) { + //#if MC>=11300 + @Override + public void onClick(double mouseX, double mouseY) { + onButton(new GuiScreenEvent.ActionPerformedEvent.Pre(getGui(event), this, new ArrayList<>())); + } + //#endif + }); break; // Remove "Advancements", "Stats" and "Open to LAN" buttons case BUTTON_ADVANCEMENTS: - buttonList.remove(achievements = b); + removeButton(event, achievements = b); break; case BUTTON_STATS: - buttonList.remove(stats = b); + removeButton(event, stats = b); break; case BUTTON_OPEN_TO_LAN: - buttonList.remove(openToLan = b); + removeButton(event, openToLan = b); break; + //#if MC>=11300 + case BUTTON_OPTIONS: + b.width = 200; + break; + //#endif } } if (achievements != null && stats != null) { moveAllButtonsDirectlyBelowUpwards(buttonList, y(achievements), x(achievements), x(stats) + stats.width); } - if (openToLan != null) { - moveAllButtonsDirectlyBelowUpwards(buttonList, y(openToLan), - x(openToLan), x(openToLan) + openToLan.width); - } + // In 1.13+ Forge, the Options button shares one row with the Open to LAN button + //#if MC<11300 + //$$ if (openToLan != null) { + //$$ moveAllButtonsDirectlyBelowUpwards(buttonList, y(openToLan), + //$$ x(openToLan), x(openToLan) + openToLan.width); + //$$ } + //#endif } } @@ -102,8 +126,8 @@ public class GuiHandler { } @SubscribeEvent - public void injectIntoMainMenu(GuiScreenEvent.InitGuiEvent event) { - if (!(getGui(event) instanceof GuiMainMenu)) { + public void ensureReplayStopped(GuiScreenEvent.InitGuiEvent event) { + if (!(getGui(event) instanceof GuiMainMenu || getGui(event) instanceof GuiMultiplayer)) { return; } @@ -120,11 +144,25 @@ public class GuiHandler { } } } + } + + @SubscribeEvent + public void injectIntoMainMenu(GuiScreenEvent.InitGuiEvent event) { + if (!(getGui(event) instanceof GuiMainMenu)) { + return; + } GuiButton button = new GuiButton(BUTTON_REPLAY_VIEWER, getGui(event).width / 2 - 100, - getGui(event).height / 4 + 10 + 3 * 24, I18n.format("replaymod.gui.replayviewer")); + getGui(event).height / 4 + 10 + 4 * 24, I18n.format("replaymod.gui.replayviewer")) { + //#if MC>=11300 + @Override + public void onClick(double mouseX, double mouseY) { + onButton(new GuiScreenEvent.ActionPerformedEvent.Pre(getGui(event), this, new ArrayList<>())); + } + //#endif + }; button.width = button.width / 2 - 2; - getButtonList(event).add(button); + addButton(event, button); } @SubscribeEvent diff --git a/src/main/java/com/replaymod/replay/mixin/MixinGuiSpectator.java b/src/main/java/com/replaymod/replay/mixin/MixinGuiSpectator.java index e2ce7c2d..2a64aef8 100644 --- a/src/main/java/com/replaymod/replay/mixin/MixinGuiSpectator.java +++ b/src/main/java/com/replaymod/replay/mixin/MixinGuiSpectator.java @@ -2,7 +2,6 @@ package com.replaymod.replay.mixin; import com.replaymod.replay.camera.CameraEntity; -import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiSpectator; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -20,7 +19,7 @@ public abstract class MixinGuiSpectator { //#endif public void isInReplay(int i, CallbackInfo ci) { // Prevent spectator gui from opening while in a replay - if (player(Minecraft.getMinecraft()) instanceof CameraEntity) { + if (player(getMinecraft()) instanceof CameraEntity) { ci.cancel(); } } diff --git a/src/main/java/com/replaymod/replay/mixin/MixinParticleManager.java b/src/main/java/com/replaymod/replay/mixin/MixinParticleManager.java index dd3201a2..14210b94 100644 --- a/src/main/java/com/replaymod/replay/mixin/MixinParticleManager.java +++ b/src/main/java/com/replaymod/replay/mixin/MixinParticleManager.java @@ -15,7 +15,11 @@ import java.util.Queue; @Mixin(ParticleManager.class) public abstract class MixinParticleManager { @Shadow - private Queue queueEntityFX; + //#if MC>=11300 + private Queue queue; + //#else + //$$ private Queue queueEntityFX; + //#endif /** * This method additionally clears the queue of particles to be added when the world is changed. @@ -27,7 +31,11 @@ public abstract class MixinParticleManager { */ @Inject(method = "clearEffects", at = @At("HEAD")) public void replayModReplay_clearParticleQueue(World world, CallbackInfo ci) { - queueEntityFX.clear(); + //#if MC>=11300 + queue.clear(); + //#else + //$$ queueEntityFX.clear(); + //#endif } } //#endif diff --git a/src/main/java/com/replaymod/replay/mixin/MixinPlayerControllerMP.java b/src/main/java/com/replaymod/replay/mixin/MixinPlayerControllerMP.java index ded49f19..fce97331 100644 --- a/src/main/java/com/replaymod/replay/mixin/MixinPlayerControllerMP.java +++ b/src/main/java/com/replaymod/replay/mixin/MixinPlayerControllerMP.java @@ -13,7 +13,11 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; //#if MC>=11200 -import net.minecraft.stats.RecipeBook; +//#if MC>=11300 +import net.minecraft.client.util.RecipeBookClient; +//#else +//$$ import net.minecraft.stats.RecipeBook; +//#endif //#endif //#if MC>=10904 import net.minecraft.stats.StatisticsManager; @@ -43,11 +47,17 @@ public abstract class MixinPlayerControllerMP { //$$ private NetHandlerPlayClient netClientHandler; //#endif - //#if MC>=11200 - @Inject(method = "func_192830_a", at=@At("HEAD"), cancellable = true) - private void replayModReplay_createReplayCamera(World worldIn, StatisticsManager statisticsManager, RecipeBook recipeBook, CallbackInfoReturnable ci) { + //#if MC>=11300 + @Inject(method = "createPlayer", at=@At("HEAD"), cancellable = true) + private void replayModReplay_createReplayCamera(World worldIn, StatisticsManager statisticsManager, RecipeBookClient recipeBookClient, CallbackInfoReturnable ci) { if (ReplayModReplay.instance.getReplayHandler() != null) { - ci.setReturnValue(new CameraEntity(mc, worldIn, connection, statisticsManager, recipeBook)); + ci.setReturnValue(new CameraEntity(mc, worldIn, connection, statisticsManager, recipeBookClient)); + //#else + //#if MC>=11200 + //$$ @Inject(method = "func_192830_a", at=@At("HEAD"), cancellable = true) + //$$ private void replayModReplay_createReplayCamera(World worldIn, StatisticsManager statisticsManager, RecipeBook recipeBook, CallbackInfoReturnable ci) { + //$$ if (ReplayModReplay.instance.getReplayHandler() != null) { + //$$ ci.setReturnValue(new CameraEntity(mc, worldIn, connection, statisticsManager, recipeBook)); //#else //#if MC>=10904 //$$ @Inject(method = "createClientPlayer", at=@At("HEAD"), cancellable = true) @@ -67,13 +77,18 @@ public abstract class MixinPlayerControllerMP { //$$ ci.setReturnValue(new CameraEntity(mc, worldIn, mc.getSession(), netClientHandler, statFileWriter)); //#endif //#endif + //#endif //#endif ci.cancel(); } } //#if MC>=10800 - @Inject(method = "isSpectator", at=@At("HEAD"), cancellable = true) + //#if MC>=11300 + @Inject(method = "isSpectatorMode", at=@At("HEAD"), cancellable = true) + //#else + //$$ @Inject(method = "isSpectator", at=@At("HEAD"), cancellable = true) + //#endif private void replayModReplay_isSpectator(CallbackInfoReturnable ci) { if (player(mc) instanceof CameraEntity) { // this check should in theory not be required ci.setReturnValue(player(mc).isSpectator()); diff --git a/src/main/java/com/replaymod/replay/mixin/MixinRenderItem.java b/src/main/java/com/replaymod/replay/mixin/MixinRenderItem.java index aaf11b77..c1e6fecc 100644 --- a/src/main/java/com/replaymod/replay/mixin/MixinRenderItem.java +++ b/src/main/java/com/replaymod/replay/mixin/MixinRenderItem.java @@ -2,25 +2,43 @@ package com.replaymod.replay.mixin; 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.injection.At; import org.spongepowered.asm.mixin.injection.Redirect; +//#if MC>=11300 +import net.minecraft.client.renderer.ItemRenderer; +import net.minecraft.util.Util; +//#else +//$$ import net.minecraft.client.Minecraft; //#if MC>=10904 -import net.minecraft.client.renderer.RenderItem; +//$$ import net.minecraft.client.renderer.RenderItem; //#else //$$ import net.minecraft.client.renderer.entity.RenderItem; //#endif +//#endif -@Mixin(RenderItem.class) +//#if MC>=11300 +@Mixin(ItemRenderer.class) +//#else +//$$ @Mixin(RenderItem.class) +//#endif public class MixinRenderItem { - @Redirect(method = "renderEffect", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getSystemTime()J")) - private long getEnchantmentTime() { + //#if MC>=11300 + @Redirect(method = "renderEffect", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Util;milliTime()J")) + private static long getEnchantmentTime() { + //#else + //$$ @Redirect(method = "renderEffect", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getSystemTime()J")) + //$$ private long getEnchantmentTime() { + //#endif ReplayHandler replayHandler = ReplayModReplay.instance.getReplayHandler(); if (replayHandler != null) { return replayHandler.getReplaySender().currentTimeStamp(); } - return Minecraft.getSystemTime(); + //#if MC>=11300 + return Util.milliTime(); + //#else + //$$ return Minecraft.getSystemTime(); + //#endif } } diff --git a/src/main/java/com/replaymod/replay/mixin/MixinRenderLivingBase.java b/src/main/java/com/replaymod/replay/mixin/MixinRenderLivingBase.java index 75afcf23..24ca21b7 100644 --- a/src/main/java/com/replaymod/replay/mixin/MixinRenderLivingBase.java +++ b/src/main/java/com/replaymod/replay/mixin/MixinRenderLivingBase.java @@ -1,7 +1,6 @@ package com.replaymod.replay.mixin; import com.replaymod.replay.camera.CameraEntity; -import net.minecraft.client.Minecraft; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import org.spongepowered.asm.mixin.Mixin; @@ -26,7 +25,7 @@ import static com.replaymod.core.versions.MCVer.*; public abstract class MixinRenderLivingBase { @Inject(method = "canRenderName", at = @At("HEAD"), cancellable = true) private void replayModReplay_canRenderInvisibleName(EntityLivingBase entity, CallbackInfoReturnable ci) { - EntityPlayer thePlayer = player(Minecraft.getMinecraft()); + EntityPlayer thePlayer = player(getMinecraft()); if (thePlayer instanceof CameraEntity && entity.isInvisible()) { ci.setReturnValue(false); } diff --git a/src/main/java/com/replaymod/replay/mixin/MixinRenderManager.java b/src/main/java/com/replaymod/replay/mixin/MixinRenderManager.java index 71ab6cfb..7c6ef7bd 100644 --- a/src/main/java/com/replaymod/replay/mixin/MixinRenderManager.java +++ b/src/main/java/com/replaymod/replay/mixin/MixinRenderManager.java @@ -3,7 +3,6 @@ package com.replaymod.replay.mixin; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.renderer.entity.RenderManager; -import net.minecraft.client.settings.GameSettings; import net.minecraft.entity.Entity; import net.minecraft.entity.passive.EntityAnimal; import net.minecraft.world.World; @@ -13,6 +12,12 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +//#if MC>=11300 +import net.minecraft.client.GameSettings; +//#else +//$$ import net.minecraft.client.settings.GameSettings; +//#endif + @Mixin(RenderManager.class) public class MixinRenderManager { @Shadow diff --git a/src/main/java/com/replaymod/replay/mixin/MixinTileEntityEndPortalRenderer.java b/src/main/java/com/replaymod/replay/mixin/MixinTileEntityEndPortalRenderer.java index cd1d9493..8ff5e3ad 100644 --- a/src/main/java/com/replaymod/replay/mixin/MixinTileEntityEndPortalRenderer.java +++ b/src/main/java/com/replaymod/replay/mixin/MixinTileEntityEndPortalRenderer.java @@ -2,11 +2,16 @@ package com.replaymod.replay.mixin; 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.injection.At; import org.spongepowered.asm.mixin.injection.Redirect; +//#if MC>=11300 +import net.minecraft.util.Util; +//#else +//$$ import net.minecraft.client.Minecraft; +//#endif + //#if MC>=10800 import net.minecraft.client.renderer.tileentity.TileEntityEndPortalRenderer; //#else @@ -19,8 +24,11 @@ import net.minecraft.client.renderer.tileentity.TileEntityEndPortalRenderer; //$$ @Mixin(RenderEndPortal.class) //#endif public class MixinTileEntityEndPortalRenderer { + //#if MC>=11300 + @Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Util;milliTime()J")) + //#else //#if MC>=10809 - @Redirect(method = "renderTileEntityAt", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getSystemTime()J")) + //$$ @Redirect(method = "renderTileEntityAt", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getSystemTime()J")) //#else //#if MC>=10800 //$$ @Redirect(method = "func_180544_a", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getSystemTime()J")) @@ -28,11 +36,16 @@ public class MixinTileEntityEndPortalRenderer { //$$ @Redirect(method = "renderTileEntityAt(Lnet/minecraft/tileentity/TileEntityEndPortal;DDDF)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getSystemTime()J")) //#endif //#endif + //#endif private long replayModReplay_getEnchantmentTime() { ReplayHandler replayHandler = ReplayModReplay.instance.getReplayHandler(); if (replayHandler != null) { return replayHandler.getReplaySender().currentTimeStamp(); } - return Minecraft.getSystemTime(); + //#if MC>=11300 + return Util.milliTime(); + //#else + //$$ return Minecraft.getSystemTime(); + //#endif } } diff --git a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java index f1cc4e0f..16ff409c 100644 --- a/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java +++ b/src/main/java/com/replaymod/simplepathing/ReplayModSimplePathing.java @@ -1,63 +1,74 @@ package com.replaymod.simplepathing; +import com.replaymod.core.KeyBindingRegistry; +import com.replaymod.core.Module; import com.replaymod.core.ReplayMod; import com.replaymod.core.events.SettingsChangedEvent; import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.events.ReplayCloseEvent; import com.replaymod.replay.events.ReplayOpenEvent; import com.replaymod.replay.gui.overlay.GuiReplayOverlay; +import com.replaymod.replaystudio.pathing.PathingRegistry; +import com.replaymod.replaystudio.pathing.change.Change; import com.replaymod.replaystudio.pathing.path.Keyframe; +import com.replaymod.replaystudio.pathing.path.Timeline; +import com.replaymod.replaystudio.pathing.serialize.TimelineSerialization; +import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.simplepathing.SPTimeline.SPPath; import com.replaymod.simplepathing.gui.GuiPathing; import com.replaymod.simplepathing.preview.PathPreview; import lombok.Getter; +import net.minecraft.crash.CrashReport; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.lwjgl.input.Keyboard; -//#if MC>=10800 -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import org.lwjgl.input.Keyboard; +//#if MC>=10800 +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; //#else -//$$ import cpw.mods.fml.common.Mod; -//$$ import cpw.mods.fml.common.event.FMLPreInitializationEvent; //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; //#endif +//#endif + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import static com.replaymod.core.versions.MCVer.*; -@Mod(modid = ReplayModSimplePathing.MOD_ID, - version = "@MOD_VERSION@", - acceptedMinecraftVersions = "@MC_VERSION@", - acceptableRemoteVersions = "*", - //#if MC>=10800 - clientSideOnly = true, - //#endif - useMetadata = true) -public class ReplayModSimplePathing { - public static final String MOD_ID = "replaymod-simplepathing"; - - @Mod.Instance(MOD_ID) +public class ReplayModSimplePathing implements Module { + { instance = this; } public static ReplayModSimplePathing instance; private ReplayMod core; - public static Logger LOGGER; + public static Logger LOGGER = LogManager.getLogger(); private GuiPathing guiPathing; + private PathPreview pathPreview = new PathPreview(this); - @Mod.EventHandler - public void preInit(FMLPreInitializationEvent event) { - LOGGER = event.getModLog(); - core = ReplayMod.instance; + public ReplayModSimplePathing(ReplayMod core) { + this.core = core; core.getSettingsRegistry().register(Setting.class); + } + @Override + public void initClient() { FML_BUS.register(this); - - PathPreview pathPreview = new PathPreview(this); pathPreview.register(); + } + @Override + public void registerKeyBindings(KeyBindingRegistry registry) { + pathPreview.registerKeyBindings(registry); core.getKeyBindingRegistry().registerKeyBinding("replaymod.input.keyframerepository", Keyboard.KEY_X, () -> { if (guiPathing != null) guiPathing.keyframeRepoButtonPressed(); }); @@ -74,8 +85,43 @@ public class ReplayModSimplePathing { @SubscribeEvent public void postReplayOpen(ReplayOpenEvent.Post event) { - clearCurrentTimeline(); + ReplayFile replayFile = event.getReplayHandler().getReplayFile(); + try { + synchronized (replayFile) { + Timeline timeline = replayFile.getTimelines(new SPTimeline()).get(""); + if (timeline != null) { + setCurrentTimeline(new SPTimeline(timeline)); + } else { + setCurrentTimeline(new SPTimeline()); + } + } + } catch (IOException e) { + throw newReportedException(CrashReport.makeCrashReport(e, "Reading timeline")); + } + guiPathing = new GuiPathing(core, this, event.getReplayHandler()); + + saveService = Executors.newSingleThreadExecutor(); + new Runnable() { + @Override + public void run() { + maybeSaveTimeline(replayFile); + if (guiPathing != null) { + core.runLater(this); + } + } + }.run(); + } + + @SubscribeEvent + public void preReplayClose(ReplayCloseEvent.Pre event) { + saveService.shutdown(); + try { + saveService.awaitTermination(1, TimeUnit.MINUTES); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + saveService = null; } @SubscribeEvent @@ -151,4 +197,52 @@ public class ReplayModSimplePathing { public GuiPathing getGuiPathing() { return guiPathing; } + + private final AtomicInteger lastSaveId = new AtomicInteger(); + private ExecutorService saveService; + private Change lastChange; + private void maybeSaveTimeline(ReplayFile replayFile) { + SPTimeline spTimeline = currentTimeline; + if (spTimeline == null || saveService == null) { + lastChange = null; + return; + } + + Change latestChange = spTimeline.getTimeline().peekUndoStack(); + if (latestChange == null || latestChange == lastChange) { + return; + } + lastChange = latestChange; + + // Clone the timeline for async saving + Timeline timeline; + try { + TimelineSerialization serialization = new TimelineSerialization(spTimeline, null); + String serialized = serialization.serialize(Collections.singletonMap("", spTimeline.getTimeline())); + timeline = serialization.deserialize(serialized).get(""); + } catch (Throwable t) { + CrashReport report = CrashReport.makeCrashReport(t, "Cloning timeline"); + throw newReportedException(report); + } + + int id = lastSaveId.incrementAndGet(); + saveService.submit(() -> { + if (lastSaveId.get() != id) { + return; // Another job has been scheduled, it will do the hard work. + } + try { + saveTimeline(replayFile, spTimeline, timeline); + } catch (IOException e) { + LOGGER.error("Auto-saving timeline:", e); + } + }); + } + + private void saveTimeline(ReplayFile replayFile, PathingRegistry pathingRegistry, Timeline timeline) throws IOException { + synchronized (replayFile) { + Map timelineMap = replayFile.getTimelines(pathingRegistry); + timelineMap.put("", timeline); + replayFile.writeTimelines(pathingRegistry, timelineMap); + } + } } diff --git a/src/main/java/com/replaymod/simplepathing/SPTimeline.java b/src/main/java/com/replaymod/simplepathing/SPTimeline.java index cb47cf4a..e0761d86 100644 --- a/src/main/java/com/replaymod/simplepathing/SPTimeline.java +++ b/src/main/java/com/replaymod/simplepathing/SPTimeline.java @@ -31,7 +31,6 @@ import com.replaymod.simplepathing.properties.ExplicitInterpolationProperty; import lombok.Getter; import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReportCategory; -import net.minecraft.util.ReportedException; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.tuple.Triple; @@ -660,7 +659,7 @@ public class SPTimeline implements PathingRegistry { CrashReport crash = CrashReport.makeCrashReport(e, "Serializing interpolator"); CrashReportCategory category = crash.makeCategory("Serializing interpolator"); addDetail(category, "Interpolator", interpolator::toString); - throw new ReportedException(crash); + throw newReportedException(crash); } return baos.toString(); @@ -674,8 +673,8 @@ public class SPTimeline implements PathingRegistry { } catch (IOException e) { CrashReport crash = CrashReport.makeCrashReport(e, "De-serializing interpolator"); CrashReportCategory category = crash.makeCategory("De-serializing interpolator"); - category.addCrashSection("Interpolator", json); - throw new ReportedException(crash); + addDetail(category, "Interpolator", json::toString); + throw newReportedException(crash); } } } diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java index 1b9e8bc0..9bdd2e4c 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiEditKeyframe.java @@ -33,16 +33,21 @@ import de.johni0702.minecraft.gui.layout.VerticalLayout; import de.johni0702.minecraft.gui.popup.AbstractGuiPopup; import de.johni0702.minecraft.gui.utils.Colors; import de.johni0702.minecraft.gui.utils.Consumer; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint; import lombok.Getter; import net.minecraft.client.resources.I18n; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.lwjgl.input.Keyboard; -import org.lwjgl.util.ReadablePoint; import java.util.Map; import java.util.Optional; +//#if MC>=11300 +import com.replaymod.core.versions.MCVer.Keyboard; +//#else +//$$ import org.lwjgl.input.Keyboard; +//#endif + import static de.johni0702.minecraft.gui.utils.Utils.link; public abstract class GuiEditKeyframe> extends AbstractGuiPopup implements Typeable { diff --git a/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java b/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java index d0285924..413cb501 100644 --- a/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java +++ b/src/main/java/com/replaymod/simplepathing/gui/GuiKeyframeTimeline.java @@ -1,6 +1,7 @@ package com.replaymod.simplepathing.gui; import com.replaymod.core.ReplayMod; +import com.replaymod.core.versions.MCVer; import com.replaymod.pathing.properties.CameraProperties; import com.replaymod.pathing.properties.SpectatorProperty; import com.replaymod.pathing.properties.TimestampProperty; @@ -16,11 +17,10 @@ import com.replaymod.simplepathing.SPTimeline.SPPath; import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.element.advanced.AbstractGuiTimeline; import de.johni0702.minecraft.gui.function.Draggable; -import net.minecraft.client.Minecraft; import org.apache.commons.lang3.tuple.Pair; -import org.lwjgl.util.Point; -import org.lwjgl.util.ReadableDimension; -import org.lwjgl.util.ReadablePoint; +import de.johni0702.minecraft.gui.utils.lwjgl.Point; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadableDimension; +import de.johni0702.minecraft.gui.utils.lwjgl.ReadablePoint; import java.util.Comparator; import java.util.Optional; @@ -46,7 +46,7 @@ public class GuiKeyframeTimeline extends AbstractGuiTimeline=11300 +//#else +//$$ import org.lwjgl.input.Keyboard; //#if MC>=10800 -import net.minecraftforge.fml.common.Loader; +//$$ import net.minecraftforge.fml.common.Loader; //#else //$$ import cpw.mods.fml.common.Loader; //#endif +//#endif import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -211,15 +214,9 @@ public class GuiPathing { }).addElements(null, timelineTime, timeline, scrollbar, zoomButtonPanel); public final GuiPanel panel = new GuiPanel() - .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(5)); - - { - panel.addElements(new HorizontalLayout.Data(0.5), playPauseButton); - if (Loader.isModLoaded("replaymod-render")) { - panel.addElements(new HorizontalLayout.Data(0.5), renderButton); - } - panel.addElements(new HorizontalLayout.Data(0.5), positionKeyframeButton, timeKeyframeButton, timelinePanel); - } + .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(5)) + .addElements(new HorizontalLayout.Data(0.5), + playPauseButton, renderButton, positionKeyframeButton, timeKeyframeButton, timelinePanel); /** * IGuiClickable dummy component that is inserted at a high level. diff --git a/src/main/java/com/replaymod/simplepathing/preview/PathPreview.java b/src/main/java/com/replaymod/simplepathing/preview/PathPreview.java index 9608c34b..578c5f9c 100644 --- a/src/main/java/com/replaymod/simplepathing/preview/PathPreview.java +++ b/src/main/java/com/replaymod/simplepathing/preview/PathPreview.java @@ -1,6 +1,6 @@ package com.replaymod.simplepathing.preview; -import com.replaymod.core.ReplayMod; +import com.replaymod.core.KeyBindingRegistry; import com.replaymod.core.SettingsRegistry; import com.replaymod.core.events.SettingsChangedEvent; import com.replaymod.replay.ReplayHandler; @@ -8,13 +8,17 @@ import com.replaymod.replay.events.ReplayCloseEvent; import com.replaymod.replay.events.ReplayOpenEvent; import com.replaymod.simplepathing.ReplayModSimplePathing; import com.replaymod.simplepathing.Setting; -import org.lwjgl.input.Keyboard; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import org.lwjgl.input.Keyboard; //#if MC>=10800 -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; //#else //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; //#endif +//#endif import static com.replaymod.core.versions.MCVer.*; @@ -30,12 +34,13 @@ public class PathPreview { public void register() { FML_BUS.register(this); + } - ReplayMod core = mod.getCore(); - mod.getCore().getKeyBindingRegistry().registerKeyBinding("replaymod.input.pathpreview", Keyboard.KEY_H, () -> { - SettingsRegistry registry = core.getSettingsRegistry(); - registry.set(Setting.PATH_PREVIEW, !registry.get(Setting.PATH_PREVIEW)); - registry.save(); + public void registerKeyBindings(KeyBindingRegistry registry) { + registry.registerKeyBinding("replaymod.input.pathpreview", Keyboard.KEY_H, () -> { + SettingsRegistry settings = mod.getCore().getSettingsRegistry(); + settings.set(Setting.PATH_PREVIEW, !settings.get(Setting.PATH_PREVIEW)); + settings.save(); }); } diff --git a/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java b/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java index 062d46d6..619f01a5 100644 --- a/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java +++ b/src/main/java/com/replaymod/simplepathing/preview/PathPreviewRenderer.java @@ -1,6 +1,7 @@ package com.replaymod.simplepathing.preview; import com.replaymod.core.ReplayMod; +import com.replaymod.core.versions.MCVer; import com.replaymod.pathing.properties.CameraProperties; import com.replaymod.pathing.properties.SpectatorProperty; import com.replaymod.pathing.properties.TimestampProperty; @@ -15,7 +16,6 @@ import com.replaymod.simplepathing.ReplayModSimplePathing; import com.replaymod.simplepathing.SPTimeline; import com.replaymod.simplepathing.gui.GuiPathing; import net.minecraft.client.Minecraft; -import net.minecraft.client.renderer.Tessellator; import net.minecraft.entity.Entity; import net.minecraft.util.ResourceLocation; import net.minecraftforge.client.event.RenderWorldLastEvent; @@ -26,7 +26,11 @@ import org.lwjgl.opengl.GL11; //#if MC>=10800 import net.minecraft.client.renderer.GlStateManager; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#if MC>=11300 +import net.minecraftforge.eventbus.api.SubscribeEvent; +//#else +//$$ import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +//#endif //#else //$$ import com.replaymod.core.versions.MCVer.GlStateManager; //$$ import cpw.mods.fml.common.eventhandler.SubscribeEvent; @@ -40,7 +44,7 @@ import static com.replaymod.core.versions.MCVer.*; public class PathPreviewRenderer { private static final ResourceLocation CAMERA_HEAD = new ResourceLocation("replaymod", "camera_head.png"); - private static final Minecraft mc = Minecraft.getMinecraft(); + private static final Minecraft mc = MCVer.getMinecraft(); private static final int SLOW_PATH_COLOR = 0xffcccc; private static final int FAST_PATH_COLOR = 0x660000; @@ -239,7 +243,7 @@ public class PathPreviewRenderer { Keyframe keyframe) { BufferBuilder_setTranslation(0, 0, 0); - mc.renderEngine.bindTexture(TEXTURE); + MCVer.bindTexture(TEXTURE); float posX = 80f / ReplayMod.TEXTURE_SIZE; float posY = 0f; @@ -286,7 +290,7 @@ public class PathPreviewRenderer { Triple rot) { BufferBuilder_setTranslation(0, 0, 0); - mc.renderEngine.bindTexture(CAMERA_HEAD); + MCVer.bindTexture(CAMERA_HEAD); GL11.glPushMatrix(); diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml new file mode 100644 index 00000000..d439e932 --- /dev/null +++ b/src/main/resources/META-INF/mods.toml @@ -0,0 +1,28 @@ +modLoader="javafml" +loaderVersion="[13,)" +updateJSONURL="https://raw.githubusercontent.com/ReplayMod/ReplayMod/master/versions.json" +issueTrackerURL="https://replaymod.com/forum" +displayURL="https://replaymod.com" +logoFile="replaymod_logo.png" +authors="CrushedPixel, johni0702" + +[[mods]] +modId="replaymod" +version="${version}" +displayName="Replay Mod" +description=''' +A Mod which allows you to record, replay and share your Minecraft experience. +''' + +[[dependencies.replaymod]] + modId="forge" + mandatory=true + versionRange="[15.0,)" + ordering="NONE" + side="BOTH" +[[dependencies.replaymod]] + modId="minecraft" + mandatory=true + versionRange="[ 1.13.2 ]" + ordering="NONE" + side="BOTH" \ No newline at end of file diff --git a/src/main/resources/META-INF/replaymod_at.cfg b/src/main/resources/META-INF/replaymod_at.cfg index b5d3cbf5..866e5a48 100644 --- a/src/main/resources/META-INF/replaymod_at.cfg +++ b/src/main/resources/META-INF/replaymod_at.cfg @@ -1,6 +1,9 @@ # To apply changes in this file run: ./gradlew clean setupDecompWorkspace idea # Minecraft +##if MC>=11300 +public net.minecraft.client.Minecraft func_184117_aA()V # processKeyBinds +##endif ##if MC>=11002 public-f net.minecraft.client.Minecraft field_71428_T # timer ##else @@ -24,7 +27,21 @@ public net.minecraft.client.Minecraft func_147121_ag()V # rightClickMouse public net.minecraft.client.Minecraft func_147112_ai()V # middleClickMouse public net.minecraft.client.Minecraft func_147115_a(Z)V # sendClickBlockToController +##if MC>=11300 +# MainWindow +public net.minecraft.client.MainWindow field_198131_r # framebufferWidth +public net.minecraft.client.MainWindow field_198132_s # framebufferHeight +##endif + +##if MC>=11300 +# FirstPersonRenderer +public net.minecraft.client.renderer.FirstPersonRenderer * +##endif + # Packets +##if MC>=11300 +public net.minecraft.network.play.server.SPacketJoinGame field_149200_f # maxPlayers +##endif ##if MC>=10904 public net.minecraft.network.play.server.SPacketSpawnMob field_149043_l # dataManager public net.minecraft.network.play.server.SPacketSpawnPlayer field_148960_i # watcher @@ -74,15 +91,24 @@ public net.minecraft.client.renderer.EntityRenderer * public net.minecraft.client.renderer.EntityRenderer *() # RenderGlobal -public net.minecraft.client.renderer.RenderGlobal field_174995_M # renderDispatcher -public net.minecraft.client.renderer.RenderGlobal field_147595_R # displayListEntitiesDirty -public net.minecraft.client.renderer.RenderGlobal field_175009_l # chunksToUpdate -public net.minecraft.client.renderer.RenderGlobal field_72755_R # renderInfos -public net.minecraft.client.renderer.RenderGlobal field_175009_l # chunksToUpdate -public net.minecraft.client.renderer.RenderGlobal field_72740_G # renderEntitiesStartupCounter +##if MC>=11300 +public net.minecraft.client.renderer.WorldRenderer field_174995_M # renderDispatcher +public net.minecraft.client.renderer.WorldRenderer field_147595_R # displayListEntitiesDirty +public net.minecraft.client.renderer.WorldRenderer field_175009_l # chunksToUpdate +public net.minecraft.client.renderer.WorldRenderer field_72755_R # renderInfos +public net.minecraft.client.renderer.WorldRenderer field_175009_l # chunksToUpdate +public net.minecraft.client.renderer.WorldRenderer field_72740_G # renderEntitiesStartupCounter +##else +#$$ public net.minecraft.client.renderer.RenderGlobal field_174995_M # renderDispatcher +#$$ public net.minecraft.client.renderer.RenderGlobal field_147595_R # displayListEntitiesDirty +#$$ public net.minecraft.client.renderer.RenderGlobal field_175009_l # chunksToUpdate +#$$ public net.minecraft.client.renderer.RenderGlobal field_72755_R # renderInfos +#$$ public net.minecraft.client.renderer.RenderGlobal field_175009_l # chunksToUpdate +#$$ public net.minecraft.client.renderer.RenderGlobal field_72740_G # renderEntitiesStartupCounter ##if MC<10904 #$$ public net.minecraft.client.renderer.RenderGlobal func_174983_a(Lnet.minecraft.util.BlockPos;Lnet.minecraft.client.renderer.chunk.RenderChunk;)Z # isPositionInRenderChunk ##endif +##endif public net.minecraft.client.renderer.RenderGlobal$ContainerLocalRenderInformation #public net.minecraft.client.renderer.RenderGlobal$ContainerLocalRenderInformation field_178553_a # renderChunk @@ -125,7 +151,11 @@ public net.minecraft.server.integrated.IntegratedServer field_71348_o # isGamePa # GameSettings ##if MC>=10904 ##if MC>=11002 -public-f net.minecraft.client.settings.GameSettings field_186714_aM # soundLevels +##if MC>=11300 +public-f net.minecraft.client.GameSettings field_186714_aM # soundLevels +##else +#$$ public-f net.minecraft.client.settings.GameSettings field_186714_aM # soundLevels +##endif ##else #$$ public net.minecraft.client.settings.GameSettings field_186714_aM # soundLevels ##endif @@ -148,7 +178,7 @@ public net.minecraft.crash.CrashReportCategory$Entry public net.minecraft.client.settings.KeyBinding field_151474_i # pressTime # Timer -public net.minecraft.util.Timer * +public-f net.minecraft.util.Timer * # GlStateManager public net.minecraft.client.renderer.GlStateManager * @@ -161,6 +191,13 @@ public net.minecraft.client.renderer.GlStateManager$BooleanState * # EntityLivingBase public net.minecraft.entity.EntityLivingBase field_184621_as # HAND_STATES public net.minecraft.entity.EntityLivingBase field_184628_bn # activeItemStackUseCount +##if MC>=11300 +public net.minecraft.entity.EntityLivingBase field_184623_bh # interpTargetX +public net.minecraft.entity.EntityLivingBase field_184624_bi # interpTargetY +public net.minecraft.entity.EntityLivingBase field_184625_bj # interpTargetZ +public net.minecraft.entity.EntityLivingBase field_184626_bk # interpTargetYaw +public net.minecraft.entity.EntityLivingBase field_70709_bj # interpTargetPitch +##endif ##endif # Particles diff --git a/src/main/resources/assets/replaymod/lang b/src/main/resources/assets/replaymod/lang index 692a6765..37de62cf 160000 --- a/src/main/resources/assets/replaymod/lang +++ b/src/main/resources/assets/replaymod/lang @@ -1 +1 @@ -Subproject commit 692a6765560bd96f02c9364798395914470c2c9e +Subproject commit 37de62cf8edae480b2a78cb6f5ed307aec351a69 diff --git a/src/main/resources/mixins.recording.replaymod.json b/src/main/resources/mixins.recording.replaymod.json index a7429f91..b1300588 100644 --- a/src/main/resources/mixins.recording.replaymod.json +++ b/src/main/resources/mixins.recording.replaymod.json @@ -4,8 +4,15 @@ "mixins": [], "server": [], "client": [ + //#if MC>=11300 + "MixinDownloadingPackFinder", + "MixinKeyboardListener", + "MixinMouseHelper", + //#endif //#if MC>=10904 - "MixinPlayerControllerMP", + //#if MC<11300 + //$$ "MixinPlayerControllerMP", + //#endif "MixinWorldClient", //#endif //#if MC<=10710 @@ -14,7 +21,9 @@ //#endif "MixinNetHandlerLoginClient", "MixinNetHandlerPlayClient", - "MixinNetworkDispatcher", + //#if MC<11300 + //$$ "MixinNetworkDispatcher", + //#endif "MixinRenderGlobal" ], "compatibilityLevel": "JAVA_8", diff --git a/src/main/resources/mixins.render.replaymod.json b/src/main/resources/mixins.render.replaymod.json index 305fa27c..ab421e58 100644 --- a/src/main/resources/mixins.render.replaymod.json +++ b/src/main/resources/mixins.render.replaymod.json @@ -11,6 +11,9 @@ //$$ "MixinRendererLivingEntity", //#endif "MixinEntityRenderer", + //#if MC>=11300 + "MixinFogRenderer", + //#endif "MixinFrustum", "MixinRender", //#if MC>=10800 diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta new file mode 100644 index 00000000..700d98b0 --- /dev/null +++ b/src/main/resources/pack.mcmeta @@ -0,0 +1,14 @@ +{ + "pack": { + "description": "ReplayMod resources", + //#if MC>=11300 + "pack_format": 4 + //#else + //#if MC>=11002 + //$$ "pack_format": 2 + //#else + //$$ "pack_format": 1 + //#endif + //#endif + } +} diff --git a/version.txt b/version.txt index ac2cdeba..7d2ed7c7 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.1.3 +2.1.4 diff --git a/versions/1.13.2/.gitkeep b/versions/1.13.2/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/versions/common.gradle b/versions/common.gradle index ae86cae7..f9de860b 100644 --- a/versions/common.gradle +++ b/versions/common.gradle @@ -5,7 +5,7 @@ buildscript { mcVersion = "${major}${minor.padLeft(2, '0')}${(patch ?: '').padLeft(2, '0')}" as int } else { def f = file('mcVersion') - mcVersion = f.exists() ? f.readLines().first() as int : 11202 + mcVersion = f.exists() ? f.readLines().first() as int : 11302 } project.ext.mcVersion = mcVersion @@ -30,6 +30,7 @@ buildscript { exclude group: 'org.ow2.asm', module: 'asm-debug-all' } classpath 'net.minecraftforge.gradle:ForgeGradle:' + ( + mcVersion >= 11300 ? '3.+' : mcVersion >= 11200 ? '2.3-SNAPSHOT' : mcVersion >= 10904 ? '2.2-SNAPSHOT' : mcVersion >= 10809 ? '2.1-SNAPSHOT' : @@ -40,9 +41,21 @@ buildscript { } } +def mcVersionStr = "${(int)(mcVersion/10000)}.${(int)(mcVersion/100)%100}" + (mcVersion%100==0 ? '' : ".${mcVersion%100}") + +def FG3 = mcVersion >= 11300 + if (mcVersion >= 10800) { - apply plugin: 'net.minecraftforge.gradle.forge' - apply plugin: 'org.spongepowered.mixin' + if (FG3) { + apply plugin: 'net.minecraftforge.gradle' + // FIXME use mixin plugin once updated + compileJava { + options.compilerArgs += ['-AreobfSrgFile=' + new File(project.buildDir, 'mcp-srg.srg').getCanonicalPath()] + } + } else { + apply plugin: 'net.minecraftforge.gradle.forge' + apply plugin: 'org.spongepowered.mixin' + } } else { apply plugin: 'forge' @@ -56,30 +69,47 @@ sourceCompatibility = 1.8 targetCompatibility = 1.8 afterEvaluate { - version = project.minecraft.version + '-' + rootProject.version + version = mcVersionStr + '-' + rootProject.version } group= "com.replaymod" archivesBaseName = "replaymod" minecraft { - if (mcVersion >= 10800) { - coreMod = 'com.replaymod.core.LoadingPlugin' + if (FG3) { + runs { + client { + workingDirectory rootProject.file('eclipse') + property 'forge.logging.console.level', 'info' + mods { + replaymod { + source sourceSets.main + } + } + } + } + } else { + if (mcVersion >= 10800) { + coreMod = 'com.replaymod.core.LoadingPlugin' + } + runDir = "../../eclipse" } - runDir = "../../eclipse" - version = [ - 11202: '1.12.2-14.23.0.2486', - 11201: '1.12.1-14.22.0.2444', - 11200: '1.12-14.21.1.2387', - 11102: '1.11.2-13.20.0.2216', - 11100: '1.11-13.19.1.2188', - 11002: '1.10.2-12.18.2.2099', - 10904: '1.9.4-12.17.0.1976', - 10809: '1.8.9-11.15.1.1722', - 10800: '1.8-11.14.4.1563', - 10710: '1.7.10-10.13.4.1558-1.7.10', - ][mcVersion] + if (!FG3) { + version = [ + 11202: '1.12.2-14.23.0.2486', + 11201: '1.12.1-14.22.0.2444', + 11200: '1.12-14.21.1.2387', + 11102: '1.11.2-13.20.0.2216', + 11100: '1.11-13.19.1.2188', + 11002: '1.10.2-12.18.2.2099', + 10904: '1.9.4-12.17.0.1976', + 10809: '1.8.9-11.15.1.1722', + 10800: '1.8-11.14.4.1563', + 10710: '1.7.10-10.13.4.1558-1.7.10', + ][mcVersion] + } mappings = [ + 11302: "snapshot_20180921-1.13", 11202: "snapshot_20170615", 11201: "snapshot_20170615", 11200: "snapshot_20170615", @@ -92,12 +122,17 @@ minecraft { 10710: "stable_12", ][mcVersion] } + afterEvaluate { - // Note cannot use minecraft.replace because that has already been forwarded to the task by FG by now - tasks.sourceMainJava.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) - tasks.sourceMainJava.replace '@MC_VERSION@', "[ ${project.minecraft.version} ]" + if (mcVersion >= 11300) { + // No longer required in 1.13+ because all version info is in the toml file + } else { + // Note cannot use minecraft.replace because that has already been forwarded to the task by FG by now + tasks.sourceMainJava.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) + tasks.sourceMainJava.replace '@MC_VERSION@', "[ $mcVersionStr ]" + } } repositories { @@ -111,32 +146,44 @@ repositories { configurations { shade compile.extendsFrom shade +} - shade.exclude group: 'com.google.code.gson', module: 'gson' // provided by MC +def shadeExclusions = { + // Cannot just add these to the shade configuration because they'd be inherited by the compile configuration then + 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 + exclude group: 'org.projectlombok', module: 'lombok' // runtime only for @SneakyThrows which isn't used } dependencies { + if (FG3) { + minecraft 'net.minecraftforge:forge:' + [ + 11302: '1.13.2-25.0.76', + ][mcVersion] + } + // compile 'com.github.SpongePowered:Mixin:404f5da' // 0.7.5-SNAPSHOT // ^ mixin doesn't compile on jitpack, so we'll have to depend on the SNAPSHOT and build it manually for reprod - compile 'org.spongepowered:mixin:0.7.5-SNAPSHOT' + if (!gradle.gradleVersion.startsWith('3.')) { + annotationProcessor 'org.spongepowered:mixin:0.7.8-SNAPSHOT' + compileOnly 'org.spongepowered:mixin:0.7.8-SNAPSHOT' + } + compile 'org.spongepowered:mixin:0.7.8-SNAPSHOT' shade 'com.googlecode.mp4parser:isoparser:1.1.7' shade 'org.apache.commons:commons-exec:1.3' - def withoutGuava = { exclude group: 'com.google.guava', module: 'guava-jdk5' } - shade 'com.google.apis:google-api-services-youtube:v3-rev178-1.22.0', withoutGuava - shade 'com.google.api-client:google-api-client-gson:1.20.0', withoutGuava - shade 'com.google.api-client:google-api-client-java6:1.20.0', withoutGuava + shade 'com.google.apis:google-api-services-youtube:v3-rev178-1.22.0', shadeExclusions + shade 'com.google.api-client:google-api-client-gson:1.20.0', shadeExclusions + shade 'com.google.api-client:google-api-client-java6:1.20.0', shadeExclusions shade 'com.google.oauth-client:google-oauth-client-jetty:1.20.0' shade 'org.aspectj:aspectjrt:1.8.2' shade 'com.github.ReplayMod.JavaBlend:2.79.0:e0a1b9a' - def studioVersion = "${(int)(mcVersion/10000)}.${(int)(mcVersion/100)%100}" + (mcVersion%100==0 ? '' : ".${mcVersion%100}") + def studioVersion = mcVersionStr if (studioVersion == '1.8.9') studioVersion = '1.8' - shade("com.github.ReplayMod.ReplayStudio:$studioVersion:3680c2e"){ - exclude group: 'com.google.guava', module: 'guava' // provided by MC - exclude group: 'org.projectlombok', module: 'lombok' // runtime only for @SneakyThrows which isn't used - } + shade "com.github.ReplayMod.ReplayStudio:$studioVersion:c41d016", shadeExclusions def jGuiVersion = project.name if (['1.10.2', '1.11', '1.11.2'].contains(jGuiVersion)) jGuiVersion = '1.9.4' @@ -146,6 +193,9 @@ dependencies { exclude group: 'org.projectlombok', module: 'lombok' // runtime only for @SneakyThrows which isn't used } + if (!gradle.gradleVersion.startsWith('3.')) { + annotationProcessor 'org.projectlombok:lombok:1.16.6' + } compile 'org.projectlombok:lombok:1.16.6' // runtime only for @SneakyThrows which isn't used testCompile 'junit:junit:4.11' @@ -175,8 +225,10 @@ if (project.name != 'core') { def preprocessedSrc = 'build/preprocessed/src' def preprocessedRes = 'build/preprocessed/res' + def preprocessedAT = 'build/preprocessed/at.cfg' def originalSrc = '../../src/main/java' def originalRes = '../../src/main/resources' + def originalAT = originalRes + '/META-INF/replaymod_at.cfg' def vars = [MC: mcVersion as int] sourceSets { @@ -202,19 +254,35 @@ if (project.name != 'core') { compileJava.dependsOn preprocessJava processResources.dependsOn preprocessResources - (mcVersion >= 10800 ? [deobfMcMCP, deobfMcSRG] : [deobfuscateJar, deobfBinJar]).each { task -> - task.dependsOn preprocessResources + if (FG3) { + // FG3 reads the AT file right after project evaluation, so we need to convert it right now + project.convertTree(vars, originalAT, preprocessedAT) + minecraft.accessTransformers << file(preprocessedAT) + } else { + (mcVersion >= 10800 ? [deobfMcMCP, deobfMcSRG] : [deobfuscateJar, deobfBinJar]).each { task -> + task.dependsOn preprocessResources + } + minecraft.accessTransformer preprocessedRes + '/META-INF/replaymod_at.cfg' } - - minecraft.accessTransformer preprocessedRes + '/META-INF/replaymod_at.cfg' } else { + if (FG3) { + minecraft.accessTransformers << file('../../src/main/resources/META-INF/replaymod_at.cfg') + } sourceSets { main.java.srcDirs = ['../../src/main/java'] main.resources.srcDirs = ['../../src/main/resources'] } } +sourceSets { + main { + java { + exclude 'com/replaymod/compat' + exclude 'com/replaymod/restrictions' + } + } +} - +if (!FG3) { // FIXME def reobfTask = mcVersion >= 10800 ? tasks.reobfJar : tasks.reobf task configureRelocation() { dependsOn tasks.jar @@ -239,6 +307,7 @@ task configureRelocation() { } } reobfTask.dependsOn configureRelocation +} if (mcVersion <= 10710) { // Dummy task so "./gradlew reobfJar" also builds 1.7.10 @@ -296,21 +365,23 @@ processResources { // this will ensure that this task is redone when the versions change. inputs.property 'version', { project.version } - inputs.property 'mcversion', { project.minecraft.version } + inputs.property 'mcversion', { mcVersionStr } - // replace stuff in mcmod.info, nothing else + // replace stuff in mcmod.info (pre 1.13) and mods.toml (1.13+), nothing else from(sourceSets.main.resources.srcDirs) { spec -> include 'mcmod.info' + include 'mods.toml' // replace version and mcversion afterEvaluate { - spec.expand 'version': project.version, 'mcversion': project.minecraft.version + spec.expand 'version': project.version, 'mcversion': mcVersionStr } } - // copy everything else, thats not the mcmod.info + // copy everything else, thats not the mcmod.info (or mods.toml) from(sourceSets.main.resources.srcDirs) { exclude 'mcmod.info' + exclude 'mods.toml' } } @@ -327,14 +398,42 @@ sourceSets { } } -task copySrg(type: Copy, dependsOn: 'genSrgs') { - from {project.tasks.genSrgs.mcpToSrg} - into 'build' +if (FG3) { + task copySrg(dependsOn: 'createMcpToSrg') { + doLast { + def tsrg = file(project.tasks.createMcpToSrg.output).readLines() + def srg = [] + def cls = '' + for (def line : tsrg) { + if (line[0] != '\t') { + srg.add('CL: ' + line) + cls = line.split(' ')[0] + } else { + def parts = line.substring(1).split(' ') + if (line.contains('(')) { + srg.add('MD: ' + cls + '/' + parts[0] + ' ' + parts[1] + ' ' + cls + '/' + parts[2] + ' ' + parts[1]) + } else { + srg.add('FD: ' + cls + '/' + parts[0] + ' ' + cls + '/' + parts[1]) + } + } + } + new File(project.buildDir, 'mcp-srg.srg').write(srg.join('\n')) + } + } +} else { + task copySrg(type: Copy, dependsOn: 'genSrgs') { + from {project.tasks.genSrgs.mcpToSrg} + into 'build' + } } -setupDecompWorkspace.dependsOn copySrg -setupDevWorkspace.dependsOn copySrg -project.tasks.idea.dependsOn copySrg +if (FG3) { + // FIXME project.tasks.idea.dependsOn copySrg + compileJava.dependsOn copySrg // FIXME can probably remove this one once mixingradle is updated +} else { + setupDecompWorkspace.dependsOn copySrg + setupDevWorkspace.dependsOn copySrg +} if (mcVersion <= 10710) { reobf.doFirst { @@ -353,6 +452,7 @@ if (mcVersion <= 10710) { } } +/* FIXME // Mixin uses multiple HashMaps to generate the refmap. // HashMaps are unordered collections and as such do not produce deterministic output. // To fix that, we simply sort the refmap json file. @@ -377,6 +477,7 @@ compileJava.doLast { refmapFile.withWriter { it.write json } } } +*/ import java.util.zip.ZipEntry import java.util.zip.ZipFile @@ -427,10 +528,11 @@ def annotationWorkaround = { jar.delete() tmp.renameTo(jar) } -if (mcVersion >= 10809) { +if (mcVersion >= 10809 && !FG3) { tasks.deobfMcMCP.doLast annotationWorkaround } +if (!FG3) { // FIXME task runIntegrationTest(type: JavaExec, dependsOn: ["makeStart", "jar"]) { main = 'GradleStart' standardOutput = System.out @@ -454,5 +556,6 @@ task runIntegrationTest(type: JavaExec, dependsOn: ["makeStart", "jar"]) { runIntegrationTest.classpath runClient.classpath + sourceSets.integrationTest.output } } +} defaultTasks 'build' diff --git a/versions/preprocessor.gradle b/versions/preprocessor.gradle index 5d47dee0..b316eb59 100644 --- a/versions/preprocessor.gradle +++ b/versions/preprocessor.gradle @@ -9,6 +9,17 @@ static def evalVar(vars, String var) { import java.util.regex.Pattern static def evalExpr(vars, String expr) { + expr = expr.trim() + + def parts = expr.split(/\|\|/) + if (parts.length > 1) { + return parts.any { evalExpr(vars, it) } + } + parts = expr.split(/&&/) + if (parts.length > 1) { + return !parts.any { !evalExpr(vars, it) } + } + def matcher = Pattern.compile(/(.+)(<=|>=|<|>)(.+)/).matcher(expr) if (matcher.matches()) { def lhs = evalVar(vars, matcher.group(1)) @@ -135,7 +146,13 @@ static def convertFile(Map kws, Map vars, File project.ext.convertTree = { Map vars, String inName, String outName=inName -> def defaultKws = [if: '//#if ', ifdef: '//#ifdef ', else: '//#else', endif: '//#endif', eval: '//$$'] def cfgKws = [if: '##if ', ifdef: '##ifdef ', else: '##else', endif: '##endif', eval: '#$$'] - def extensions = ['.java': defaultKws, '.gradle': defaultKws, '.json': defaultKws, '.cfg': cfgKws] + def extensions = [ + '.java': defaultKws, + '.gradle': defaultKws, + '.json': defaultKws, + '.mcmeta': defaultKws, + '.cfg': cfgKws + ] def inPath = project.file(inName).toPath() def outPath = project.file(outName).toPath() def inPlace = inPath.toAbsolutePath().equals(outPath.toAbsolutePath())