Compare commits

..

61 Commits

Author SHA1 Message Date
Jonas Herzig
91573d9487 Update jGui, ReplayStudio and Translations
jGui:
9e84e72 Fix inversion of empty selection

ReplayStudio:
ff12b1d Remove invalid filter from services files
c6a9325 Proper value wrapping in CatmullRomSplineInterpolator (fixes #8 GH)
2017-05-31 09:16:19 +02:00
Jonas Herzig
d71358bcba Add confirmation dialog before overwriting path presets (fixes #65) 2017-05-21 19:34:46 +02:00
Jonas Herzig
1a55983986 Move addCallback method from integration test Utils to main Utils 2017-05-21 19:34:46 +02:00
Jonas Herzig
18c5bcd469 Be more cooperative with other mods on the ingame menu (fixes #42) 2017-05-21 18:59:29 +02:00
Jonas Herzig
c054fe83f6 Register keys for simplepathing only once (fixes #63) 2017-05-21 17:11:15 +02:00
Jonas Herzig
f13297c5a5 Fix default interpolator handling in keyframe gui and after loading (fixes #64) 2017-05-21 16:29:59 +02:00
Jonas Herzig
391f304c5f Show error popup if entity tracker fails to load 2017-05-21 12:42:13 +02:00
Jonas Herzig
0e0eaaa9a7 Fix entity tracker being set even if it failed loading (fixes #50) 2017-05-21 12:27:20 +02:00
Jonas Herzig
a34bbbcd68 Fix NPE when spectating a player without a camera entity 2017-05-21 11:57:30 +02:00
Jonas Herzig
60879fbbed Change local class to be an inner class because Srg2Source breaks with it 2017-04-21 17:24:43 +02:00
Jonas Herzig
dfafbecf35 Load translations from github repo, only reload language not all resource packs 2017-04-21 16:36:47 +02:00
Jonas Herzig
8a6ec5191b Add missing tooltips to player overview and various missing chat messages 2017-04-21 16:36:47 +02:00
Jonas Herzig
5677fc54cb Re-add warning to replay editor 2017-04-21 16:36:47 +02:00
Jonas Herzig
748a91e7b2 Fix missing tooltips on fav/like/dislike buttons in replay center 2017-04-21 16:36:47 +02:00
Jonas Herzig
ee24866f29 Fix upload replay button not being updated when name/tags change 2017-04-21 16:36:47 +02:00
Jonas Herzig
dba085c700 Move translations into separate repo 2017-04-21 16:36:47 +02:00
Jonas Herzig
3f0e3e725e Fix NPE when receiving Replay|Restrict messages (fixes #16 GH) 2017-04-21 13:38:08 +02:00
johni0702
40e1d850da Fix crash when saving the last keyframe in the edit gui (fixes #61) 2017-04-14 17:31:16 +02:00
johni0702
03aada1f5f Update Mixin to 0.6.8 (fixes #9 GH) 2017-04-14 17:31:16 +02:00
johni0702
0c1dc658ad Fix interpolator being lost when moving keyframe to the end (fixes #62) 2017-04-14 17:31:16 +02:00
johni0702
fcbbbc9ea1 Add integration test. Run with ./gradlew runIntegrationTest 2017-04-14 17:31:15 +02:00
johni0702
fe6ded0143 Fix movement of keyframe via GuiEditKeyframe not updating selected keyframe
Selecting a keyframe, double clicking it to open the gui, changing the
timestamp, saving and then clicking the Add/Remove Keyframe button used to
crash the client. This is fixed by updating the selection whenever the time is
changed.
2017-04-13 18:18:17 +02:00
johni0702
f58fa8f9c2 Fix Login GUI not respecting other GUIs opened on startup
Previously the Login GUI would simply replace the currently active GUI.
Now the currently active GUI is stored and then re-opened after login.
2017-04-13 18:12:07 +02:00
johni0702
4fc3a3166c Fix OpenEye being installed into working dir instead of mcDataDir 2017-04-13 18:06:21 +02:00
johni0702
1ffeb010d5 Update jGui 2017-01-29 11:14:54 +01:00
johni0702
c122d30065 Merge branch '1.8-path-segments' into 1.8 2017-01-29 11:12:06 +01:00
johni0702
27fd25ae84 Fix crash when server responds with garbage to language query (fixes #51) 2017-01-28 13:18:36 +01:00
johni0702
db6b0ff5a6 Fix / by 0 when two spectator keyframes are closer than 50ms (fixes #60) 2017-01-28 13:12:28 +01:00
johni0702
d61d0ac670 Fix hand of invisible spectated player being visible (fixes #59) 2017-01-28 12:54:42 +01:00
johni0702
f7460f9bf5 Re-enable time path after path playback (fixes #55) 2017-01-28 11:46:11 +01:00
johni0702
bdbffd5c33 Add Catmull-Rom spline interpolator to simplepathing 2017-01-28 11:03:59 +01:00
johni0702
5cbfd2884e Generate interpolator settings from InterpolatorType enum 2017-01-28 10:51:24 +01:00
johni0702
6095aef3d1 Fix loading and storing of default interpolator with settings 2017-01-28 10:33:45 +01:00
johni0702
64898ce288 Fix NPE when saving recorded resource pack 2017-01-24 13:08:55 +01:00
johni0702
70e3e54038 Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)
Lambdas are not properly re-obfuscated prior to FG2.2 causing said errors
when used. This is the only instance of such a lambda.
FG2.2 only supports MC 1.8.8 and up as such the lambda was replaced with
an anonymous inner class to solve the problem.
2017-01-24 13:07:50 +01:00
johni0702
3f2b3d62f7 Fix replacing source file in Replay Editor (fixes #57) 2017-01-24 09:43:14 +01:00
johni0702
d2def94fc1 Fix duration of replays corrupted because of JVM/OS crash
A JVM or OS crash can lead to incomplete packets at the end of a recording.
The duration of a corrupted replay file is not restored properly if that file
contains an incomplete packet. Furthermore, the entity tracker does not load
properly with such an incomplete packet. Both bugs are fixed with this commit.

To fix these bugs for replays that have already been recovered, simply open
them in the Replay Viewer, modify them somehow (save a new path) and force
the game to quit forcefully. The replays will be recovered the next time the
game starts and both bugs will be fixed.
2017-01-23 18:00:38 +01:00
johni0702
2c64030459 Refactor simple pathing timeline logic into dedicated class with unit tests
Fix logic for user-set interpolators
2017-01-21 13:30:47 +01:00
johni0702
4deb374a7f Enable validation on focus change for all number input fields 2017-01-20 16:16:01 +01:00
johni0702
5302bcaed4 Fix id and text of Replay Editor button in docs 2017-01-20 16:02:42 +01:00
johni0702
057edccf26 Manually inject root cert for cross-signed LetsEncrypt cert in requests
JREs prior to 8u101 do not ship with the root certificate that cross-signed
the LetsEncrypt X3 CA used to sign server certificates.
As the standard native Minecraft launcher for Windows and OSX ship with an
older Java version, this commit imports the required root cert and uses it
wherever communication with the replaymod.com server is established.

Effectively reverts 682fb4b
2017-01-19 17:07:14 +01:00
johni0702
1c606874ff Update ReplayStudio and jGui
ReplayStudio:
2344abe Fix OOM when saving large EntityPositionTrackers (fixes #46)

jGui:
94e4b4c Fix render offset in text area and field being out of bounds after resize
2017-01-14 22:33:44 +01:00
CrushedPixel
d9b796db5e Mixin'd the Shader's mods ShadersRender class to only render hands if the Forge event isn't cancelled
This fixes player hands being rendered while spectating an entity in a replay while having Shaders enabled
2017-01-14 16:30:15 +01:00
CrushedPixel
6bbff3bba7 Updated Mixin to 0.6.4-SNAPSHOT and set mixin target level 2017-01-14 16:30:15 +01:00
johni0702
6f3ac71a8b Add CustomMainMenu compatibility info to docs 2017-01-13 18:30:00 +01:00
CrushedPixel
cd16211309 Register new Property to imply that the path segment following a keyframe has a user-set interpolator instead of the default
Modified Keyframe GUI to allow for custom Path interpolator
2017-01-13 12:05:07 +01:00
johni0702
b0a1cdfce2 Search for ffmpeg executable in common locations to simplify setup 2017-01-02 14:22:47 +01:00
CrushedPixel
d46589193e Fixed GuiEditPositionKeyframe's width to fit on all screen sizes
Centered all rows of GuiEditPositionKeyframe's VerticalLayout
2017-01-02 14:13:39 +01:00
johni0702
1b099f9ef6 Add render queue 2016-12-31 16:29:17 +01:00
johni0702
03534c6515 Update docs 2016-12-30 14:29:07 +01:00
johni0702
4388d6977d Only try to create the downloads folder if it does not yet exist 2016-12-30 12:11:53 +01:00
johni0702
7ad19aa025 Add mod version and accepted MC version directly to the @Mod annotation 2016-12-30 12:10:43 +01:00
johni0702
655bea0b2d Replace @Mod.Instance with direct static references 2016-12-30 12:10:13 +01:00
Marius Metzger
458dad6f9a Merge pull request #4 from ReplayMod/1.8-editor
Add Replay Editor (for now, trimming only)
2016-12-29 15:13:14 +01:00
johni0702
eb5ccb2fd5 Add Replay Editor (for now, trimming only) 2016-12-26 20:35:50 +01:00
johni0702
6498d625ad Fix server game data snapshot being restored in singleplayer (fixes #44) 2016-12-24 11:22:01 +01:00
johni0702
2c898a8192 Prevent rendering without sufficient keyframes (fixes #45) 2016-12-24 01:44:51 +01:00
johni0702
5d520a5a3c Fix deserialization of chroma key color (fixes #43) 2016-12-24 01:24:55 +01:00
johni0702
c681742a69 Use caching maven proxy for faster and more stable drone builds 2016-12-23 20:40:23 +01:00
CrushedPixel
fbf165e618 Added Anti-Aliasing to the Render Settings 2016-12-17 03:22:37 +01:00
johni0702
6163585fe5 Update ReplayStudio to fix dependencies 2016-12-16 20:48:43 +01:00
145 changed files with 5647 additions and 1938 deletions

View File

@@ -3,9 +3,11 @@ pipeline:
image: plugins/git image: plugins/git
tags: true tags: true
build: build:
image: openjdk:8 image: maven:3.3.9-jdk-8
commands: commands:
- JAVA_OPTS=-Xmx1516M ./gradlew setupDecompWorkspace - curl --fail --create-dirs -o $HOME/.gradle/init.d/mirror.gradle https://maven.johni0702.de/config/gradle
- curl --fail --create-dirs -o $HOME/.m2/settings.xml https://maven.johni0702.de/config/maven
- ./gradlew setupCIWorkspace
- ./gradlew :build - ./gradlew :build
archive: archive:
image: plugins/s3 image: plugins/s3

View File

@@ -1 +1 @@
eyJhbGciOiJIUzI1NiJ9.cGlwZWxpbmU6CiAgY2xvbmU6CiAgICBpbWFnZTogcGx1Z2lucy9naXQKICAgIHRhZ3M6IHRydWUKICBidWlsZDoKICAgIGltYWdlOiBvcGVuamRrOjgKICAgIGNvbW1hbmRzOgogICAgICAtIEpBVkFfT1BUUz0tWG14MTUxNk0gLi9ncmFkbGV3IHNldHVwRGVjb21wV29ya3NwYWNlCiAgICAgIC0gLi9ncmFkbGV3IDpidWlsZAogIGFyY2hpdmU6CiAgICBpbWFnZTogcGx1Z2lucy9zMwogICAgYWNsOiBwdWJsaWMtcmVhZAogICAgcmVnaW9uOiAidXMtZWFzdC0xIgogICAgYnVja2V0OiAicmVwbGF5bW9kIgogICAgcGF0aF9zdHlsZTogdHJ1ZQogICAgc291cmNlOiBidWlsZC9saWJzLyoKICAgIHN0cmlwX3ByZWZpeDogYnVpbGQvbGlicy8KICAgIHRhcmdldDogLwo.Q_jQxd73CuNh8ZTGTw31yMmBIzWprIagmaddFYovDrc eyJhbGciOiJIUzI1NiJ9.cGlwZWxpbmU6CiAgY2xvbmU6CiAgICBpbWFnZTogcGx1Z2lucy9naXQKICAgIHRhZ3M6IHRydWUKICBidWlsZDoKICAgIGltYWdlOiBtYXZlbjozLjMuOS1qZGstOAogICAgY29tbWFuZHM6CiAgICAgIC0gY3VybCAtLWZhaWwgLS1jcmVhdGUtZGlycyAtbyAkSE9NRS8uZ3JhZGxlL2luaXQuZC9taXJyb3IuZ3JhZGxlIGh0dHBzOi8vbWF2ZW4uam9obmkwNzAyLmRlL2NvbmZpZy9ncmFkbGUKICAgICAgLSBjdXJsIC0tZmFpbCAtLWNyZWF0ZS1kaXJzIC1vICRIT01FLy5tMi9zZXR0aW5ncy54bWwgaHR0cHM6Ly9tYXZlbi5qb2huaTA3MDIuZGUvY29uZmlnL21hdmVuCiAgICAgIC0gLi9ncmFkbGV3IHNldHVwQ0lXb3Jrc3BhY2UKICAgICAgLSAuL2dyYWRsZXcgOmJ1aWxkCiAgYXJjaGl2ZToKICAgIGltYWdlOiBwbHVnaW5zL3MzCiAgICBhY2w6IHB1YmxpYy1yZWFkCiAgICByZWdpb246ICJ1cy1lYXN0LTEiCiAgICBidWNrZXQ6ICJyZXBsYXltb2QiCiAgICBwYXRoX3N0eWxlOiB0cnVlCiAgICBzb3VyY2U6IGJ1aWxkL2xpYnMvKgogICAgc3RyaXBfcHJlZml4OiBidWlsZC9saWJzLwogICAgdGFyZ2V0OiAvCg.riE6lhwfdNUhI7LO2SxEKFOjzjaX40ne7Ae9gyP75KE

4
.gitmodules vendored
View File

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

View File

@@ -1,7 +1,5 @@
import groovy.json.JsonOutput import groovy.json.JsonOutput
import java.util.zip.ZipInputStream
buildscript { buildscript {
repositories { repositories {
mavenCentral() mavenCentral()
@@ -19,8 +17,8 @@ buildscript {
} }
} }
dependencies { dependencies {
classpath 'net.minecraftforge.gradle:ForgeGradle:2.2-SNAPSHOT' classpath 'net.minecraftforge.gradle:ForgeGradle:2.0-SNAPSHOT'
classpath 'org.spongepowered:mixingradle:0.4-SNAPSHOT' classpath 'org.spongepowered:mixingradle:0.3-SNAPSHOT'
} }
} }
@@ -36,9 +34,11 @@ archivesBaseName = "replaymod"
minecraft { minecraft {
coreMod = 'com.replaymod.core.LoadingPlugin' coreMod = 'com.replaymod.core.LoadingPlugin'
version = '1.9.4-12.17.0.1976' version = '1.8-11.14.4.1563'
runDir = "eclipse" runDir = "eclipse"
mappings = "snapshot_20160518" mappings = "snapshot_nodoc_20141130"
replace '@MOD_VERSION@', project.version
replace '@MC_VERSION@', project.minecraft.version
} }
repositories { repositories {
@@ -59,7 +59,7 @@ configurations {
dependencies { dependencies {
compile 'org.projectlombok:lombok:1.16.4' compile 'org.projectlombok:lombok:1.16.4'
compile 'org.spongepowered:mixin:0.5.9-SNAPSHOT' compile 'org.spongepowered:mixin:0.6.8-SNAPSHOT'
shade 'com.googlecode.mp4parser:isoparser:1.1.7' shade 'com.googlecode.mp4parser:isoparser:1.1.7'
shade 'org.apache.commons:commons-exec:1.3' shade 'org.apache.commons:commons-exec:1.3'
shade 'com.google.apis:google-api-services-youtube:v3-rev178-1.22.0' shade 'com.google.apis:google-api-services-youtube:v3-rev178-1.22.0'
@@ -101,27 +101,6 @@ jar {
} }
from noticeDir from noticeDir
def langDir = file("$buildDir/languages")
doFirst {
try {
langDir.deleteDir()
langDir.mkdirs()
def dir = new File(langDir, 'assets/replaymod/lang/')
dir.mkdirs()
def zip = new ZipInputStream(new URL('http://replaymod.com/api/grab_languages').openStream())
def e;
while ((e = zip.nextEntry) != null) {
new File(dir, e.getName()) << zip
zip.closeEntry()
}
} catch(Exception e) {
e.printStackTrace();
}
}
from (langDir) {
exclude '**/en_US.lang'
}
from ({shade().collect { it.isDirectory() ? it : zipTree(it) }}) { from ({shade().collect { it.isDirectory() ? it : zipTree(it) }}) {
exclude '**/NOTICE*' exclude '**/NOTICE*'
// exclude everything taken in from jGui for running the mod in a dev environment // exclude everything taken in from jGui for running the mod in a dev environment
@@ -136,7 +115,6 @@ jar {
manifest { manifest {
attributes 'TweakClass': 'org.spongepowered.asm.launch.MixinTweaker', attributes 'TweakClass': 'org.spongepowered.asm.launch.MixinTweaker',
'TweakOrder': '0', 'TweakOrder': '0',
'FMLCorePluginContainsFMLMod': 'true',
'FMLAT': 'replaymod_at.cfg' 'FMLAT': 'replaymod_at.cfg'
} }
} }
@@ -187,6 +165,13 @@ sourceSets {
} }
refMap = "mixins.replaymod.refmap.json" refMap = "mixins.replaymod.refmap.json"
} }
integrationTest {
compileClasspath += main.runtimeClasspath + main.output
java {
srcDir file('src/integration-test/java')
}
resources.srcDir file('src/integration-test/resources')
}
} }
task copySrg(type: Copy, dependsOn: 'genSrgs') { task copySrg(type: Copy, dependsOn: 'genSrgs') {
@@ -198,6 +183,30 @@ setupDecompWorkspace.dependsOn copySrg
setupDevWorkspace.dependsOn copySrg setupDevWorkspace.dependsOn copySrg
project.tasks.idea.dependsOn copySrg project.tasks.idea.dependsOn copySrg
task runIntegrationTest(type: JavaExec, dependsOn: ["makeStart", "jar"]) {
main = 'GradleStart'
standardOutput = System.out
errorOutput = System.err
workingDir file(minecraft.runDir)
def testDir = new File(minecraft.runDir, "integration-test")
doFirst {
testDir.deleteDir()
testDir.mkdirs()
}
doLast {
testDir.deleteDir()
}
afterEvaluate {
def runClient = tasks.getByName("runClient")
runIntegrationTest.jvmArgs = runClient.jvmArgs + "-Dfml.noGrab=true"
runIntegrationTest.args = runClient.args + "--gameDir" + testDir.canonicalPath
runIntegrationTest.classpath runClient.classpath + sourceSets.integrationTest.output
}
}
def generateVersionsJson() { def generateVersionsJson() {
// List all tags // List all tags
def stdout = new ByteArrayOutputStream() def stdout = new ByteArrayOutputStream()

View File

@@ -2,7 +2,14 @@
## Installing the Replay Mod [replaymod] ## Installing the Replay Mod [replaymod]
The **ReplayMod** requires **Minecraft Forge** to be installed. The **ReplayMod** requires **Minecraft Forge** to be installed.
For the use with the **Replay Mod**, we recommend using Forge Version `1.8-11.14.3.1450`, which can be downloaded [here](http://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.8.html) Depending on your **Replay Mod** version we recommend the following **Forge** versions:
- Replay Mod 2.0.0 or later for Minecraft 1.11: Forge [1.11-13.19.1.2189](https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.11.html)
- Replay Mod 2.0.0 or later for Minecraft 1.10.2: Forge [1.10.2-12.18.2.2099](https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.10.2.html)
- Replay Mod 2.0.0 or later for Minecraft 1.9.4: Forge [1.9.4-12.17.0.1976](https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.9.4.html)
- Replay Mod 2.0.0 or later for Minecraft 1.8: Forge [1.8-11.14.4.1563](https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.8.html)
- Replay Mod 1.0.8 or older for Minecraft 1.8: Forge [1.8-11.14.3.1450](http://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.8.html)
Other Forge Versions might, but don't necessarily work. Other Forge Versions might, but don't necessarily work.
If you don't know how to install Forge, follow [this tutorial](https://www.youtube.com/watch?v=4i7-RystzC4). If you don't know how to install Forge, follow [this tutorial](https://www.youtube.com/watch?v=4i7-RystzC4).
@@ -25,6 +32,10 @@ Next, download this **Batch Script** to quickly install FFmpeg: <https://replaym
Put the downloaded script into the folder where you extracted FFmpeg and run itt by double-clicking it. Put the downloaded script into the folder where you extracted FFmpeg and run itt by double-clicking it.
If you see a success message in the console, you've sucessfully installed **FFmpeg**! If you see a success message in the console, you've sucessfully installed **FFmpeg**!
Alternatively, starting with ReplayMod 2.0.0-b5, you can also extract the downloaded `.7z` file into a `ffmpeg` folder
(you have to create it, it doesn't exist by default) in your `.minecraft` folder. No need to run any **Batch Script**.
The FFmpeg executable should end up at `.minecraft/ffmpeg/bin/ffmpeg.exe`.
### Mac OSX [mac] ### Mac OSX [mac]
On OSX, you can install **FFmpeg** with **[Homebrew](http://brew.sh/)** using `brew install ffmpeg`. On OSX, you can install **FFmpeg** with **[Homebrew](http://brew.sh/)** using `brew install ffmpeg`.
@@ -37,26 +48,35 @@ If in doubt, consult the documentation of your distribution.
## Compatibility with other Mods [compatibility] ## Compatibility with other Mods [compatibility]
### General information [general] ### General information [general]
In General, the Replay Mod _should_ be compatible with most Forge Mods. In General, the Replay Mod _should_ be compatible with most Forge Mods.
However, due to the way that recording works, many Mods that add new Blocks, Mobs or Game Mechanics are not supported.
Mods that simply modify the client (like the [PixelCam Mod](http://www.minecraftforum.net/forums/mapping-and-modding/minecraft-mods/2327429-pixelcam-camera-studio-for-minecraft-1-8)
or the [CommandGroups Mod](http://www.minecraftforum.net/forums/mapping-and-modding/minecraft-mods/2443430-command-groups-mod-order-label-and-color-command)) usually work.
### Optifine [optifine]
The **Replay Mod** is compatible with `Optifine 1.8.0 HD U D5`, which can be downloaded at <https://optifine.net/downloads>.
Other Optifine versions for Minecraft 1.8.0 might, but don't have to be compatible as well.
> **Warning:** If you have Optifine installed, you **can not render videos** from within the Replay Mod.
This might change in future Replay Mod versions.
### Shaders Mod [shaders] ### Shaders Mod [shaders]
You can use the Replay Mod together with [karyonix' Shaders Mod](http://www.minecraftforum.net/forums/mapping-and-modding/minecraft-mods/1286604-shaders-mod-updated-by-karyonix), [Karyonix' Shaders Mod](http://www.minecraftforum.net/forums/mapping-and-modding/minecraft-mods/1286604-shaders-mod-updated-by-karyonix)
but rendering videos results in **possible crashes** and **malformed videos**. is no longer compatible with Minecraft Forge starting with 1.9.4. As such it is not compatible with the Replay Mod either.
Therefore, you have to **uninstall the Shaders Mod before rendering**. Please use [Optifine](https://optifine.net/) instead.
### Custom Main Menu [custom-main-menu]
The [Custom Main Menu](https://mods.curse.com/mc-mods/minecraft/226406-custom-main-menu) mod is often used in mod packs to customize their Main Menu with a button layout fitting the background image, links to their website / bug tracker and similar.
If you are familiar with it, the button ids for the Replay Mod are: **17890234** (text: `replaymod.gui.replayviewer`), **17890237** (text: `replaymod.gui.replayeditor`) and **17890236** (text: `replaymod.gui.replaycenter`)
Due to the nature of this Custom Main Menu mod, buttons added to the Main Menu by 3rd party mods like the **Replay Mod** will not show up by default.
Thus, to access the Replay Viewer/Editor/Center, you need to manually configure the position for those buttons.
To do so, you need to modify the Custom Main Menu configuration file.
Usually, you can find it at `.minecraft/config/CustomMainMenu/mainmenu.json` (if you're using a mod pack launcher, it normally has its own .minecraft folder).
For an explanation of this config file, have a look at Custom Main Menu's page.
You can find a list of already modified config files [here](https://gist.github.com/Johni0702/3f3fab81dbf7ada83d045d9fe8f345aa).
## Troubleshooting [troubleshooting] ## Troubleshooting [troubleshooting]
If you need help installing the **Replay Mod** or **FFmpeg**, please read [this forum thread](https://www.replaymod.com/forum/thread/220) If you need help installing the **Replay Mod** or **FFmpeg**, please read [this forum thread](https://www.replaymod.com/forum/thread/220)
by [bela333](https://www.replaymod.com/user/bela333) - it covers most of the problems that users encountered so far. by [bela333](https://www.replaymod.com/user/bela333) - it covers most of the problems that users encountered so far.
## Settings [settings]
To access the **Replay Mod Settings** from the Main Menu click the **"Mods"** button, select the **Replay Mod** from the list and
the click the **"Config"** button.
When in a Replay, you can either bind a hotkey to the **Replay Mod Settings** in Minecraft's Control settings
or use the hotkey GUI by clicking on the arrow button in the lower left corner.
# Recording [recording] # Recording [recording]
![](img/recording-indicator.jpg) ![](img/recording-indicator.jpg)
The **Recording Indicator** which is displayed while recording The **Recording Indicator** which is displayed while recording
@@ -88,6 +108,8 @@ You can move through block similar to Spectator Mode.
Using the **Mouse Wheel**, you can modify your flying speed. Scrolling **up** causes you to fly faster, while scrolling **down** slows you down. Using the **Mouse Wheel**, you can modify your flying speed. Scrolling **up** causes you to fly faster, while scrolling **down** slows you down.
If you prefer the camera to be controlled similar to vanilla Creative Mode, you can change that in the **Replay Settings**.
### Camera Roll [roll] ### Camera Roll [roll]
![](img/camera-tilted.jpg) ![](img/camera-tilted.jpg)
A Screenshot taken with a **tilted camera** A Screenshot taken with a **tilted camera**
@@ -251,13 +273,15 @@ The **Keyframe Repository** with two **Path Presets**
Using the `X` key, you can open the **Keyframe Repository**. Every Replay File has a separate Keyframe Repository which can contain several **Keyframe Presets**. Using the `X` key, you can open the **Keyframe Repository**. Every Replay File has a separate Keyframe Repository which can contain several **Keyframe Presets**.
To save the Keyframes currently on your Timeline, click the **"Save current Path"** button. To save the Keyframes currently on your Timeline, click the **"Save as..."** button.
You can change the Preset's name using the Text Input Field in the upper right corner. You can change a Preset's name, select it from the list and click the **"Rename"** button.
To load a Keyframe Preset, select a Preset from the list on the left side and click the **"Load"** button. To load a Keyframe Preset, select a Preset from the list and click the **"Load"** button.
Using the **"Remove"** button, you can permanently delete a Keyframe Preset from the **Keyframe Repository**. Using the **"Remove"** button, you can permanently delete a Keyframe Preset from the **Keyframe Repository**.
## Custom Objects [objects] ## Custom Objects [objects]
> Custom Objects are not available in the 2.0.0 versions. They are expected to return eventually.
[YouTube](_Xdpg828fbE) [YouTube](_Xdpg828fbE)
### Introduction [introduction] ### Introduction [introduction]
@@ -416,12 +440,10 @@ There are **7 Encoding Presets** you can choose from:
### Advanced Settings [advanced] ### Advanced Settings [advanced]
![](img/rendersettings-advanced.jpg) ![](img/rendersettings-advanced.jpg)
The **Advanced Render Settings** Screen The **Advanced Render Settings**
By clicking the **"Advanced Settings" Button** in the lower right corner, you can access more settings. #### Render Nametags [nametags]
If you don't want to see Entity's Nametags in the rendered video, simply un-tick the checkbox. Hidden Nametags also include Nametags of **Armor Stands** and **Mobs**, **not only Players**!
#### Hide Nametags [nametags]
If you don't want to see Entity's Nametags in the rendered video, simply tick the checkbox. Hidden Nametags also include Nametags of **Armor Stands** and **Mobs**, **not only Players**!
#### Stabilize Camera [stabilize] #### Stabilize Camera [stabilize]
This setting is only applicable for **Cubic Rendering** and **Equirectangular Rendering**. This setting is only applicable for **Cubic Rendering** and **Equirectangular Rendering**.
@@ -443,14 +465,14 @@ Using a **Video Editing Software** like **Adobe After Effects** or **Sony Vegas*
### Command Line Settings [commandline] ### Command Line Settings [commandline]
![](img/rendersettings-commandline.jpg) ![](img/rendersettings-commandline.jpg)
The **Command Line Render Settings** Screen The **Command Line Render Settings**
> **Note**: This is for advanced users only. If you do not know what your're doing, leave these settings as they are. > **Note**: This is for advanced users only. If you do not know what your're doing, leave these settings as they are.
The **Replay Mod** runs [FFmpeg](http://ffmpeg.org/) via the **Command Line** to encode videos. The **Replay Mod** runs [FFmpeg](http://ffmpeg.org/) via the **Command Line** to encode videos.
You can customize both the **executed Command** and the **Command Line Arguments** in the **Command Line Settings** Tag of the **Render Settings Screen**. You can customize both the **executed Command** and the **Command Line Arguments** in the **Command Line Settings** part of the **Render Settings Screen**.
#### Custom Command [command] #### Custom Command [command]
If you leave the left input field blank, `ffmpeg` will be used as **command**. If you haven't set your **PATH variable** to link to your FFmpeg distribution, simply enter the full path to your FFmpeg executable (e.g. `C:/ffmpeg/ffmpeg.exe` or `/usr/local/bin/ffmpeg`). If you leave the left input field blank, `ffmpeg` will be used as **command**. If you haven't set your **PATH variable** to link to your FFmpeg distribution, simply enter the full path to your FFmpeg executable (e.g. `C:/ffmpeg/ffmpeg.exe` or `/usr/local/bin/ffmpeg`).
@@ -532,7 +554,7 @@ By clicking on a player's **Name or Head**, you can **spectate** this player.
Next to each player, there's a **checkbox** using which you can toggle the player's **visibility**. Next to each player, there's a **checkbox** using which you can toggle the player's **visibility**.
This way, you can hide certain players from the Replay. This way, you can hide certain players from the Replay.
Above the individual checkboxes, there are two checkbox buttons - one to **hide all players** and one to **show all players**. Below the individual checkboxes, there are two checkbox buttons - one to **hide all players** and one to **show all players**.
If you want to **save the player visibility settings**, check **"Remember Hidden Players" checkbox**. If you want to **save the player visibility settings**, check **"Remember Hidden Players" checkbox**.
If checked, invisible players will stay invisible after closing the Replay (this can of course be reverted), If checked, invisible players will stay invisible after closing the Replay (this can of course be reverted),

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

2
jGui

Submodule jGui updated: 5c0ca7d7d8...9e84e724aa

View File

@@ -0,0 +1,30 @@
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Entry point for running integration tests from within the IDE.
* This is <b>not</b> called when running integration tests directly from gradle.
*/
public class IntegrationTest {
public static void main(String[] args) throws Throwable {
// Prevent MC from grabbing our mouse during the test (it's not needed)
System.setProperty("fml.noGrab", "true");
// Make sure the test folder exists and is fresh
File gameDir = new File(System.getProperty("user.dir"), "integration-test");
if (gameDir.exists()) {
FileUtils.forceDelete(gameDir);
}
FileUtils.forceMkdir(gameDir);
// Set game dir to test folder and call regular entry point
List<String> argsList = new ArrayList<>(Arrays.asList(args));
argsList.add("--gameDir");
argsList.add(gameDir.getCanonicalPath());
GradleStart.main(argsList.toArray(new String[argsList.size()]));
}
}

View File

@@ -0,0 +1,342 @@
package com.replaymod.core;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import de.johni0702.minecraft.gui.container.AbstractGuiOverlay;
import de.johni0702.minecraft.gui.container.AbstractGuiScreen;
import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiOverlay;
import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiElement;
import de.johni0702.minecraft.gui.element.GuiTexturedButton;
import de.johni0702.minecraft.gui.popup.AbstractGuiPopup;
import de.johni0702.minecraft.gui.utils.Consumer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.settings.KeyBinding;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import org.lwjgl.input.Keyboard;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.TimeoutException;
import static com.replaymod.core.ReplayModIntegrationTest.LOGGER;
import static com.replaymod.core.utils.Utils.addCallback;
public abstract class AbstractTask implements Task {
public static Task create(Consumer<AbstractTask> init) {
return new AbstractTask() {
@Override
protected void init() {
init.consume(this);
}
};
}
public static final Minecraft mc = Minecraft.getMinecraft();
public final ReplayMod core = ReplayMod.instance;
public SettableFuture<Void> future;
@Override
public ListenableFuture<Void> execute() {
future = SettableFuture.create();
FMLCommonHandler.instance().bus().register(this);
MinecraftForge.EVENT_BUS.register(this);
addCallback(future, success -> {
FMLCommonHandler.instance().bus().unregister(this);
MinecraftForge.EVENT_BUS.unregister(this);
}, error -> {});
init();
return future;
}
protected void init() {}
protected void runLater(Runnable runnable) {
core.runLater(() -> {
try {
runnable.run();
} catch (Throwable t) {
future.setException(t);
}
});
}
public void expectGuiClosed(Runnable onClosed) {
expectGuiClosed0(10, onClosed);
}
public void expectGuiClosed(int timeout, Runnable onClosed) {
expectGuiClosed0(timeout, onClosed);
}
private void expectGuiClosed0(int timeout, Runnable onClosed) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
class EventHandler {
final net.minecraft.client.gui.GuiScreen currentScreen = mc.currentScreen;
int framesPassed;
@SubscribeEvent
public void onGuiOpen(TickEvent.RenderTickEvent event) {
if (event.phase != TickEvent.Phase.START) return;
if (currentScreen != mc.currentScreen) {
FMLCommonHandler.instance().bus().unregister(this);
onClosed.run();
} else {
if (framesPassed < timeout) {
framesPassed++;
} else {
Object gui = (gui = GuiScreen.from(currentScreen)) == null ? currentScreen : gui;
Exception e = new TimeoutException("Timeout while waiting for " + gui + " to be closed.");
e.setStackTrace(Arrays.copyOfRange(stackTrace, 3, stackTrace.length));
future.setException(e);
}
}
}
}
FMLCommonHandler.instance().bus().register(new EventHandler());
}
public void expectPopupClosed(Runnable onClosed) {
expectPopupClosed0(10, onClosed);
}
public void expectPopupClosed(int timeout, Runnable onClosed) {
expectPopupClosed0(timeout, onClosed);
}
private void expectPopupClosed0(int timeout, Runnable onClosed) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
AbstractGuiPopup popup = getPopup(mc.currentScreen);
if (popup == null) {
throw new IllegalStateException("No popup found.");
}
class EventHandler {
int framesPassed;
@SubscribeEvent
public void onGuiOpen(TickEvent.RenderTickEvent event) {
if (event.phase != TickEvent.Phase.START) return;
if (getPopup(mc.currentScreen) != popup) {
FMLCommonHandler.instance().bus().unregister(this);
onClosed.run();
} else {
if (framesPassed < timeout) {
framesPassed++;
} else {
Exception e = new TimeoutException("Timeout while waiting for " + popup + " to be closed.");
e.setStackTrace(Arrays.copyOfRange(stackTrace, 3, stackTrace.length));
future.setException(e);
}
}
}
}
FMLCommonHandler.instance().bus().register(new EventHandler());
}
private AbstractGuiPopup getPopup(net.minecraft.client.gui.GuiScreen minecraft) {
GuiContainer<?> container = GuiOverlay.from(minecraft);
if (container == null) {
container = GuiScreen.from(minecraft);
}
if (container != null) {
while (container.getContainer() != null) {
container = container.getContainer();
}
GuiElement popup = Iterables.getLast(container.getChildren());
if (popup instanceof AbstractGuiPopup) {
return (AbstractGuiPopup) popup;
}
}
return null;
}
public <T> void expectGui(Class<T> guiClass, Consumer<T> onOpen) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
class EventHandler {
net.minecraft.client.gui.GuiScreen currentScreen;
int framesPassed;
@SubscribeEvent
public void onGuiOpen(TickEvent.RenderTickEvent event) {
if (event.phase != TickEvent.Phase.START) return;
if (currentScreen != mc.currentScreen) {
currentScreen = mc.currentScreen;
framesPassed = 0;
}
if (framesPassed < 10) {
framesPassed++;
return;
}
FMLCommonHandler.instance().bus().unregister(this);
Object foundGui = null;
if (AbstractGuiScreen.class.isAssignableFrom(guiClass)) {
AbstractGuiScreen guiScreen = GuiScreen.from(currentScreen);
if (guiClass.isInstance(guiScreen)) {
onOpen.consume(guiClass.cast(guiScreen));
return;
}
foundGui = guiScreen;
} else if (AbstractGuiOverlay.class.isAssignableFrom(guiClass)) {
AbstractGuiOverlay guiScreen = GuiOverlay.from(currentScreen);
if (guiClass.isInstance(guiScreen)) {
onOpen.consume(guiClass.cast(guiScreen));
return;
}
foundGui = guiScreen;
} else if (AbstractGuiPopup.class.isAssignableFrom(guiClass)) {
AbstractGuiPopup popup = getPopup(currentScreen);
if (guiClass.isInstance(popup)) {
onOpen.consume(guiClass.cast(popup));
return;
}
} else {
if (guiClass.isInstance(currentScreen)) {
onOpen.consume(guiClass.cast(currentScreen));
return;
}
}
class UnexpectedGuiException extends Exception {
UnexpectedGuiException(Object foundGui) {
super("Expected instance of " + guiClass + " but found " + foundGui);
setStackTrace(Arrays.copyOfRange(stackTrace, 2, stackTrace.length));
}
}
future.setException(new UnexpectedGuiException(foundGui == null ? currentScreen : foundGui));
}
}
FMLCommonHandler.instance().bus().register(new EventHandler());
}
private void clickNow(int x, int y) {
try {
Method method = net.minecraft.client.gui.GuiScreen.class
.getDeclaredMethod("mouseClicked", int.class, int.class, int.class);
method.setAccessible(true);
method.invoke(mc.currentScreen, x, y, 0);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
future.setException(e);
}
}
public void click(int x, int y) {
runLater(() -> {
LOGGER.info("Clicking at {}/{}", x, y);
clickNow(x, y);
});
}
private void dragNow(int x, int y) {
try {
Method method = net.minecraft.client.gui.GuiScreen.class
.getDeclaredMethod("mouseClickMove", int.class, int.class, int.class, long.class);
method.setAccessible(true);
method.invoke(mc.currentScreen, x, y, 0, 0);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
future.setException(e);
}
}
public void drag(int x, int y) {
runLater(() -> {
LOGGER.info("Dragging to {}/{}", x, y);
dragNow(x, y);
});
}
public void click(GuiButton button) {
runLater(() -> {
if (!button.isEnabled()) {
future.setException(new IllegalStateException("Button is disabled: " + button.getLabel()));
return;
}
LOGGER.info("Clicking button {}", button.getLabel());
button.onClick();
});
}
public void click(GuiTexturedButton button) {
runLater(() -> {
if (!button.isEnabled()) {
future.setException(new IllegalStateException("Button is disabled: " + button.getTexture()));
return;
}
LOGGER.info("Clicking textured button {}", button.getTexture());
button.onClick();
});
}
public void click(String buttonText) {
runLater(() -> {
LOGGER.info("Clicking button {}", buttonText);
try {
Field field = net.minecraft.client.gui.GuiScreen.class.getDeclaredField("buttonList");
field.setAccessible(true);
@SuppressWarnings("unchecked")
List<net.minecraft.client.gui.GuiButton> buttonList = (List)
field.get(mc.currentScreen);
net.minecraft.client.gui.GuiButton button = null;
for (net.minecraft.client.gui.GuiButton guiButton : buttonList) {
if (guiButton.displayString.equals(buttonText)) {
button = guiButton;
}
}
if (button == null) {
future.setException(new NoSuchElementException("No button with label: " + buttonText));
return;
}
clickNow(button.xPosition + 5, button.yPosition + 5);
} catch (IllegalAccessException | NoSuchFieldException e) {
future.setException(e);
}
});
}
public void type(String string) {
for (char c : string.toCharArray()) {
press(c, Keyboard.getKeyIndex(String.valueOf(c).toUpperCase()));
}
}
public void press(int keyCode) {
String keyName = Keyboard.getKeyName(keyCode);
char character = keyName.length() == 1 ? keyName.charAt(0) : '\0';
press(character, keyCode);
}
public void press(char character, int keyCode) {
runLater(() -> {
LOGGER.info("Pressing key {}", Keyboard.getKeyName(keyCode));
if (mc.currentScreen == null || mc.currentScreen.allowUserInput) {
KeyBinding.onTick(keyCode);
FMLCommonHandler.instance().fireKeyInput();
}
if (mc.currentScreen != null) {
try {
Method method = net.minecraft.client.gui.GuiScreen.class
.getDeclaredMethod("keyTyped", char.class, int.class);
method.setAccessible(true);
method.invoke(mc.currentScreen, character, keyCode);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
future.setException(e);
}
}
});
}
}

View File

@@ -0,0 +1,41 @@
package com.replaymod.core;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import static com.replaymod.core.ReplayModIntegrationTest.LOGGER;
import static com.replaymod.core.utils.Utils.addCallback;
public class CompositeTask implements Task {
private SettableFuture<Void> future;
protected final Task[] children;
public CompositeTask(Task[] children) {
this.children = children;
}
@Override
public ListenableFuture<Void> execute() {
future = SettableFuture.create();
executeChild(0);
return future;
}
private void executeChild(int childIndex) {
if (future.isDone()) return;
if (childIndex < children.length) {
ReplayMod.instance.runLater(() -> {
try {
Task task = children[childIndex];
LOGGER.info("Running task {}", task);
ListenableFuture<Void> childFuture = task.execute();
addCallback(childFuture, done -> executeChild(childIndex + 1), err -> future.setException(err));
} catch (Throwable t) {
future.setException(t);
}
});
} else {
future.set(null);
}
}
}

View File

@@ -0,0 +1,73 @@
package com.replaymod.core;
import com.replaymod.core.regression.RegressionTest60;
import com.replaymod.core.regression.RegressionTest61;
import com.replaymod.core.regression.RegressionTest62;
import com.replaymod.extra.DownloadOpenEye;
import com.replaymod.online.SkipLogin;
import com.replaymod.recording.CreateSPWorld;
import com.replaymod.recording.ExitSPWorld;
import com.replaymod.replay.ExitReplay;
import com.replaymod.replay.LoadReplay;
import com.replaymod.replay.OpenReplayViewer;
import net.minecraft.client.Minecraft;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import org.apache.logging.log4j.Logger;
import static com.replaymod.core.AbstractTask.mc;
import static com.replaymod.core.ReplayModIntegrationTest.MOD_ID;
import static com.replaymod.core.utils.Utils.addCallback;
/**
* Helper mod that initiates the integration tests.
*/
@Mod(modid = MOD_ID)
public class ReplayModIntegrationTest {
public static final String MOD_ID = "replaymod-integration-test";
public static Logger LOGGER;
@Mod.EventHandler
public void init(FMLPreInitializationEvent event) {
LOGGER = event.getModLog();
// Make sure the game window doesn't have to remain in focus during the test
mc.gameSettings.pauseOnLostFocus = false;
runTasks(
new SkipLogin(),
new DownloadOpenEye(),
new CreateSPWorld(),
new Wait(5000),
new ExitSPWorld(),
new OpenReplayViewer(),
new LoadReplay(),
new RegressionTest60(),
new RegressionTest61(),
new RegressionTest62(),
// new AbstractTask() {}, // Uncomment to not exit on success (useful for writing more tests)
new ExitReplay()
);
}
private void runTasks(Task... tests) {
addCallback(new CompositeTask(tests).execute(), success -> {
if (!Minecraft.getMinecraft().hasCrashed) {
LOGGER.info("===================================================");
LOGGER.info("= ALL TESTS PASSED =");
LOGGER.info("===================================================");
FMLCommonHandler.instance().exitJava(0, false);
}
}, error -> {
LOGGER.error("Failed task:", error);
LOGGER.error("===================================================");
LOGGER.error("= TEST FAILED =");
LOGGER.error("===================================================");
FMLCommonHandler.instance().exitJava(1, false);
});
}
}

View File

@@ -0,0 +1,35 @@
package com.replaymod.core;
import com.google.common.util.concurrent.ListenableFuture;
public interface Task {
ListenableFuture<Void> execute();
static Task click(int x, int y) {
return AbstractTask.create(task -> {
task.click(x, y);
task.runLater(() -> task.future.set(null));
});
}
static Task drag(int x, int y) {
return AbstractTask.create(task -> {
task.drag(x, y);
task.runLater(() -> task.future.set(null));
});
}
static Task pressKey(int keyCode) {
return AbstractTask.create(task -> {
task.press(keyCode);
task.runLater(() -> task.future.set(null));
});
}
static Task pressKey(char character, int keyCode) {
return AbstractTask.create(task -> {
task.press(character, keyCode);
task.runLater(() -> task.future.set(null));
});
}
}

View File

@@ -0,0 +1,9 @@
package com.replaymod.core;
public class Utils {
public static void times(int x, Runnable runnable) {
for (int i = 0; i < x; i++) {
runnable.run();
}
}
}

View File

@@ -0,0 +1,21 @@
package com.replaymod.core;
public class Wait extends AbstractTask {
private final int duration;
public Wait(int duration) {
this.duration = duration;
}
@Override
protected void init() {
new Thread(() -> {
try {
Thread.sleep(duration);
runLater(() -> future.set(null));
} catch (InterruptedException e) {
runLater(() -> future.setException(e));
}
}).start();
}
}

View File

@@ -0,0 +1,59 @@
package com.replaymod.core.regression;
import com.replaymod.core.AbstractTask;
import com.replaymod.core.CompositeTask;
import com.replaymod.core.Task;
import com.replaymod.replay.SpectatePlayer;
import com.replaymod.replay.overlay.OverlayGui;
import com.replaymod.simplepathing.GuiPathingTasks;
import com.replaymod.simplepathing.gui.GuiEditKeyframe;
import org.lwjgl.input.Keyboard;
import static com.replaymod.core.AbstractTask.mc;
import static com.replaymod.core.Utils.times;
/**
* Regression test: #60 Crash in path preview when two spectator keyframes are closer than 50ms
*/
public class RegressionTest60 extends CompositeTask {
public RegressionTest60() {
super(new Task[]{
new SpectatePlayer(),
OverlayGui.whileOpened(
// Place first spectator keyframe
new GuiPathingTasks.ClickPositionKeyframeButton(),
// Place second spectator keyframe
Task.click(130, 50),
new GuiPathingTasks.ClickPositionKeyframeButton(),
// Move second keyframe to 20ms on the keyframe timeline
AbstractTask.create(task -> {
// Double click keyframe
task.click(130, 50);
task.click(130, 50);
task.expectGui(GuiEditKeyframe.Spectator.class, gui -> {
// Set ms field to 20
task.click(mc.currentScreen.width / 2 + 80, mc.currentScreen.height / 2);
times(4, () -> task.press(Keyboard.KEY_BACK));
task.type("20");
// Set other fields to 0
times(2, () -> {
task.press(Keyboard.KEY_TAB);
times(4, () -> task.press(Keyboard.KEY_BACK));
task.press(Keyboard.KEY_0);
});
task.click(gui.saveButton);
task.expectPopupClosed(() -> task.future.set(null));
});
}),
// Place second spectator keyframe
new GuiPathingTasks.ClickPositionKeyframeButton()
),
// Stop spectating player
new SpectatePlayer.End(),
// Enable path preview
Task.pressKey(Keyboard.KEY_H),
new GuiPathingTasks.ClearKeyframeTimeline(),
Task.pressKey(Keyboard.KEY_H),
});
}
}

View File

@@ -0,0 +1,35 @@
package com.replaymod.core.regression;
import com.replaymod.core.AbstractTask;
import com.replaymod.core.CompositeTask;
import com.replaymod.core.Task;
import com.replaymod.replay.overlay.OverlayGui;
import com.replaymod.simplepathing.GuiPathingTasks;
import com.replaymod.simplepathing.gui.GuiEditKeyframe;
/**
* Regression test: #61 NPE when saving in edit keyframe gui of last keyframe
*/
public class RegressionTest61 extends CompositeTask {
public RegressionTest61() {
super(new Task[]{
OverlayGui.whileOpened(
// Place keyframe
Task.click(130, 50),
new GuiPathingTasks.ClickPositionKeyframeButton(),
AbstractTask.create(task -> {
// Double click keyframe
task.click(130, 50);
task.click(130, 50);
task.expectGui(GuiEditKeyframe.Position.class, gui -> {
task.click(gui.saveButton);
task.expectPopupClosed(() -> task.future.set(null));
});
}),
// Place second spectator keyframe
new GuiPathingTasks.ClickPositionKeyframeButton()
),
new GuiPathingTasks.ClearKeyframeTimeline(),
});
}
}

View File

@@ -0,0 +1,28 @@
package com.replaymod.core.regression;
import com.replaymod.core.CompositeTask;
import com.replaymod.core.Task;
import com.replaymod.replay.overlay.OverlayGui;
import com.replaymod.simplepathing.GuiPathingTasks;
/**
* Regression test: #62 Swapping the only two existing keyframes causes NPE
*/
public class RegressionTest62 extends CompositeTask {
public RegressionTest62() {
super(new Task[]{
OverlayGui.whileOpened(
// Place first keyframe
Task.click(130, 50),
new GuiPathingTasks.ClickPositionKeyframeButton(),
// Place second keyframe
Task.click(150, 50),
new GuiPathingTasks.ClickPositionKeyframeButton(),
// Move first keyframe past second keyframe
Task.click(130,50),
Task.drag(170,50)
),
new GuiPathingTasks.ClearKeyframeTimeline()
});
}
}

View File

@@ -0,0 +1,25 @@
package com.replaymod.extra;
import com.replaymod.core.AbstractTask;
import com.replaymod.extras.OpenEyeExtra;
import net.minecraftforge.fml.common.Loader;
import java.io.File;
import java.nio.file.NoSuchFileException;
public class DownloadOpenEye extends AbstractTask {
@Override
protected void init() {
expectGui(OpenEyeExtra.OfferGui.class, offerGui -> {
click(offerGui.yesButton);
expectGuiClosed(20 * 1000, () -> {
File targetFile = new File(mc.mcDataDir, "mods/" + Loader.MC_VERSION + "/OpenEye.jar");
if (!targetFile.exists()) {
future.setException(new NoSuchFileException(targetFile.getAbsolutePath()));
} else {
future.set(null);
}
});
});
}
}

View File

@@ -0,0 +1,14 @@
package com.replaymod.online;
import com.replaymod.core.AbstractTask;
import com.replaymod.online.gui.GuiLoginPrompt;
public class SkipLogin extends AbstractTask {
@Override
protected void init() {
expectGui(GuiLoginPrompt.class, gui -> {
click(gui.cancelButton);
expectGuiClosed(() -> future.set(null));
});
}
}

View File

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

View File

@@ -0,0 +1,36 @@
package com.replaymod.recording;
import com.replaymod.core.AbstractTask;
import net.minecraft.client.gui.GuiIngameMenu;
import net.minecraft.client.gui.GuiMainMenu;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import java.io.IOException;
public class ExitSPWorld extends AbstractTask {
@Override
protected void init() {
mc.displayInGameMenu();
expectGui(GuiIngameMenu.class, ingameMenu -> {
click("Save and Quit to Title");
expectGui(GuiMainMenu.class, mainMenu -> new Thread(() -> {
try {
while (true) {
String[] dirs = core.getReplayFolder().list(DirectoryFileFilter.DIRECTORY);
if (dirs == null) {
future.setException(new NullPointerException("dirs is null"));
return;
}
if (dirs.length == 0) {
runLater(() -> future.set(null));
return;
}
Thread.sleep(10);
}
} catch (IOException | InterruptedException e) {
future.setException(e);
}
}).start());
});
}
}

View File

@@ -0,0 +1,16 @@
package com.replaymod.replay;
import com.replaymod.core.AbstractTask;
import net.minecraft.client.gui.GuiIngameMenu;
import net.minecraft.client.gui.GuiMainMenu;
public class ExitReplay extends AbstractTask {
@Override
protected void init() {
mc.displayInGameMenu();
expectGui(GuiIngameMenu.class, ingameMenu -> {
click("Exit Replay");
expectGui(GuiMainMenu.class, mainMenu -> future.set(null));
});
}
}

View File

@@ -0,0 +1,32 @@
package com.replaymod.replay;
import com.replaymod.core.AbstractTask;
import com.replaymod.replay.gui.screen.GuiReplayViewer;
import de.johni0702.minecraft.gui.function.Clickable;
import net.minecraftforge.client.event.RenderGameOverlayEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import org.lwjgl.util.Point;
import org.lwjgl.util.ReadableDimension;
public class LoadReplay extends AbstractTask {
@Override
protected void init() {
expectGui(GuiReplayViewer.class, replayViewer -> runLater(() -> {
ReadableDimension size = replayViewer.getMaxSize();
// Select first entry
replayViewer.forEach(Clickable.class).mouseClick(new Point(size.getWidth() / 2, 40), 0);
// Load first replay
click(replayViewer.loadButton);
class EventHandler {
@SubscribeEvent
public void onRenderIngame(RenderGameOverlayEvent.Pre event) {
MinecraftForge.EVENT_BUS.unregister(this);
runLater(() -> future.set(null));
}
}
MinecraftForge.EVENT_BUS.register(new EventHandler());
}));
}
}

View File

@@ -0,0 +1,15 @@
package com.replaymod.replay;
import com.replaymod.core.AbstractTask;
import com.replaymod.replay.gui.screen.GuiReplayViewer;
import net.minecraft.client.gui.GuiMainMenu;
public class OpenReplayViewer extends AbstractTask {
@Override
protected void init() {
expectGui(GuiMainMenu.class, mainMenu -> {
click("Replay Viewer");
expectGui(GuiReplayViewer.class, replayViewer -> future.set(null));
});
}
}

View File

@@ -0,0 +1,42 @@
package com.replaymod.replay;
import com.replaymod.core.AbstractTask;
import com.replaymod.extras.playeroverview.PlayerOverviewGui;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import org.lwjgl.input.Keyboard;
import java.util.concurrent.TimeoutException;
public class SpectatePlayer extends AbstractTask {
@Override
protected void init() {
press(Keyboard.KEY_B);
expectGui(PlayerOverviewGui.class, overview -> {
click(overview.getMaxSize().getWidth() / 2 - 60, 60);
press(Keyboard.KEY_ESCAPE);
expectGuiClosed(() -> future.set(null));
});
}
public static class End extends AbstractTask {
private int timeout;
@Override
protected void init() {
runLater(() -> press(Keyboard.KEY_LSHIFT));
}
@SubscribeEvent
public void onTick(TickEvent.RenderTickEvent event) {
if (event.phase != TickEvent.Phase.START) return;
if (timeout++ > 20) {
future.setException(new TimeoutException("Camera hasn't stopped spectating."));
return;
}
if (mc.getRenderViewEntity() == mc.thePlayer) {
future.set(null);
}
}
}
}

View File

@@ -0,0 +1,33 @@
package com.replaymod.replay.overlay;
import com.replaymod.core.AbstractTask;
import com.replaymod.core.CompositeTask;
import com.replaymod.core.Task;
import com.replaymod.replay.gui.overlay.GuiReplayOverlay;
import org.lwjgl.input.Keyboard;
public class OverlayGui {
public static Task whileOpened(Task...tasks) {
Task[] nTasks = new Task[tasks.length + 2];
System.arraycopy(tasks, 0, nTasks, 1, tasks.length);
nTasks[0] = new Open();
nTasks[tasks.length + 1] = new Close();
return new CompositeTask(nTasks);
}
public static class Open extends AbstractTask {
@Override
protected void init() {
press(Keyboard.KEY_T);
expectGui(GuiReplayOverlay.class, done -> future.set(null));
}
}
public static class Close extends AbstractTask {
@Override
protected void init() {
press(Keyboard.KEY_ESCAPE);
expectGuiClosed(() -> future.set(null));
}
}
}

View File

@@ -0,0 +1,35 @@
package com.replaymod.simplepathing;
import com.replaymod.core.AbstractTask;
import com.replaymod.replay.gui.overlay.GuiReplayOverlay;
import com.replaymod.simplepathing.gui.GuiPathing;
import de.johni0702.minecraft.gui.popup.GuiYesNoPopup;
import org.lwjgl.input.Keyboard;
public abstract class GuiPathingTasks extends AbstractTask {
@Override
protected void init() {
expectGui(GuiReplayOverlay.class, ign -> init0(ReplayModSimplePathing.instance.getGuiPathing()));
}
protected abstract void init0(GuiPathing guiPathing);
public static class ClickPositionKeyframeButton extends GuiPathingTasks {
@Override
protected void init0(GuiPathing guiPathing) {
click(guiPathing.positionKeyframeButton);
runLater(() -> future.set(null));
}
}
public static class ClearKeyframeTimeline extends AbstractTask {
@Override
protected void init() {
press(Keyboard.KEY_C);
expectGui(GuiYesNoPopup.class, popup -> {
click(popup.getYesButton());
expectGuiClosed(() -> future.set(null));
});
}
}
}

View File

@@ -2,22 +2,21 @@ package com.replaymod.compat;
import com.replaymod.compat.optifine.DisableFastRender; import com.replaymod.compat.optifine.DisableFastRender;
import com.replaymod.compat.shaders.ShaderBeginRender; import com.replaymod.compat.shaders.ShaderBeginRender;
import com.replaymod.core.ReplayMod; import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent; import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.eventhandler.EventBus; import net.minecraftforge.fml.common.eventhandler.EventBus;
@Mod(modid = ReplayModCompat.MOD_ID, useMetadata = true) @Mod(modid = ReplayModCompat.MOD_ID,
version = "@MOD_VERSION@",
acceptedMinecraftVersions = "@MC_VERSION@",
useMetadata = true)
public class ReplayModCompat { public class ReplayModCompat {
public static final String MOD_ID = "replaymod-compat"; public static final String MOD_ID = "replaymod-compat";
@Mod.Instance(ReplayMod.MOD_ID)
private static ReplayMod core;
@Mod.EventHandler @Mod.EventHandler
public void init(FMLInitializationEvent event) { public void init(FMLInitializationEvent event) {
EventBus bus = MinecraftForge.EVENT_BUS; EventBus bus = FMLCommonHandler.instance().bus();
bus.register(new ShaderBeginRender()); bus.register(new ShaderBeginRender());
bus.register(new DisableFastRender()); bus.register(new DisableFastRender());
} }

View File

@@ -0,0 +1,22 @@
package com.replaymod.compat.shaders.mixin;
import net.minecraft.client.renderer.EntityRenderer;
import net.minecraftforge.client.ForgeHooksClient;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Pseudo
@Mixin(targets = "shadersmod/client/ShadersRender", remap = false)
public abstract class MixinShadersRender {
@Inject(method = "renderHand0", at = @At("HEAD"), cancellable = true)
private static void replayModCompat_disableRenderHand0(EntityRenderer er, float partialTicks, int renderPass, CallbackInfo ci) {
if (ForgeHooksClient.renderFirstPersonHand(er.mc.renderGlobal, partialTicks, renderPass)) {
ci.cancel();
}
}
}

View File

@@ -16,6 +16,7 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Callable;
public class KeyBindingRegistry { public class KeyBindingRegistry {
private Map<String, KeyBinding> keyBindings = new HashMap<String, KeyBinding>(); private Map<String, KeyBinding> keyBindings = new HashMap<String, KeyBinding>();
@@ -85,7 +86,7 @@ public class KeyBindingRegistry {
CrashReport crashReport = CrashReport.makeCrashReport(cause, "Handling Key Binding"); CrashReport crashReport = CrashReport.makeCrashReport(cause, "Handling Key Binding");
CrashReportCategory category = crashReport.makeCategory("Key Binding"); CrashReportCategory category = crashReport.makeCategory("Key Binding");
category.addCrashSection("Key Binding", keyBinding); category.addCrashSection("Key Binding", keyBinding);
category.setDetail("Handler", runnable::toString); category.addCrashSectionCallable("Handler", runnable::toString);
throw new ReportedException(crashReport); throw new ReportedException(crashReport);
} }
} }
@@ -100,7 +101,12 @@ public class KeyBindingRegistry {
CrashReport crashReport = CrashReport.makeCrashReport(cause, "Handling Raw Key Binding"); CrashReport crashReport = CrashReport.makeCrashReport(cause, "Handling Raw Key Binding");
CrashReportCategory category = crashReport.makeCategory("Key Binding"); CrashReportCategory category = crashReport.makeCategory("Key Binding");
category.addCrashSection("Key Code", keyCode); category.addCrashSection("Key Code", keyCode);
category.setDetail("Handler", runnable::toString); category.addCrashSectionCallable("Handler", new Callable() {
@Override
public Object call() throws Exception {
return runnable;
}
});
throw new ReportedException(crashReport); throw new ReportedException(crashReport);
} }
} }

View File

@@ -29,7 +29,7 @@ public class LoadingPlugin implements IFMLLoadingPlugin {
if (file.isFile()) { if (file.isFile()) {
// This forces forge to reexamine the jar file for FML mods // This forces forge to reexamine the jar file for FML mods
// Should eventually be handled by Mixin itself, maybe? // Should eventually be handled by Mixin itself, maybe?
CoreModManager.getIgnoredMods().remove(file.getName()); CoreModManager.getLoadedCoremods().remove(file.getName());
} }
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
e.printStackTrace(); e.printStackTrace();

View File

@@ -6,6 +6,7 @@ import com.replaymod.core.gui.GuiReplaySettings;
import com.replaymod.core.gui.RestoreReplayGui; import com.replaymod.core.gui.RestoreReplayGui;
import com.replaymod.core.handler.MainMenuHandler; import com.replaymod.core.handler.MainMenuHandler;
import com.replaymod.core.utils.OpenGLUtils; import com.replaymod.core.utils.OpenGLUtils;
import com.replaymod.render.utils.SoundHandler;
import com.replaymod.replaystudio.util.I18n; import com.replaymod.replaystudio.util.I18n;
import de.johni0702.minecraft.gui.container.GuiScreen; import de.johni0702.minecraft.gui.container.GuiScreen;
import lombok.Getter; import lombok.Getter;
@@ -13,11 +14,10 @@ import net.minecraft.client.Minecraft;
import net.minecraft.client.settings.GameSettings; import net.minecraft.client.settings.GameSettings;
import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReport;
import net.minecraft.crash.CrashReportCategory; import net.minecraft.crash.CrashReportCategory;
import net.minecraft.util.ResourceLocation; import net.minecraft.util.*;
import net.minecraft.util.text.*;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.config.Configuration; import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.fml.client.FMLClientHandler; import net.minecraftforge.fml.client.FMLClientHandler;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.Loader; import net.minecraftforge.fml.common.Loader;
import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.Mod.EventHandler; import net.minecraftforge.fml.common.Mod.EventHandler;
@@ -31,9 +31,12 @@ import org.apache.commons.io.FileUtils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import java.util.Queue;
@Mod(modid = ReplayMod.MOD_ID, @Mod(modid = ReplayMod.MOD_ID,
useMetadata = true, useMetadata = true,
version = "@MOD_VERSION@",
acceptedMinecraftVersions = "@MC_VERSION@",
updateJSON = "https://raw.githubusercontent.com/ReplayMod/ReplayMod/master/versions.json", updateJSON = "https://raw.githubusercontent.com/ReplayMod/ReplayMod/master/versions.json",
guiFactory = "com.replaymod.core.gui.GuiFactory") guiFactory = "com.replaymod.core.gui.GuiFactory")
public class ReplayMod { public class ReplayMod {
@@ -65,6 +68,8 @@ public class ReplayMod {
@Deprecated @Deprecated
public static Configuration config; public static Configuration config;
@Deprecated
public static SoundHandler soundHandler = new SoundHandler();
private final KeyBindingRegistry keyBindingRegistry = new KeyBindingRegistry(); private final KeyBindingRegistry keyBindingRegistry = new KeyBindingRegistry();
private final SettingsRegistry settingsRegistry = new SettingsRegistry(); private final SettingsRegistry settingsRegistry = new SettingsRegistry();
@@ -107,7 +112,7 @@ public class ReplayMod {
new MainMenuHandler().register(); new MainMenuHandler().register();
MinecraftForge.EVENT_BUS.register(keyBindingRegistry); FMLCommonHandler.instance().bus().register(keyBindingRegistry);
getKeyBindingRegistry().registerKeyBinding("replaymod.input.settings", 0, () -> { getKeyBindingRegistry().registerKeyBinding("replaymod.input.settings", 0, () -> {
new GuiReplaySettings(null, settingsRegistry).display(); new GuiReplaySettings(null, settingsRegistry).display();
@@ -121,7 +126,150 @@ public class ReplayMod {
if(!FMLClientHandler.instance().hasOptifine()) if(!FMLClientHandler.instance().hasOptifine())
GameSettings.Options.RENDER_DISTANCE.setValueMax(64f); GameSettings.Options.RENDER_DISTANCE.setValueMax(64f);
testIfMoeshAndExitMinecraft(); if (System.getProperty("replaymod.render.file") != null) {
final File file = new File(System.getProperty("replaymod.render.file"));
if (!file.exists()) {
throw new IOException("File \"" + file.getPath() + "\" not found.");
}
final String path = System.getProperty("replaymod.render.path");
String type = System.getProperty("replaymod.render.type");
String bitrate = System.getProperty("replaymod.render.bitrate");
String fps = System.getProperty("replaymod.render.fps");
String waitForChunks = System.getProperty("replaymod.render.waitforchunks");
String linearMovement = System.getProperty("replaymod.render.linearmovement");
String skyColor = System.getProperty("replaymod.render.skycolor");
String width = System.getProperty("replaymod.render.width");
String height = System.getProperty("replaymod.render.height");
String exportCommand = System.getProperty("replaymod.render.exportcommand");
String exportCommandArgs = System.getProperty("replaymod.render.exportcommandargs");
/*
final RenderOptions options = new RenderOptions();
if (bitrate != null) {
options.setBitrate(bitrate);
}
if (fps != null) {
options.setFps(Integer.parseInt(fps));
}
if (waitForChunks != null) {
options.setWaitForChunks(Boolean.parseBoolean(waitForChunks));
}
if (linearMovement != null) {
options.setLinearMovement(Boolean.parseBoolean(linearMovement));
}
if (skyColor != null) {
if (skyColor.startsWith("0x")) {
options.setSkyColor(Integer.parseInt(skyColor.substring(2), 16));
} else {
options.setSkyColor(Integer.parseInt(skyColor));
}
}
if (width != null) {
options.setWidth(Integer.parseInt(width));
}
if (height != null) {
options.setHeight(Integer.parseInt(height));
}
if (exportCommand != null) {
options.setExportCommand(exportCommand);
}
if (exportCommandArgs != null) {
options.setExportCommandArgs(exportCommandArgs);
} else {
options.setExportCommandArgs(EncodingPreset.MP4DEFAULT.getCommandLineArgs());
}
options.setOutputFile(new File(String.valueOf(System.currentTimeMillis())));
Pipelines.Preset pipelinePreset = Pipelines.Preset.DEFAULT;
if (type != null) {
String[] parts = type.split(":");
type = parts[0].toUpperCase();
if ("NORMAL".equals(type) || "DEFAULT".equals(type)) {
pipelinePreset = Pipelines.Preset.DEFAULT;
} else if ("STEREO".equals(type) || "STEREOSCOPIC".equals(type)) {
pipelinePreset = Pipelines.Preset.STEREOSCOPIC;
} else if ("CUBIC".equals(type)) {
if (parts.length < 2) {
throw new IllegalArgumentException("Cubic renderer requires boolean for whether it's stable.");
}
pipelinePreset = Pipelines.Preset.CUBIC;
} else if ("EQUIRECTANGULAR".equals(type)) {
if (parts.length < 2) {
throw new IllegalArgumentException("Equirectangular renderer requires boolean for whether it's stable.");
}
pipelinePreset = Pipelines.Preset.EQUIRECTANGULAR;
} else if ("ODS".equals(type)) {
if (parts.length < 2) {
throw new IllegalArgumentException("ODS renderer requires boolean for whether it's stable.");
}
pipelinePreset = Pipelines.Preset.ODS;
} else {
throw new IllegalArgumentException("Unknown type: " + parts[0]);
}
}
options.setMode(pipelinePreset);
*/
@SuppressWarnings("unchecked")
Queue<ListenableFutureTask> tasks = mc.scheduledTasks;
synchronized (mc.scheduledTasks) {
tasks.add(ListenableFutureTask.create(new Runnable() {
@Override
public void run() {
String renderDistance = System.getProperty("replaymod.render.mc.renderdistance");
String clouds = System.getProperty("replaymod.render.mc.clouds");
if (renderDistance != null) {
mc.gameSettings.renderDistanceChunks = Integer.parseInt(renderDistance);
}
if (clouds != null) {
mc.gameSettings.clouds = Boolean.parseBoolean(clouds);
}
System.out.println("Loading replay...");
// TODO
// try {
// ReplayHandler.startReplay(file, false);
// } catch (Throwable t) {
// t.printStackTrace();
// FMLCommonHandler.instance().exitJava(1, false);
// }
//
// int index = 0;
// if (path != null) {
// for (KeyframeSet set : ReplayHandler.getKeyframeRepository()) {
// if (set.getName().equals(path)) {
// break;
// }
// index++;
// }
// if (index >= ReplayHandler.getKeyframeRepository().length) {
// throw new IllegalArgumentException("No path named \"" + path + "\".");
// }
// } else if (ReplayHandler.getKeyframeRepository().length == 0) {
// throw new IllegalArgumentException("Replay file has no paths defined.");
// }
// ReplayHandler.useKeyframePresetFromRepository(index);
//
// System.out.println("Rendering started...");
// try {
// ReplayProcess.startReplayProcess(options, true);
// } catch (Throwable t) {
// t.printStackTrace();
// FMLCommonHandler.instance().exitJava(1, false);
// }
// if (mc.hasCrashed) {
// System.out.println(mc.crashReporter.getCompleteReport());
// }
// System.out.println("Rendering done. Shutting down...");
// mc.shutdown();
}
}, null));
}
testIfMoeshAndExitMinecraft();
}
runLater(() -> { runLater(() -> {
// Cleanup deleted corrupted replays // Cleanup deleted corrupted replays
@@ -158,8 +306,10 @@ public class ReplayMod {
} }
public void runLater(Runnable runnable) { public void runLater(Runnable runnable) {
@SuppressWarnings("unchecked")
Queue<ListenableFutureTask> tasks = mc.scheduledTasks;
synchronized (mc.scheduledTasks) { synchronized (mc.scheduledTasks) {
mc.scheduledTasks.add(ListenableFutureTask.create(runnable, null)); tasks.add(ListenableFutureTask.create(runnable, null));
} }
} }
@@ -188,13 +338,13 @@ public class ReplayMod {
private void printToChat(boolean warning, String message, Object... args) { private void printToChat(boolean warning, String message, Object... args) {
if (getSettingsRegistry().get(Setting.NOTIFICATIONS)) { if (getSettingsRegistry().get(Setting.NOTIFICATIONS)) {
// Some nostalgia: "§8[§6Replay Mod§8]§r Your message goes here" // Some nostalgia: "§8[§6Replay Mod§8]§r Your message goes here"
Style coloredDarkGray = new Style().setColor(TextFormatting.DARK_GRAY); ChatStyle coloredDarkGray = new ChatStyle().setColor(EnumChatFormatting.DARK_GRAY);
Style coloredGold = new Style().setColor(TextFormatting.GOLD); ChatStyle coloredGold = new ChatStyle().setColor(EnumChatFormatting.GOLD);
ITextComponent text = new TextComponentString("[").setStyle(coloredDarkGray) IChatComponent text = new ChatComponentText("[").setChatStyle(coloredDarkGray)
.appendSibling(new TextComponentTranslation("replaymod.title").setStyle(coloredGold)) .appendSibling(new ChatComponentTranslation("replaymod.title").setChatStyle(coloredGold))
.appendSibling(new TextComponentString("] ")) .appendSibling(new ChatComponentText("] "))
.appendSibling(new TextComponentTranslation(message, args).setStyle(new Style() .appendSibling(new ChatComponentTranslation(message, args).setChatStyle(new ChatStyle()
.setColor(warning ? TextFormatting.RED : TextFormatting.DARK_GREEN))); .setColor(warning ? EnumChatFormatting.RED : EnumChatFormatting.DARK_GREEN)));
// Send message to chat GUI // Send message to chat GUI
// The ingame GUI is initialized at startup, therefore this is possible before the client is connected // The ingame GUI is initialized at startup, therefore this is possible before the client is connected
mc.ingameGUI.getChatGUI().printChatMessage(text); mc.ingameGUI.getChatGUI().printChatMessage(text);

View File

@@ -2,8 +2,8 @@ package com.replaymod.core;
import com.replaymod.core.events.SettingsChangedEvent; import com.replaymod.core.events.SettingsChangedEvent;
import net.minecraft.client.resources.I18n; import net.minecraft.client.resources.I18n;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.config.Configuration; import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.fml.common.FMLCommonHandler;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
@@ -83,7 +83,7 @@ public class SettingsRegistry {
throw new IllegalArgumentException("Default type " + key.getDefault().getClass() + " not supported."); throw new IllegalArgumentException("Default type " + key.getDefault().getClass() + " not supported.");
} }
settings.put(key, value); settings.put(key, value);
MinecraftForge.EVENT_BUS.post(new SettingsChangedEvent(this, key)); FMLCommonHandler.instance().bus().post(new SettingsChangedEvent(this, key));
} }
public void save() { public void save() {

View File

@@ -3,6 +3,7 @@ package com.replaymod.core.gui;
import com.google.common.io.Files; import com.google.common.io.Files;
import com.replaymod.replaystudio.PacketData; import com.replaymod.replaystudio.PacketData;
import com.replaymod.replaystudio.io.ReplayInputStream; import com.replaymod.replaystudio.io.ReplayInputStream;
import com.replaymod.replaystudio.io.ReplayOutputStream;
import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ReplayMetaData; import com.replaymod.replaystudio.replay.ReplayMetaData;
import com.replaymod.replaystudio.replay.ZipReplayFile; import com.replaymod.replaystudio.replay.ZipReplayFile;
@@ -45,15 +46,19 @@ public class RestoreReplayGui extends AbstractGuiScreen<RestoreReplayGui> {
ReplayMetaData metaData = replayFile.getMetaData(); ReplayMetaData metaData = replayFile.getMetaData();
if (metaData != null && metaData.getDuration() == 0) { if (metaData != null && metaData.getDuration() == 0) {
// Try to restore replay duration // Try to restore replay duration
try (ReplayInputStream in = replayFile.getPacketData()) { // We need to re-write the packet data in case there are any incomplete packets dangling at the end
try (ReplayInputStream in = replayFile.getPacketData();
ReplayOutputStream out = replayFile.writePacketData()) {
PacketData last = null; PacketData last = null;
while ((last = in.readPacket()) != null) { while ((last = in.readPacket()) != null) {
metaData.setDuration((int) last.getTime()); metaData.setDuration((int) last.getTime());
out.write(last);
} }
replayFile.writeMetaData(metaData);
} catch (Throwable t) { } catch (Throwable t) {
t.printStackTrace(); t.printStackTrace();
} }
// Write back the actual duration
replayFile.writeMetaData(metaData);
} }
replayFile.save(); replayFile.save();
replayFile.close(); replayFile.close();

View File

@@ -6,6 +6,8 @@ import net.minecraftforge.client.event.GuiScreenEvent;
import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import java.util.List;
/** /**
* Moves certain buttons on the main menu upwards so we can inject our own. * Moves certain buttons on the main menu upwards so we can inject our own.
*/ */
@@ -16,12 +18,14 @@ public class MainMenuHandler {
@SubscribeEvent @SubscribeEvent
public void onInit(GuiScreenEvent.InitGuiEvent.Post event) { public void onInit(GuiScreenEvent.InitGuiEvent.Post event) {
if (event.getGui() instanceof GuiMainMenu) { if (event.gui instanceof GuiMainMenu) {
for (GuiButton button : event.getButtonList()) { @SuppressWarnings("unchecked")
List<GuiButton> buttonList = event.buttonList;
for (GuiButton button : buttonList) {
// Buttons that aren't in a rectangle directly above our space don't need moving // Buttons that aren't in a rectangle directly above our space don't need moving
if (button.xPosition + button.width < event.getGui().width / 2 - 100 if (button.xPosition + button.width < event.gui.width / 2 - 100
|| button.xPosition > event.getGui().width / 2 + 100 || button.xPosition > event.gui.width / 2 + 100
|| button.yPosition > event.getGui().height / 4 + 10 + 4 * 24) continue; || button.yPosition > event.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 two rows of buttons
// and then move back down by 10 to compensate for the space to the exit button that was already there // and then move back down by 10 to compensate for the space to the exit button that was already there
button.yPosition -= 2 * 24 - 10; button.yPosition -= 2 * 24 - 10;

View File

@@ -2,8 +2,7 @@ package com.replaymod.core.utils;
import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.VertexBuffer; import net.minecraft.client.renderer.WorldRenderer;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import org.lwjgl.BufferUtils; import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL11;
@@ -35,14 +34,14 @@ public class OpenGLUtils {
float f5 = 1.0F / textureHeight; float f5 = 1.0F / textureHeight;
Tessellator tessellator = Tessellator.getInstance(); Tessellator tessellator = Tessellator.getInstance();
VertexBuffer vertexBuffer = tessellator.getBuffer(); WorldRenderer worldrenderer = tessellator.getWorldRenderer();
GlStateManager.translate(x+(width/2), y+(width/2), 0); GlStateManager.translate(x+(width/2), y+(width/2), 0);
GlStateManager.rotate(rotation, 0, 0, 1); GlStateManager.rotate(rotation, 0, 0, 1);
vertexBuffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX); worldrenderer.startDrawingQuads();
vertexBuffer.pos(-width / 2, height / 2, 0.0D).tex((double) (u * f4), (double) ((v + (float) height) * f5)).endVertex(); worldrenderer.addVertexWithUV(-width / 2, height / 2, 0.0D, (double) (u * f4), (double) ((v + (float) height) * f5));
vertexBuffer.pos(width/2, height/2, 0.0D).tex((double)((u + (float)width) * f4), (double)((v + (float)height) * f5)).endVertex(); worldrenderer.addVertexWithUV(width/2, height/2, 0.0D, (double)((u + (float)width) * f4), (double)((v + (float)height) * f5));
vertexBuffer.pos(width/2, -height/2, 0.0D).tex((double)((u + (float)width) * f4), (double)(v * f5)).endVertex(); worldrenderer.addVertexWithUV(width/2, -height/2, 0.0D, (double)((u + (float)width) * f4), (double)(v * f5));
vertexBuffer.pos(-width/2, -height/2, 0.0D).tex((double)(u * f4), (double)(v * f5)).endVertex(); worldrenderer.addVertexWithUV(-width/2, -height/2, 0.0D, (double)(u * f4), (double)(v * f5));
tessellator.draw(); tessellator.draw();
GlStateManager.popMatrix(); GlStateManager.popMatrix();

View File

@@ -1,7 +1,7 @@
package com.replaymod.core.utils; package com.replaymod.core.utils;
import net.minecraft.network.PacketBuffer; import net.minecraft.network.PacketBuffer;
import net.minecraft.network.play.server.SPacketCustomPayload; import net.minecraft.network.play.server.S3FPacketCustomPayload;
/** /**
* Restrictions set by the server, * Restrictions set by the server,
@@ -14,7 +14,7 @@ public class Restrictions {
private boolean onlyFirstPerson; private boolean onlyFirstPerson;
private boolean onlyRecordingPlayer; private boolean onlyRecordingPlayer;
public String handle(SPacketCustomPayload packet) { public String handle(S3FPacketCustomPayload packet) {
PacketBuffer buffer = packet.getBufferData(); PacketBuffer buffer = packet.getBufferData();
while (buffer.isReadable()) { while (buffer.isReadable()) {
String name = buffer.readStringFromBuffer(64); String name = buffer.readStringFromBuffer(64);

View File

@@ -1,16 +1,55 @@
package com.replaymod.core.utils; package com.replaymod.core.utils;
import net.minecraft.client.Minecraft; import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.AbstractGuiScrollable;
import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiScrollable;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiElement;
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.GuiInfoPopup;
import de.johni0702.minecraft.gui.utils.Colors;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.network.NetworkPlayerInfo; import net.minecraft.client.network.NetworkPlayerInfo;
import net.minecraft.client.resources.DefaultPlayerSkin; import net.minecraft.client.resources.DefaultPlayerSkin;
import net.minecraft.crash.CrashReport;
import net.minecraft.util.ResourceLocation; import net.minecraft.util.ResourceLocation;
import org.apache.logging.log4j.Logger;
import org.lwjgl.input.Keyboard; import org.lwjgl.input.Keyboard;
import org.lwjgl.util.Dimension; import org.lwjgl.util.Dimension;
import org.lwjgl.util.ReadableDimension; import org.lwjgl.util.ReadableDimension;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.UUID; import java.util.UUID;
import java.util.function.Consumer;
import static net.minecraft.client.Minecraft.getMinecraft;
public class Utils { public class Utils {
@@ -27,6 +66,42 @@ public class Utils {
DEFAULT_THUMBNAIL = thumbnail; DEFAULT_THUMBNAIL = thumbnail;
} }
/**
* Neither the root certificate of LetsEncrypt nor the root that cross-signed it is included in the default
* Java keystore prior to 8u101.
* Therefore whenever a connection to the replaymod.com site is made, this SSLContext has to be used instead.
* It has been constructed to include the necessary root certificates.
* @see #SSL_SOCKET_FACTORY
*/
public static final SSLContext SSL_CONTEXT;
/**
* @see #SSL_CONTEXT
*/
public static final SSLSocketFactory SSL_SOCKET_FACTORY;
static {
// Largely from https://community.letsencrypt.org/t/134/37
try (InputStream in = Utils.class.getResourceAsStream("/dst_root_ca_x3.pem")){
Certificate certificate = CertificateFactory.getInstance("X.509").generateCertificate(in);
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("1", certificate);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, trustManagerFactory.getTrustManagers(), null);
SSL_CONTEXT = ctx;
SSL_SOCKET_FACTORY = ctx.getSocketFactory();
} catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
}
}
public static String convertSecondsToShortString(int seconds) { public static String convertSecondsToShortString(int seconds) {
int hours = seconds/(60*60); int hours = seconds/(60*60);
int min = seconds/60 - hours*60; int min = seconds/60 - hours*60;
@@ -63,7 +138,7 @@ public class Utils {
} }
public static ResourceLocation getResourceLocationForPlayerUUID(UUID uuid) { public static ResourceLocation getResourceLocationForPlayerUUID(UUID uuid) {
NetworkPlayerInfo info = Minecraft.getMinecraft().getConnection().getPlayerInfo(uuid); NetworkPlayerInfo info = getMinecraft().getNetHandler().getPlayerInfo(uuid);
ResourceLocation skinLocation; ResourceLocation skinLocation;
if (info != null && info.hasLocationSkin()) { if (info != null && info.hasLocationSkin()) {
skinLocation = info.getLocationSkin(); skinLocation = info.getLocationSkin();
@@ -76,4 +151,94 @@ public class Utils {
public static boolean isCtrlDown() { public static boolean isCtrlDown() {
return Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); return Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL);
} }
public static <T> void addCallback(ListenableFuture<T> future, Consumer<T> onSuccess, Consumer<Throwable> onFailure) {
Futures.addCallback(future, new FutureCallback<T>() {
@Override
public void onSuccess(@Nullable T result) {
onSuccess.accept(result);
}
@Override
public void onFailure(@Nonnull Throwable t) {
onFailure.accept(t);
}
});
}
public static GuiInfoPopup error(Logger logger, GuiContainer container, CrashReport crashReport, Runnable onClose) {
// Convert crash report to string
String crashReportStr = crashReport.getCompleteReport();
// Log via logger
logger.error(crashReportStr);
// Try to save the crash report
if (crashReport.getFile() == null) {
try {
File folder = new File(getMinecraft().mcDataDir, "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);
} catch (Throwable t) {
logger.error("Saving crash report file:", t);
}
} else {
logger.debug("Not saving crash report as file already exists: {}", crashReport.getFile());
}
logger.trace("Opening crash report popup GUI");
GuiCrashReportPopup popup = new GuiCrashReportPopup(container, crashReportStr);
Futures.addCallback(popup.getFuture(), new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
logger.trace("Crash report popup closed");
if (onClose != null) {
onClose.run();
}
}
@Override
public void onFailure(Throwable t) {
logger.error("During error popup:", t);
}
});
return popup;
}
private static class GuiCrashReportPopup extends GuiInfoPopup {
private final GuiScrollable scrollable;
public GuiCrashReportPopup(GuiContainer container, String crashReport) {
super(container);
setBackgroundColor(Colors.DARK_TRANSPARENT);
// Add crash report to scrollable info
getInfo().addElements(new VerticalLayout.Data(0.5),
new GuiLabel().setColor(Colors.BLACK).setI18nText("replaymod.gui.unknownerror"),
scrollable = new GuiScrollable().setScrollDirection(AbstractGuiScrollable.Direction.VERTICAL)
.setLayout(new VerticalLayout().setSpacing(2))
.addElements(null,Arrays.stream(crashReport.replace("\t", " ").split("\n")).map(
l -> new GuiLabel().setText(l).setColor(Colors.BLACK)).toArray(GuiElement[]::new)));
// Replace close button with panel containing close and copy buttons
GuiButton copyToClipboardButton = new GuiButton().setI18nLabel("chat.copy").onClick(() ->
GuiScreen.setClipboardString(crashReport)).setSize(150, 20);
GuiButton closeButton = getCloseButton();
popup.removeElement(closeButton);
popup.addElements(new VerticalLayout.Data(1),
new GuiPanel().setLayout(new HorizontalLayout().setSpacing(5)).setSize(305, 20)
.addElements(null, copyToClipboardButton, closeButton));
// And open it
open();
}
@Override
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
// Re-size the scrollable containing the crash report to 3/4 of the screen
scrollable.setSize(size.getWidth() * 3 / 4, size.getHeight() * 3 / 4);
super.draw(renderer, size, renderInfo);
}
}
} }

View File

@@ -27,7 +27,7 @@ public class WrappedTimer extends Timer {
to.elapsedPartialTicks = from.elapsedPartialTicks; to.elapsedPartialTicks = from.elapsedPartialTicks;
to.lastSyncSysClock = from.lastSyncSysClock; to.lastSyncSysClock = from.lastSyncSysClock;
to.lastSyncHRClock = from.lastSyncHRClock; to.lastSyncHRClock = from.lastSyncHRClock;
to.counter = from.counter; to.field_74285_i = from.field_74285_i;
to.timeSyncAdjustment = from.timeSyncAdjustment; to.timeSyncAdjustment = from.timeSyncAdjustment;
} }
} }

View File

@@ -0,0 +1,41 @@
package com.replaymod.editor;
import com.replaymod.core.ReplayMod;
import com.replaymod.editor.handler.GuiHandler;
import com.replaymod.online.Setting;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import org.apache.logging.log4j.Logger;
@Mod(modid = ReplayModEditor.MOD_ID,
version = "@MOD_VERSION@",
acceptedMinecraftVersions = "@MC_VERSION@",
useMetadata = true)
public class ReplayModEditor {
public static final String MOD_ID = "replaymod-editor";
@Mod.Instance(MOD_ID)
public static ReplayModEditor instance;
private ReplayMod core;
public static Logger LOGGER;
@Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) {
ReplayModEditor.LOGGER = event.getModLog();
core = ReplayMod.instance;
core.getSettingsRegistry().register(Setting.class);
}
@Mod.EventHandler
public void init(FMLInitializationEvent event) {
new GuiHandler(this).register();
}
public ReplayMod getCore() {
return core;
}
}

View File

@@ -0,0 +1,389 @@
package com.replaymod.editor.gui;
import com.google.common.io.Files;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.gson.JsonObject;
import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Utils;
import com.replaymod.replaystudio.PacketData;
import com.replaymod.replaystudio.Studio;
import com.replaymod.replaystudio.filter.StreamFilter;
import com.replaymod.replaystudio.io.ReplayInputStream;
import com.replaymod.replaystudio.io.ReplayOutputStream;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ReplayMetaData;
import com.replaymod.replaystudio.replay.ZipReplayFile;
import com.replaymod.replaystudio.stream.PacketStream;
import com.replaymod.replaystudio.studio.ReplayStudio;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
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.GuiButton;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTextField;
import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar;
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.GuiInfoPopup;
import de.johni0702.minecraft.gui.popup.GuiYesNoPopup;
import de.johni0702.minecraft.gui.utils.Colors;
import net.minecraft.crash.CrashReport;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.lwjgl.util.ReadableDimension;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import static com.replaymod.editor.ReplayModEditor.LOGGER;
public class GuiReplayEditor extends GuiScreen {
private final ReplayMod mod;
private volatile ProcessingStage processingStage;
private volatile double progress;
private GuiProcessingProgressPopup popup;
public GuiButton currentTabButton;
public GuiPanel currentTabPanel;
public final GuiLabel warningLabel = new GuiLabel(this).setColor(Colors.RED)
.setI18nText("replaymod.gui.editor.disclaimer");
public final GuiPanel tabButtons = new GuiPanel(this).setLayout(new GridLayout().setSpacingX(5));
public final List<GuiPanel> tabPanels = new ArrayList<>();
public final GuiButton saveButton = new GuiButton().setI18nLabel("replaymod.gui.save").setSize(100, 20);
public final GuiButton backButton = new GuiButton().setI18nLabel("replaymod.gui.back").setSize(100, 20);
public final GuiPanel buttonPanel = new GuiPanel(this).setLayout(new HorizontalLayout().setSpacing(5))
.addElements(null, saveButton, backButton);
public GuiReplayEditor(GuiScreen parent, ReplayMod mod) {
this.mod = mod;
backButton.onClick(parent::display);
setTitle(new GuiLabel().setI18nText("replaymod.gui.replayeditor"));
makeTab("trim", () -> new GuiTrimPanel(this));
makeTab("connect", GuiPanel::new).setDisabled(); // Not yet implemented
makeTab("modify", GuiPanel::new).setDisabled(); // Not yet implemented
setLayout(new CustomLayout<GuiScreen>() {
@Override
protected void layout(GuiScreen container, int width, int height) {
// Move all inactive panels aside
tabPanels.forEach(e -> pos(e, Integer.MIN_VALUE, Integer.MIN_VALUE));
pos(warningLabel, 10, 22);
size(warningLabel, width - 20, 10);
pos(tabButtons, 10, y(warningLabel) + height(warningLabel) + 2);
size(tabButtons, width - 20, 20);
pos(buttonPanel, width - 10 - width(buttonPanel), height - 10 - height(buttonPanel));
if (currentTabPanel != null) {
pos(currentTabPanel, 10, y(tabButtons) + height(tabButtons) + 10);
size(currentTabPanel, width - 20, y(buttonPanel) - 10 - y(currentTabPanel));
}
}
});
}
public GuiButton makeTab(String name, Supplier<GuiPanel> panel) {
GuiButton button = new GuiButton().setI18nLabel("replaymod.gui.editor." + name + ".title")
.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.editor." + name + ".description"))
.setSize(Integer.MAX_VALUE, Integer.MAX_VALUE);
button.onClick(() -> {
// Remove previous tab
if (currentTabButton != null) {
currentTabButton.setEnabled();
removeElement(currentTabPanel);
}
// Activate new tab
currentTabButton = button.setDisabled();
addElements(null, currentTabPanel = panel.get());
});
// Add new tab button to tabs panel
tabButtons.addElements(null, button);
((GridLayout) tabButtons.getLayout()).setColumns(tabButtons.getChildren().size());
return button;
}
public void save(File inputFile, PacketStream.FilterInfo...filters) {
save(FilenameUtils.getBaseName(inputFile.getName()), (outputFile) -> {
Studio studio = new ReplayStudio();
File tmpDir = null;
try {
File actualOutputFile = outputFile;
if (outputFile.getCanonicalPath().equals(inputFile.getCanonicalPath())) {
// Input and output files are identical. Due to the way the ZipReplayFile stores its temporary
// data, the same replay file must not be opened twice for writing (tmp files will be deleted once
// either one is closed or, on Windows, will throw an exception when deleted).
tmpDir = Files.createTempDir();
outputFile = new File(tmpDir, "replay.mcpr");
LOGGER.debug("Output file is identical to input file, using temporary output file {} instead",
outputFile);
}
try (ReplayFile outputReplay = new ZipReplayFile(studio, inputFile, outputFile);
ReplayOutputStream out = outputReplay.writePacketData();
ReplayFile inputReplay = new ZipReplayFile(studio, inputFile);
ReplayInputStream in = inputReplay.getPacketData()) {
ReplayMetaData metaData = inputReplay.getMetaData();
PacketStream stream = studio.createReplayStream(in, true);
stream.addFilter(new ProgressFilter(metaData.getDuration()));
for (PacketStream.FilterInfo info : filters) {
stream.addFilter(info.getFilter(), info.getFrom(), info.getTo());
LOGGER.debug("Added filter {}", info);
}
LOGGER.info("Built pipeline: {}", stream);
stream.start();
long lastTimestamp = 0;
PacketData data;
while ((data = stream.next()) != null) {
out.write(data);
lastTimestamp = data.getTime();
}
for (PacketData d : stream.end()) {
out.write(d);
lastTimestamp = d.getTime();
}
updateProgress(ProcessingStage.FINALIZING, 0);
// Update duration of new replay
metaData.setDuration((int) lastTimestamp);
outputReplay.writeMetaData(metaData);
out.close();
outputReplay.save();
}
if (outputFile != actualOutputFile) {
LOGGER.debug("Moving temporary output file {} to {}");
FileUtils.forceDelete(actualOutputFile);
FileUtils.moveFile(outputFile, actualOutputFile);
}
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (tmpDir != null && !FileUtils.deleteQuietly(tmpDir)) {
LOGGER.warn("Failed to delete temporary directory {}", tmpDir);
}
}
});
}
public void save(String sourceName, Consumer<File> runnable) {
GuiTextField nameField = new GuiTextField().setSize(200, 20).setFocused(true).setText(sourceName);
GuiYesNoPopup popup = GuiYesNoPopup.open(this,
nameField
).setYesI18nLabel("replaymod.gui.editor.savemode.override").setNoI18nLabel("replaymod.gui.cancel");
nameField.onEnter(() -> {
if (popup.getYesButton().isEnabled()) {
popup.getYesButton().onClick();
}
}).onTextChanged(obj -> {
popup.getYesButton().setEnabled(!nameField.getText().isEmpty());
popup.setYesI18nLabel("replaymod.gui.editor.savemode." +
(nameField.getText().equals(sourceName) ? "override" : "newfile"));
});
Futures.addCallback(popup.getFuture(), new FutureCallback<Boolean>() {
@Override
public void onSuccess(@Nullable Boolean result) {
if (result == Boolean.TRUE) {
// Sanitize their input
String name = nameField.getText().trim().replace("[^a-zA-Z0-9\\.\\- ]", "_");
// This file is what they want
File replayFolder;
try {
replayFolder = mod.getReplayFolder();
} catch (IOException e) {
Utils.error(LOGGER, GuiReplayEditor.this, CrashReport.makeCrashReport(e, "Getting replay folder"), null);
return;
}
File targetFile = new File(replayFolder, name + ".mcpr");
if (targetFile.exists()) {
LOGGER.trace("Selected file already exists, asking for confirmation");
Futures.addCallback(GuiYesNoPopup.open(GuiReplayEditor.this,
new GuiLabel().setColor(Colors.BLACK)
.setI18nText("replaymod.gui.replaymodified.warning1", name),
new GuiLabel().setColor(Colors.BLACK)
.setI18nText("replaymod.gui.replaymodified.warning2"))
.setYesI18nLabel("gui.yes").setNoI18nLabel("gui.no")
.getFuture(), new FutureCallback<Boolean>() {
@Override
public void onSuccess(@Nullable Boolean result) {
if (result == Boolean.TRUE) {
LOGGER.trace("Saving output in {}", targetFile);
runEdit(() -> runnable.accept(targetFile));
} else {
LOGGER.trace("Not overwriting file {}, reopening naming dialog", targetFile);
save(sourceName, runnable);
}
}
@Override
public void onFailure(Throwable t) {
LOGGER.error("During save file confirmation dialog:", t);
}
});
} else {
LOGGER.trace("Saving output in {}", targetFile);
runEdit(() -> runnable.accept(targetFile));
}
} else {
LOGGER.trace("Saving cancelled");
}
}
@Override
public void onFailure(Throwable t) {
LOGGER.error("During save file dialog:", t);
}
});
}
public void runEdit(Runnable runnable) {
popup = new GuiProcessingProgressPopup(this);
processingStage = ProcessingStage.INITIALIZING;
progress = 0;
new Thread(() -> {
LOGGER.info("Starting editing of replay");
try {
runnable.run();
} catch (Throwable t) {
CrashReport crashReport = CrashReport.makeCrashReport(t, "Editing replay file");
mod.runLater(() -> Utils.error(LOGGER, this, crashReport, popup::close));
return;
}
LOGGER.info("Successfully finished editing");
mod.runLater(() -> {
popup.close();
GuiInfoPopup.open(this, "replaymod.gui.editor.progress.status.finished");
});
}).start();
}
public void updateProgress(ProcessingStage stage, double progress) {
if (this.processingStage != stage) {
LOGGER.trace("Changing state from {} to {}", this.processingStage, stage);
}
this.processingStage = stage;
this.progress = progress;
}
public ReplayMod getMod() {
return mod;
}
public enum ProcessingStage {
INITIALIZING("initializing", 0.01),
WORKING("writing.raw", 0.89),
FINALIZING("writing.final", 0.1),
DONE("finished", 0);
private final String i18nKey;
private final double progressFraction;
ProcessingStage(String i18nKey, double progressFraction) {
this.i18nKey = "replaymod.gui.editor.progress.status." + i18nKey;
this.progressFraction = progressFraction;
}
public double getOverallProgress(double progress) {
progress *= progressFraction;
for (ProcessingStage processingStage : values()) {
if (processingStage == this) break;
progress += processingStage.progressFraction;
}
return progress;
}
}
private class ProgressFilter implements StreamFilter {
private final long total;
public ProgressFilter(long total) {
this.total = total;
}
@Override
public String getName() {
return "progress";
}
@Override
public void init(Studio studio, JsonObject config) {
}
@Override
public void onStart(PacketStream stream) {
updateProgress(ProcessingStage.WORKING, 0);
}
@Override
public boolean onPacket(PacketStream stream, PacketData data) {
updateProgress(ProcessingStage.WORKING, (double) data.getTime() / total);
return true;
}
@Override
public void onEnd(PacketStream stream, long timestamp) {}
}
private class GuiProcessingProgressPopup extends AbstractGuiPopup<GuiProcessingProgressPopup> {
private final GuiProgressBar progressBar = new GuiProgressBar().setSize(300, 20);
{
popup.setLayout(new VerticalLayout().setSpacing(10));
popup.addElements(new VerticalLayout.Data(0.5),
new GuiLabel().setI18nText("replaymod.gui.editor.progress.title").setColor(Colors.BLACK),
new GuiLabel().setI18nText("replaymod.gui.editor.progress.pleasewait").setColor(Colors.BLACK),
progressBar);
}
public GuiProcessingProgressPopup(GuiContainer container) {
super(container);
open();
}
@Override
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
// Update progress bar
progressBar.setI18nLabel(processingStage.i18nKey);
progressBar.setProgress((float) processingStage.getOverallProgress(progress));
super.draw(renderer, size, renderInfo);
}
@Override
public void close() {
super.close();
}
@Override
protected GuiProcessingProgressPopup getThis() {
return this;
}
}
}

View File

@@ -0,0 +1,202 @@
package com.replaymod.editor.gui;
import com.google.gson.JsonObject;
import com.replaymod.core.utils.Utils;
import com.replaymod.replaystudio.data.Marker;
import com.replaymod.replaystudio.filter.ChangeTimestampFilter;
import com.replaymod.replaystudio.filter.RemoveFilter;
import com.replaymod.replaystudio.filter.SquashFilter;
import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ReplayMetaData;
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.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 net.minecraft.client.resources.I18n;
import net.minecraft.crash.CrashReport;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOCase;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.lwjgl.util.Dimension;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import static com.replaymod.editor.ReplayModEditor.LOGGER;
import static java.util.Optional.ofNullable;
public class GuiTrimPanel extends GuiPanel {
private static GuiNumberField newGuiNumberField() {
return new GuiNumberField().setMaxLength(2).setSize(20, 20).setValidateOnFocusChange(true);
}
// Special value indicating no replay files were found
private static final File NO_REPLAY = new File(".");
// Special value for the initial "Select Marker Keyframe" entry
private static final Marker NO_MARKER = new Marker(); static {NO_MARKER.setTime(-1);}
private final GuiReplayEditor gui;
public final GuiDropdownMenu<File> inputReplays = new GuiDropdownMenu<File>(this)
.setMinSize(new Dimension(200, 20)).onSelection(i -> updateSelectedReplay())
.setToString(f -> f == NO_REPLAY ? "" : FilenameUtils.getBaseName(f.getName()));
public final GuiNumberField startHour = newGuiNumberField();
public final GuiNumberField startMin = newGuiNumberField();
public final GuiNumberField startSec = newGuiNumberField();
public final GuiNumberField startMilli = newGuiNumberField().setSize(40, 20).setMaxLength(4);
public final GuiDropdownMenu<Marker> startMarker = new GuiDropdownMenu<>();
public final GuiNumberField endHour = newGuiNumberField();
public final GuiNumberField endMin = newGuiNumberField();
public final GuiNumberField endSec = newGuiNumberField();
public final GuiNumberField endMilli = newGuiNumberField().setSize(40, 20).setMaxLength(4);
public final GuiDropdownMenu<Marker> endMarker = new GuiDropdownMenu<>();
public final GuiPanel timePanel = new GuiPanel(this)
.setLayout(new GridLayout().setCellsEqualSize(false).setSpacingX(20).setSpacingY(5).setColumns(3))
.addElements(new GridLayout.Data(0, 0.5),
new GuiLabel().setI18nText("replaymod.gui.start"),
new GuiPanel().setLayout(new HorizontalLayout().setSpacing(2)).addElements(
new HorizontalLayout.Data(0.5),
startHour, new GuiLabel().setI18nText("replaymod.gui.hours"),
startMin, new GuiLabel().setI18nText("replaymod.gui.minutes"),
startSec, new GuiLabel().setI18nText("replaymod.gui.seconds"),
startMilli, new GuiLabel().setI18nText("replaymod.gui.milliseconds")
), startMarker,
new GuiLabel().setI18nText("replaymod.gui.end"),
new GuiPanel().setLayout(new HorizontalLayout().setSpacing(2)).addElements(
new HorizontalLayout.Data(0.5),
endHour, new GuiLabel().setI18nText("replaymod.gui.hours"),
endMin, new GuiLabel().setI18nText("replaymod.gui.minutes"),
endSec, new GuiLabel().setI18nText("replaymod.gui.seconds"),
endMilli, new GuiLabel().setI18nText("replaymod.gui.milliseconds")
), endMarker);
public GuiTrimPanel(GuiReplayEditor gui) {
this.gui = gui;
setLayout(new VerticalLayout().setSpacing(10));
Function<Marker, String> toString = m -> {
if (m == NO_MARKER) {
return I18n.format("replaymod.gui.editor.trim.marker");
} else {
return ofNullable(m.getName()).orElse(I18n.format("replaymod.gui.ingame.unnamedmarker"))
+ " (" + Utils.convertSecondsToShortString(m.getTime() / 1000) + ")";
}
};
startMarker.setToString(toString).setMinSize(new Dimension(100, 20)).onSelection(i ->
onSelectedMarkerChanged(startMarker, startHour, startMin, startSec, startMilli));
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);
}
updateSelectedReplay();
updateReadyState();
gui.saveButton.onClick(() -> {
// Read start and end times
int start = ((startHour.getInteger() * 60 + startMin.getInteger()) * 60 + startSec.getInteger()) * 1000 + startMilli.getInteger();
int end = ((endHour.getInteger() * 60 + endMin.getInteger()) * 60 + endSec.getInteger()) * 1000 + endMilli.getInteger();
// Configure offset of change timestamp filter
ChangeTimestampFilter ctf = new ChangeTimestampFilter();
JsonObject config = new JsonObject();
config.addProperty("offset", -start);
ctf.init(null, config);
// Pass filters to save dialog
gui.save(inputReplays.getSelectedValue(),
new PacketStream.FilterInfo(new SquashFilter(), -1, start),
new PacketStream.FilterInfo(ctf, start, end),
new PacketStream.FilterInfo(new RemoveFilter(), end, -1)
);
});
}
private void onSelectedMarkerChanged(GuiDropdownMenu<Marker> dropdown, GuiNumberField hour, GuiNumberField min,
GuiNumberField sec, GuiNumberField milli) {
Marker marker = dropdown.getSelectedValue();
if (marker != NO_MARKER) {
int time = marker.getTime();
setTime(time, hour, min, sec, milli);
}
}
private void setTime(int time, GuiNumberField hour, GuiNumberField min,
GuiNumberField sec, GuiNumberField milli) {
milli.setValue(time % 1000); time /= 1000;
sec.setValue(time % 60); time /= 60;
min.setValue(time % 60); time /= 60;
hour.setValue(time);
}
private void updateSelectedReplay() {
File file = inputReplays.getSelectedValue();
// Load markers and meta data from replay file
ReplayMetaData metaData;
Set<Marker> markers;
if (file == NO_REPLAY) {
metaData = new ReplayMetaData();
markers = Collections.emptySet();
} else {
try (ReplayFile replayFile = new ZipReplayFile(new ReplayStudio(), file)) {
metaData = replayFile.getMetaData();
markers = replayFile.getMarkers().or(Collections.emptySet());
LOGGER.debug("Loaded {} markers from replay {}", markers.size(), file);
} catch (Exception e) {
Utils.error(LOGGER, gui, CrashReport.makeCrashReport(e, "Reading markers from replay"), null);
metaData = new ReplayMetaData();
markers = Collections.emptySet();
}
}
// Sort and add initial "Select Event Marker" element
List<Marker> markerList = new ArrayList<>(markers);
markerList.sort(Comparator.comparing(Marker::getTime));
markerList.add(0, NO_MARKER);
// Set marker dropdown values
Marker[] markerArray = markerList.stream().toArray(Marker[]::new);
startMarker.setValues(markerArray).setSelected(0);
endMarker.setValues(markerArray).setSelected(0);
// Set start and end time values
setTime(0, startHour, startMin, startSec, startMilli);
setTime(metaData.getDuration(), endHour, endMin, endSec, endMilli);
}
private void updateReadyState() {
gui.saveButton.setEnabled(inputReplays.getSelectedValue() != NO_REPLAY);
}
}

View File

@@ -0,0 +1,54 @@
package com.replaymod.editor.handler;
import com.replaymod.editor.ReplayModEditor;
import com.replaymod.editor.gui.GuiReplayEditor;
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 net.minecraftforge.client.event.GuiScreenEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import java.util.List;
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() {
FMLCommonHandler.instance().bus().register(this);
MinecraftForge.EVENT_BUS.register(this);
}
@SubscribeEvent
public void injectIntoMainMenu(GuiScreenEvent.InitGuiEvent event) {
if (!(event.gui instanceof GuiMainMenu)) {
return;
}
@SuppressWarnings("unchecked")
List<GuiButton> buttonList = event.buttonList;
GuiButton button = new GuiButton(BUTTON_REPLAY_EDITOR, event.gui.width / 2 + 2,
event.gui.height / 4 + 10 + 3 * 24, I18n.format("replaymod.gui.replayeditor"));
button.width = button.width / 2 - 2;
buttonList.add(button);
}
@SubscribeEvent
public void onButton(GuiScreenEvent.ActionPerformedEvent.Pre event) {
if(!event.button.enabled) return;
if (event.gui instanceof GuiMainMenu) {
if (event.button.id == BUTTON_REPLAY_EDITOR) {
new GuiReplayEditor(GuiScreen.wrap(event.gui), mod.getCore()).display();
}
}
}
}

View File

@@ -9,15 +9,13 @@ import de.johni0702.minecraft.gui.element.GuiImage;
import de.johni0702.minecraft.gui.element.IGuiImage; import de.johni0702.minecraft.gui.element.IGuiImage;
import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import net.minecraft.client.settings.GameSettings; import net.minecraft.client.settings.GameSettings;
import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent; import net.minecraftforge.fml.common.gameevent.TickEvent;
import org.lwjgl.input.Keyboard; import org.lwjgl.input.Keyboard;
public class FullBrightness implements Extra { public class FullBrightness implements Extra {
@Mod.Instance(ReplayModReplay.MOD_ID) private ReplayModReplay module;
private static ReplayModReplay module;
private final IGuiImage indicator = new GuiImage().setTexture(ReplayMod.TEXTURE, 90, 20, 19, 13).setSize(19, 13); private final IGuiImage indicator = new GuiImage().setTexture(ReplayMod.TEXTURE, 90, 20, 19, 13).setSize(19, 13);
@@ -27,6 +25,7 @@ public class FullBrightness implements Extra {
@Override @Override
public void register(final ReplayMod mod) throws Exception { public void register(final ReplayMod mod) throws Exception {
this.module = ReplayModReplay.instance;
this.gameSettings = mod.getMinecraft().gameSettings; this.gameSettings = mod.getMinecraft().gameSettings;
mod.getKeyBindingRegistry().registerKeyBinding("replaymod.input.lighting", Keyboard.KEY_Z, new Runnable() { mod.getKeyBindingRegistry().registerKeyBinding("replaymod.input.lighting", Keyboard.KEY_Z, new Runnable() {
@@ -41,7 +40,7 @@ public class FullBrightness implements Extra {
} }
}); });
MinecraftForge.EVENT_BUS.register(this); FMLCommonHandler.instance().bus().register(this);
} }
@SubscribeEvent @SubscribeEvent

View File

@@ -17,7 +17,7 @@ import de.johni0702.minecraft.gui.layout.GridLayout;
import de.johni0702.minecraft.gui.layout.HorizontalLayout; import de.johni0702.minecraft.gui.layout.HorizontalLayout;
import de.johni0702.minecraft.gui.layout.LayoutData; import de.johni0702.minecraft.gui.layout.LayoutData;
import net.minecraft.client.resources.I18n; import net.minecraft.client.resources.I18n;
import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import org.lwjgl.input.Keyboard; import org.lwjgl.input.Keyboard;
import org.lwjgl.util.Dimension; import org.lwjgl.util.Dimension;
@@ -35,7 +35,7 @@ public class HotkeyButtons implements Extra {
public void register(ReplayMod mod) throws Exception { public void register(ReplayMod mod) throws Exception {
this.mod = mod; this.mod = mod;
MinecraftForge.EVENT_BUS.register(this); FMLCommonHandler.instance().bus().register(this);
} }
@SubscribeEvent @SubscribeEvent

View File

@@ -1,106 +1,110 @@
package com.replaymod.extras; package com.replaymod.extras;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.replaymod.core.ReplayMod; import com.replaymod.core.ReplayMod;
import com.replaymod.online.ReplayModOnline;
import com.replaymod.online.api.ApiClient;
import com.replaymod.online.api.ApiException;
import lombok.RequiredArgsConstructor;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.IResourcePack; import net.minecraft.client.resources.IResourcePack;
import net.minecraft.client.resources.data.IMetadataSection; import net.minecraft.client.resources.data.IMetadataSection;
import net.minecraft.client.resources.data.MetadataSerializer; import net.minecraft.client.resources.data.IMetadataSerializer;
import net.minecraft.util.ResourceLocation; import net.minecraft.util.ResourceLocation;
import net.minecraftforge.fml.common.Mod; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.ConnectException; import java.net.URL;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; 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 { public class LocalizationExtra implements Extra {
@Mod.Instance(ReplayModOnline.MOD_ID) private static final String ZIP_FILE_URL = "https://github.com/ReplayMod/Translations/archive/master.zip";
private static ReplayModOnline module; private static final String LANG_PREFIX = "Translations-master/";
@Override @Override
public void register(ReplayMod mod) throws Exception { public void register(ReplayMod mod) throws Exception {
final Minecraft mc = mod.getMinecraft(); final Minecraft mc = mod.getMinecraft();
Thread localizedResourcePackLoader = new Thread(new Runnable() { if (Boolean.parseBoolean(System.getProperty("replaymod.offline", "false"))) {
@Override return;
public void run() { }
try { Thread localizedResourcePackLoader = new Thread(() -> {
try {
// Download zip of lang files
LOGGER.debug("Downloading languages from {}", ZIP_FILE_URL);
Map<String, byte[]> 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") @SuppressWarnings("unchecked")
List<IResourcePack> defaultResourcePacks = mc.defaultResourcePacks; List<IResourcePack> defaultResourcePacks = mc.defaultResourcePacks;
defaultResourcePacks.add(new LocalizedResourcePack(module.getApiClient())); defaultResourcePacks.add(new LocalizedResourcePack(languages));
mc.addScheduledTask(new Runnable() { mc.getLanguageManager().onResourceManagerReload(mc.getResourceManager());
@Override LOGGER.debug("Added language files to resource packs and reloaded LanguageManager");
public void run() { });
mc.refreshResources(); } catch (Throwable t) {
} LOGGER.error("Loading localized resource pack:", t);
});
} catch(Exception e) {
e.printStackTrace();
}
} }
}, "localizedResourcePackLoader"); }, "localizedResourcePackLoader");
localizedResourcePackLoader.setDaemon(true);
localizedResourcePackLoader.start(); localizedResourcePackLoader.start();
} }
@RequiredArgsConstructor
public static class LocalizedResourcePack implements IResourcePack { public static class LocalizedResourcePack implements IResourcePack {
private final ApiClient apiClient; private final Pattern LANG_PATTERN = Pattern.compile("^lang/([.+].lang)$");
private Map<String, String> availableLanguages = new HashMap<>(); private final Map<String, byte[]> languages;
private boolean websiteAvailable = true;
public LocalizedResourcePack(Map<String, byte[]> languages) {
this.languages = languages;
}
@Override @Override
public InputStream getInputStream(ResourceLocation loc) { public InputStream getInputStream(ResourceLocation loc) {
if(!loc.getResourcePath().endsWith(".lang")) return null; if (!"replaymod".equals(loc.getResourceDomain())) return null;
String langcode = loc.getResourcePath().split("/")[1].split("\\.")[0]; Matcher matcher = LANG_PATTERN.matcher(loc.getResourcePath());
if(availableLanguages.containsKey(langcode)) return new ByteArrayInputStream(availableLanguages.get(langcode).getBytes(Charsets.UTF_8)); if (matcher.matches()) {
byte[] bytes = languages.get(matcher.group());
if (bytes != null) {
return new ByteArrayInputStream(bytes);
}
}
return null; return null;
} }
@Override @Override
public boolean resourceExists(ResourceLocation loc) { public boolean resourceExists(ResourceLocation loc) {
if(!(loc.getResourcePath().endsWith(".lang"))) return false; // Assumes that getInputStream returns a ByteArrayInputStream that doesn't need to be closed
String langcode = loc.getResourcePath().split("/")[1].split("\\.")[0]; return getInputStream(loc) != null;
if(availableLanguages.containsKey(langcode)) return true;
if(!websiteAvailable) return false;
try {
if (Boolean.parseBoolean(System.getProperty("replaymod.offline", "false"))) {
return false;
}
String lang = apiClient.getTranslation(langcode);
String prop = StringEscapeUtils.unescapeHtml4(lang);
availableLanguages.put(langcode, prop);
return true;
} catch (ApiException e) {
if (e.getError().getId() != 16) { // This language has not been translated
e.printStackTrace();
}
} catch(ConnectException ce) {
websiteAvailable = false;
ce.printStackTrace();
} catch(Exception e) {
e.printStackTrace();
}
return false;
} }
@Override @Override
public Set<String> getResourceDomains() { public Set getResourceDomains() {
return ImmutableSet.of("minecraft", "replaymod"); return ImmutableSet.of("replaymod");
} }
@Override @Override
public <T extends IMetadataSection> T getPackMetadata(MetadataSerializer metadataSerializer, String metadataSectionName) throws IOException { public IMetadataSection getPackMetadata(IMetadataSerializer p_135058_1_, String p_135058_2_) throws IOException {
return null; return null;
} }

View File

@@ -17,6 +17,7 @@ import de.johni0702.minecraft.gui.utils.Colors;
import net.minecraftforge.fml.common.Loader; import net.minecraftforge.fml.common.Loader;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import javax.net.ssl.HttpsURLConnection;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.net.URL; import java.net.URL;
@@ -24,8 +25,10 @@ import java.nio.channels.Channels;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel; import java.nio.channels.ReadableByteChannel;
import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY;
public class OpenEyeExtra implements Extra { public class OpenEyeExtra implements Extra {
private static final String DOWNLOAD_URL = "http://www.replaymod.com/dl/openeye/" + Loader.MC_VERSION; private static final String DOWNLOAD_URL = "https://www.replaymod.com/dl/openeye/" + Loader.MC_VERSION;
private static final Setting<Boolean> ASK_FOR_OPEN_EYE = new Setting<>("advanced", "askForOpenEye", null, true); private static final Setting<Boolean> ASK_FOR_OPEN_EYE = new Setting<>("advanced", "askForOpenEye", null, true);
private ReplayMod mod; private ReplayMod mod;
@@ -40,7 +43,7 @@ public class OpenEyeExtra implements Extra {
} }
} }
private class OfferGui extends AbstractGuiScreen<OfferGui> { public class OfferGui extends AbstractGuiScreen<OfferGui> {
public final GuiScreen parent; public final GuiScreen parent;
public final GuiPanel textPanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(3)) public final GuiPanel textPanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(3))
.addElements(new VerticalLayout.Data(0.5), .addElements(new VerticalLayout.Data(0.5),
@@ -66,10 +69,12 @@ public class OpenEyeExtra implements Extra {
GuiPopup popup = new GuiPopup(OfferGui.this); GuiPopup popup = new GuiPopup(OfferGui.this);
new Thread(() -> { new Thread(() -> {
try { try {
File targetFile = new File("mods/" + Loader.MC_VERSION, "OpenEye.jar"); File targetFile = new File(mod.getMinecraft().mcDataDir, "mods/" + Loader.MC_VERSION + "/OpenEye.jar");
FileUtils.forceMkdir(targetFile.getParentFile()); FileUtils.forceMkdir(targetFile.getParentFile());
ReadableByteChannel in = Channels.newChannel(new URL(DOWNLOAD_URL).openStream()); HttpsURLConnection connection = (HttpsURLConnection) new URL(DOWNLOAD_URL).openConnection();
connection.setSSLSocketFactory(SSL_SOCKET_FACTORY);
ReadableByteChannel in = Channels.newChannel(connection.getInputStream());
FileChannel out = new FileOutputStream(targetFile).getChannel(); FileChannel out = new FileOutputStream(targetFile).getChannel();
out.transferFrom(in, 0, Long.MAX_VALUE); out.transferFrom(in, 0, Long.MAX_VALUE);
} catch (Throwable e) { } catch (Throwable e) {
@@ -102,7 +107,7 @@ public class OpenEyeExtra implements Extra {
} }
} }
private static final class GuiPopup extends AbstractGuiPopup<GuiPopup> { public static final class GuiPopup extends AbstractGuiPopup<GuiPopup> {
GuiPopup(GuiContainer container) { GuiPopup(GuiContainer container) {
super(container); super(container);
popup.addElements(null, new GuiIndicator().setColor(Colors.BLACK)); popup.addElements(null, new GuiIndicator().setColor(Colors.BLACK));

View File

@@ -12,16 +12,16 @@ import org.apache.logging.log4j.Logger;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@Mod(modid = ReplayModExtras.MOD_ID, useMetadata = true) @Mod(modid = ReplayModExtras.MOD_ID,
version = "@MOD_VERSION@",
acceptedMinecraftVersions = "@MC_VERSION@",
useMetadata = true)
public class ReplayModExtras { public class ReplayModExtras {
public static final String MOD_ID = "replaymod-extras"; public static final String MOD_ID = "replaymod-extras";
@Mod.Instance(MOD_ID) @Mod.Instance(MOD_ID)
public static ReplayModExtras instance; public static ReplayModExtras instance;
@Mod.Instance(ReplayMod.MOD_ID)
private static ReplayMod core;
private static final List<Class<? extends Extra>> builtin = Arrays.asList( private static final List<Class<? extends Extra>> builtin = Arrays.asList(
PlayerOverview.class, PlayerOverview.class,
UriSchemeExtra.class, UriSchemeExtra.class,
@@ -32,11 +32,11 @@ public class ReplayModExtras {
OpenEyeExtra.class OpenEyeExtra.class
); );
private Logger logger; public static Logger LOGGER;
@Mod.EventHandler @Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) { public void preInit(FMLPreInitializationEvent event) {
logger = event.getModLog(); LOGGER = event.getModLog();
} }
@Mod.EventHandler @Mod.EventHandler
@@ -44,9 +44,9 @@ public class ReplayModExtras {
for (Class<? extends Extra> cls : builtin) { for (Class<? extends Extra> cls : builtin) {
try { try {
Extra extra = cls.newInstance(); Extra extra = cls.newInstance();
extra.register(core); extra.register(ReplayMod.instance);
} catch (Throwable t) { } catch (Throwable t) {
logger.warn("Failed to load extra " + cls.getName() + ": ", t); LOGGER.warn("Failed to load extra " + cls.getName() + ": ", t);
} }
} }
} }

View File

@@ -11,9 +11,11 @@ import com.replaymod.replay.events.ReplayCloseEvent;
import com.replaymod.replay.events.ReplayOpenEvent; import com.replaymod.replay.events.ReplayOpenEvent;
import net.minecraft.client.renderer.entity.RenderManager; import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.entity.RenderPlayer; import net.minecraft.client.renderer.entity.RenderPlayer;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayer;
import net.minecraftforge.client.event.RenderHandEvent;
import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import org.lwjgl.input.Keyboard; import org.lwjgl.input.Keyboard;
@@ -21,14 +23,15 @@ import java.io.IOException;
import java.util.*; import java.util.*;
public class PlayerOverview implements Extra { public class PlayerOverview implements Extra {
@Mod.Instance(ReplayModReplay.MOD_ID) private ReplayModReplay module;
private static ReplayModReplay module;
private final Set<UUID> hiddenPlayers = new HashSet<>(); private final Set<UUID> hiddenPlayers = new HashSet<>();
private boolean savingEnabled; private boolean savingEnabled;
@Override @Override
public void register(final ReplayMod mod) throws Exception { public void register(final ReplayMod mod) throws Exception {
this.module = ReplayModReplay.instance;
mod.getKeyBindingRegistry().registerKeyBinding("replaymod.input.playeroverview", Keyboard.KEY_B, new Runnable() { mod.getKeyBindingRegistry().registerKeyBinding("replaymod.input.playeroverview", Keyboard.KEY_B, new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -55,6 +58,7 @@ public class PlayerOverview implements Extra {
} }
}); });
FMLCommonHandler.instance().bus().register(this);
MinecraftForge.EVENT_BUS.register(this); MinecraftForge.EVENT_BUS.register(this);
RenderManager renderManager = mod.getMinecraft().getRenderManager(); RenderManager renderManager = mod.getMinecraft().getRenderManager();
@@ -92,6 +96,14 @@ public class PlayerOverview implements Extra {
hiddenPlayers.clear(); hiddenPlayers.clear();
} }
@SubscribeEvent
public void oRenderHand(RenderHandEvent event) {
Entity view = module.getCore().getMinecraft().getRenderViewEntity();
if (view != null && isHidden(view.getUniqueID())) {
event.setCanceled(true);
}
}
public boolean isSavingEnabled() { public boolean isSavingEnabled() {
return savingEnabled; return savingEnabled;
} }

View File

@@ -4,10 +4,15 @@ import com.replaymod.core.utils.Utils;
import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.ReplayModReplay;
import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.*; import de.johni0702.minecraft.gui.container.GuiClickable;
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.container.GuiVerticalList;
import de.johni0702.minecraft.gui.element.GuiCheckbox; import de.johni0702.minecraft.gui.element.GuiCheckbox;
import de.johni0702.minecraft.gui.element.GuiImage; import de.johni0702.minecraft.gui.element.GuiImage;
import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.element.IGuiCheckbox; import de.johni0702.minecraft.gui.element.IGuiCheckbox;
import de.johni0702.minecraft.gui.function.Closeable; import de.johni0702.minecraft.gui.function.Closeable;
import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.CustomLayout;
@@ -16,8 +21,7 @@ import de.johni0702.minecraft.gui.utils.Colors;
import net.minecraft.client.audio.PositionedSoundRecord; import net.minecraft.client.audio.PositionedSoundRecord;
import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EnumPlayerModelParts; import net.minecraft.entity.player.EnumPlayerModelParts;
import net.minecraft.init.MobEffects; import net.minecraft.potion.Potion;
import net.minecraft.init.SoundEvents;
import net.minecraft.util.ResourceLocation; import net.minecraft.util.ResourceLocation;
import org.lwjgl.util.Dimension; import org.lwjgl.util.Dimension;
import org.lwjgl.util.ReadableDimension; import org.lwjgl.util.ReadableDimension;
@@ -37,21 +41,22 @@ public class PlayerOverviewGui extends GuiScreen implements Closeable {
public final GuiVerticalList playersScrollable = new GuiVerticalList(contentPanel) public final GuiVerticalList playersScrollable = new GuiVerticalList(contentPanel)
.setDrawSlider(true).setDrawShadow(true); .setDrawSlider(true).setDrawShadow(true);
public final GuiCheckbox saveCheckbox = new GuiCheckbox(contentPanel) public final GuiCheckbox saveCheckbox = new GuiCheckbox(contentPanel)
.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.playeroverview.remembersettings.description"))
.setI18nLabel("replaymod.gui.playeroverview.remembersettings"); .setI18nLabel("replaymod.gui.playeroverview.remembersettings");
public final GuiCheckbox checkAll = new GuiCheckbox(contentPanel){ public final GuiCheckbox checkAll = new GuiCheckbox(contentPanel){
@Override @Override
public void onClick() { public void onClick() {
getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(SoundEvents.UI_BUTTON_CLICK, 1.0F)); getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.create(BUTTON_SOUND, 1.0F));
playersScrollable.forEach(IGuiCheckbox.class).setChecked(true); playersScrollable.forEach(IGuiCheckbox.class).setChecked(true);
} }
}.setLabel("").setChecked(true); }.setLabel("").setChecked(true).setTooltip(new GuiTooltip().setI18nText("replaymod.gui.playeroverview.showall"));
public final GuiCheckbox uncheckAll = new GuiCheckbox(contentPanel){ public final GuiCheckbox uncheckAll = new GuiCheckbox(contentPanel){
@Override @Override
public void onClick() { public void onClick() {
getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(SoundEvents.UI_BUTTON_CLICK, 1.0F)); getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.create(BUTTON_SOUND, 1.0F));
playersScrollable.forEach(IGuiCheckbox.class).setChecked(false); playersScrollable.forEach(IGuiCheckbox.class).setChecked(false);
} }
}.setLabel("").setChecked(false); }.setLabel("").setChecked(false).setTooltip(new GuiTooltip().setI18nText("replaymod.gui.playeroverview.hideall"));
{ {
setBackground(Background.NONE); setBackground(Background.NONE);
@@ -91,7 +96,7 @@ public class PlayerOverviewGui extends GuiScreen implements Closeable {
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) { public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
renderer.bindTexture(texture); renderer.bindTexture(texture);
renderer.drawTexturedRect(0, 0, 8, 8, 16, 16, 8, 8, 64, 64); renderer.drawTexturedRect(0, 0, 8, 8, 16, 16, 8, 8, 64, 64);
if (p.isWearing(EnumPlayerModelParts.HAT)) { if (p.func_175148_a(EnumPlayerModelParts.HAT)) {
renderer.drawTexturedRect(0, 0, 40, 8, size.getWidth(), size.getHeight(), 8, 8, 64, 64); renderer.drawTexturedRect(0, 0, 40, 8, size.getWidth(), size.getHeight(), 8, 8, 64, 64);
} }
} }
@@ -140,7 +145,7 @@ public class PlayerOverviewGui extends GuiScreen implements Closeable {
} }
private static boolean isSpectator(EntityPlayer e) { private static boolean isSpectator(EntityPlayer e) {
return e.isInvisible() && e.getActivePotionEffect(MobEffects.INVISIBILITY) == null; return e.isInvisible() && e.getActivePotionEffect(Potion.invisibility) == null;
} }
private static final class PlayerComparator implements Comparator<EntityPlayer> { private static final class PlayerComparator implements Comparator<EntityPlayer> {

View File

@@ -1,9 +1,9 @@
package com.replaymod.extras.playeroverview; package com.replaymod.extras.playeroverview;
import net.minecraft.client.entity.AbstractClientPlayer;
import net.minecraft.client.renderer.culling.ICamera; import net.minecraft.client.renderer.culling.ICamera;
import net.minecraft.client.renderer.entity.RenderManager; import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.client.renderer.entity.RenderPlayer; import net.minecraft.client.renderer.entity.RenderPlayer;
import net.minecraft.entity.Entity;
public class PlayerRenderHook extends RenderPlayer { public class PlayerRenderHook extends RenderPlayer {
private final PlayerOverview extra; private final PlayerOverview extra;
@@ -14,7 +14,7 @@ public class PlayerRenderHook extends RenderPlayer {
} }
@Override @Override
public boolean shouldRender(AbstractClientPlayer entity, ICamera camera, double camX, double camY, double camZ) { public boolean shouldRender(Entity entity, ICamera camera, double camX, double camY, double camZ) {
return !extra.isHidden(entity.getUniqueID()) && super.shouldRender(entity, camera, camX, camY, camZ); return !extra.isHidden(entity.getUniqueID()) && super.shouldRender(entity, camera, camX, camY, camZ);
} }
} }

View File

@@ -4,7 +4,6 @@ import com.replaymod.core.ReplayMod;
import com.replaymod.extras.Extra; import com.replaymod.extras.Extra;
import com.replaymod.online.ReplayModOnline; import com.replaymod.online.ReplayModOnline;
import de.johni0702.minecraft.gui.container.GuiScreen; import de.johni0702.minecraft.gui.container.GuiScreen;
import net.minecraftforge.fml.common.Mod;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import java.io.IOException; import java.io.IOException;
@@ -13,14 +12,14 @@ import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
public class UriSchemeExtra implements Extra { public class UriSchemeExtra implements Extra {
@Mod.Instance(ReplayModOnline.MOD_ID) private ReplayModOnline module;
private static ReplayModOnline module;
private ReplayMod mod; private ReplayMod mod;
@Override @Override
public void register(final ReplayMod mod) throws Exception { public void register(final ReplayMod mod) throws Exception {
this.mod = mod; this.mod = mod;
this.module = ReplayModOnline.instance;
UriScheme uriScheme = UriScheme.create(); UriScheme uriScheme = UriScheme.create();
if (uriScheme == null) { if (uriScheme == null) {

View File

@@ -17,8 +17,8 @@ public class YoutubeUpload implements Extra {
@SubscribeEvent @SubscribeEvent
public void onGuiOpen(GuiScreenEvent.InitGuiEvent.Post event) { public void onGuiOpen(GuiScreenEvent.InitGuiEvent.Post event) {
if (GuiScreen.from(event.getGui()) instanceof GuiRenderingDone) { if (GuiScreen.from(event.gui) instanceof GuiRenderingDone) {
GuiRenderingDone gui = (GuiRenderingDone) GuiScreen.from(event.getGui()); GuiRenderingDone gui = (GuiRenderingDone) GuiScreen.from(event.gui);
// Check if there already is a youtube button // Check if there already is a youtube button
if (gui.actionsPanel.getChildren().stream().anyMatch(it -> it instanceof YoutubeButton)) { if (gui.actionsPanel.getChildren().stream().anyMatch(it -> it instanceof YoutubeButton)) {
return; // Button already added return; // Button already added

View File

@@ -12,8 +12,8 @@ import com.replaymod.replaystudio.replay.ReplayFile;
import com.replaymod.replaystudio.replay.ZipReplayFile; import com.replaymod.replaystudio.replay.ZipReplayFile;
import com.replaymod.replaystudio.studio.ReplayStudio; import com.replaymod.replaystudio.studio.ReplayStudio;
import de.johni0702.minecraft.gui.container.GuiScreen; import de.johni0702.minecraft.gui.container.GuiScreen;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.config.Configuration; import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent; import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPostInitializationEvent; import net.minecraftforge.fml.common.event.FMLPostInitializationEvent;
@@ -26,18 +26,19 @@ import java.io.IOException;
import static net.minecraft.client.Minecraft.getMinecraft; import static net.minecraft.client.Minecraft.getMinecraft;
@Mod(modid = ReplayModOnline.MOD_ID, useMetadata = true) @Mod(modid = ReplayModOnline.MOD_ID,
version = "@MOD_VERSION@",
acceptedMinecraftVersions = "@MC_VERSION@",
useMetadata = true)
public class ReplayModOnline { public class ReplayModOnline {
public static final String MOD_ID = "replaymod-online"; public static final String MOD_ID = "replaymod-online";
@Mod.Instance(MOD_ID) @Mod.Instance(MOD_ID)
public static ReplayModOnline instance; public static ReplayModOnline instance;
@Mod.Instance(ReplayMod.MOD_ID) private ReplayMod core;
private static ReplayMod core;
@Mod.Instance(ReplayModReplay.MOD_ID) private ReplayModReplay replayModule;
private static ReplayModReplay replayModule;
private Logger logger; private Logger logger;
@@ -53,6 +54,8 @@ public class ReplayModOnline {
@Mod.EventHandler @Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) { public void preInit(FMLPreInitializationEvent event) {
logger = event.getModLog(); logger = event.getModLog();
core = ReplayMod.instance;
replayModule = ReplayModReplay.instance;
core.getSettingsRegistry().register(Setting.class); core.getSettingsRegistry().register(Setting.class);
@@ -64,12 +67,14 @@ public class ReplayModOnline {
@Mod.EventHandler @Mod.EventHandler
public void init(FMLInitializationEvent event) { public void init(FMLInitializationEvent event) {
if (!getDownloadsFolder().mkdirs()) { if (!getDownloadsFolder().exists()){
logger.warn("Failed to create downloads folder: " + getDownloadsFolder()); if (!getDownloadsFolder().mkdirs()) {
logger.warn("Failed to create downloads folder: " + getDownloadsFolder());
}
} }
new GuiHandler(this).register(); new GuiHandler(this).register();
MinecraftForge.EVENT_BUS.register(this); FMLCommonHandler.instance().bus().register(this);
} }
@Mod.EventHandler @Mod.EventHandler
@@ -77,7 +82,7 @@ public class ReplayModOnline {
// Initial login prompt // Initial login prompt
if (!core.getSettingsRegistry().get(Setting.SKIP_LOGIN_PROMPT)) { if (!core.getSettingsRegistry().get(Setting.SKIP_LOGIN_PROMPT)) {
if (!isLoggedIn()) { if (!isLoggedIn()) {
new GuiLoginPrompt(apiClient, null, null, false).display(); core.runLater(() -> new GuiLoginPrompt(apiClient, GuiScreen.wrap(getMinecraft().currentScreen), null, false).display());
} }
} }
} }

View File

@@ -15,13 +15,19 @@ import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.net.ssl.HttpsURLConnection;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.*; import java.io.BufferedInputStream;
import java.net.HttpURLConnection; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY;
public class ApiClient { public class ApiClient {
private static final Minecraft mc = Minecraft.getMinecraft(); private static final Minecraft mc = Minecraft.getMinecraft();
@@ -130,7 +136,11 @@ public class ApiClient {
QueryBuilder builder = new QueryBuilder(ReplayModApiMethods.get_thumbnail); QueryBuilder builder = new QueryBuilder(ReplayModApiMethods.get_thumbnail);
builder.put("id", file); builder.put("id", file);
URL url = new URL(builder.toString()); URL url = new URL(builder.toString());
return ImageIO.read(url); HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
connection.setSSLSocketFactory(SSL_SOCKET_FACTORY);
try (InputStream in = connection.getInputStream()) {
return ImageIO.read(in);
}
} }
private boolean cancelDownload = false; private boolean cancelDownload = false;
@@ -143,7 +153,8 @@ public class ApiClient {
builder.put("id", file); builder.put("id", file);
String url = builder.toString(); String url = builder.toString();
URL website = new URL(url); URL website = new URL(url);
HttpURLConnection con = (HttpURLConnection) website.openConnection(); HttpsURLConnection con = (HttpsURLConnection) website.openConnection();
con.setSSLSocketFactory(SSL_SOCKET_FACTORY);
int fileSize = con.getContentLength(); int fileSize = con.getContentLength();

View File

@@ -7,12 +7,14 @@ import com.google.gson.JsonParser;
import com.replaymod.online.api.replay.holders.ApiError; import com.replaymod.online.api.replay.holders.ApiError;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import javax.net.ssl.HttpsURLConnection;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.util.Map; import java.util.Map;
import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY;
public class SimpleApiClient { public class SimpleApiClient {
private static final JsonParser jsonParser = new JsonParser(); private static final JsonParser jsonParser = new JsonParser();
@@ -72,10 +74,11 @@ public class SimpleApiClient {
// read response // read response
String responseContent = null; String responseContent = null;
InputStream is = null; InputStream is = null;
HttpURLConnection httpUrlConnection = null; HttpsURLConnection httpUrlConnection = null;
try { try {
URL url = new URL(urlString); URL url = new URL(urlString);
httpUrlConnection = (HttpURLConnection) url.openConnection(); httpUrlConnection = (HttpsURLConnection) url.openConnection();
httpUrlConnection.setSSLSocketFactory(SSL_SOCKET_FACTORY);
httpUrlConnection.setRequestMethod("GET"); httpUrlConnection.setRequestMethod("GET");

View File

@@ -9,16 +9,18 @@ import lombok.RequiredArgsConstructor;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import javax.net.ssl.HttpsURLConnection;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.InputStream; import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import static com.replaymod.core.utils.Utils.SSL_SOCKET_FACTORY;
@RequiredArgsConstructor @RequiredArgsConstructor
public class FileUploader { public class FileUploader {
private static final Gson gson = new Gson(); private static final Gson gson = new Gson();
@@ -48,7 +50,8 @@ public class FileUploader {
postData += "&name=" + URLEncoder.encode(filename, "UTF-8"); postData += "&name=" + URLEncoder.encode(filename, "UTF-8");
String url = ReplayModApiMethods.upload_file + postData; String url = ReplayModApiMethods.upload_file + postData;
HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); HttpsURLConnection con = (HttpsURLConnection) new URL(url).openConnection();
con.setSSLSocketFactory(SSL_SOCKET_FACTORY);
con.setUseCaches(false); con.setUseCaches(false);
con.setDoOutput(true); con.setDoOutput(true);
con.setRequestMethod("POST"); con.setRequestMethod("POST");

View File

@@ -2,7 +2,7 @@ package com.replaymod.online.api.replay;
public class ReplayModApiMethods { public class ReplayModApiMethods {
public static final String REPLAYMOD_BASE_URL = "http://ReplayMod.com/api/"; public static final String REPLAYMOD_BASE_URL = "https://ReplayMod.com/api/";
public static final String register = REPLAYMOD_BASE_URL+"register"; public static final String register = REPLAYMOD_BASE_URL+"register";
public static final String check_authkey = REPLAYMOD_BASE_URL+"check_authkey"; public static final String check_authkey = REPLAYMOD_BASE_URL+"check_authkey";

View File

@@ -2,8 +2,7 @@ package com.replaymod.online.api.replay.holders;
public enum MinecraftVersion { public enum MinecraftVersion {
MC_1_8("Minecraft 1.8", "1.8"), MC_1_8("Minecraft 1.8", "1.8");
MC_1_9_4("Minecraft 1.9.4", "1.9.4");
private String niceName, apiName; private String niceName, apiName;

View File

@@ -21,7 +21,7 @@ public class GuiLoginPrompt extends AbstractGuiScreen<GuiLoginPrompt> {
private GuiLabel noAccountLabel = new GuiLabel(this).setI18nText("replaymod.gui.login.noacc"); private GuiLabel noAccountLabel = new GuiLabel(this).setI18nText("replaymod.gui.login.noacc");
private GuiLabel statusLabel = new GuiLabel(this); private GuiLabel statusLabel = new GuiLabel(this);
private GuiButton loginButton = new GuiButton(this).setI18nLabel("replaymod.gui.login").setSize(150, 20).setEnabled(false); private GuiButton loginButton = new GuiButton(this).setI18nLabel("replaymod.gui.login").setSize(150, 20).setEnabled(false);
private GuiButton cancelButton = new GuiButton(this).setI18nLabel("replaymod.gui.cancel").setSize(150, 20); public GuiButton cancelButton = new GuiButton(this).setI18nLabel("replaymod.gui.cancel").setSize(150, 20);
private GuiButton registerButton = new GuiButton(this).setI18nLabel("replaymod.gui.register").setSize(150, 20); private GuiButton registerButton = new GuiButton(this).setI18nLabel("replaymod.gui.register").setSize(150, 20);
private GuiTextField username = new GuiTextField(this).setSize(145, 20).setMaxLength(16).setFocused(true); private GuiTextField username = new GuiTextField(this).setSize(145, 20).setMaxLength(16).setFocused(true);
private GuiPasswordField password = new GuiPasswordField(this).setSize(145, 20) private GuiPasswordField password = new GuiPasswordField(this).setSize(145, 20)

View File

@@ -28,6 +28,7 @@ import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiImage; import de.johni0702.minecraft.gui.element.GuiImage;
import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.element.IGuiButton; import de.johni0702.minecraft.gui.element.IGuiButton;
import de.johni0702.minecraft.gui.element.advanced.GuiResourceLoadingList; import de.johni0702.minecraft.gui.element.advanced.GuiResourceLoadingList;
import de.johni0702.minecraft.gui.layout.CustomLayout; import de.johni0702.minecraft.gui.layout.CustomLayout;
@@ -109,6 +110,7 @@ public class GuiReplayCenter extends GuiScreen {
public void run() { public void run() {
GuiReplayEntry selected = list.getSelected(); GuiReplayEntry selected = list.getSelected();
replayButtonPanel.forEach(IGuiButton.class).setEnabled(selected != null); replayButtonPanel.forEach(IGuiButton.class).setEnabled(selected != null);
replayButtonPanel.forEach(IGuiButton.class).setTooltip(null);
if (selected != null) { if (selected != null) {
int replayId = selected.fileInfo.getId(); int replayId = selected.fileInfo.getId();
boolean favorited = favoritedReplays.contains(replayId); boolean favorited = favoritedReplays.contains(replayId);
@@ -123,10 +125,22 @@ public class GuiReplayCenter extends GuiScreen {
favoriteButton.setI18nLabel("replaymod.gui.center." + (favorited ? "unfavorite" : "favorite")); favoriteButton.setI18nLabel("replaymod.gui.center." + (favorited ? "unfavorite" : "favorite"));
// Only allow button usage for either unfavorite or favorite after they've actually downloaded it // Only allow button usage for either unfavorite or favorite after they've actually downloaded it
favoriteButton.setEnabled(favorited || selected.downloaded); favoriteButton.setEnabled(favorited || selected.downloaded);
if (favoriteButton.isEnabled()) {
favoriteButton.setTooltip(null);
} else {
favoriteButton.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.center.downloadrequired"));
}
// Similar for like/dislike buttons // Similar for like/dislike buttons
likeButton.setEnabled(selected.downloaded); likeButton.setEnabled(selected.downloaded);
dislikeButton.setEnabled(selected.downloaded); dislikeButton.setEnabled(selected.downloaded);
if (likeButton.isEnabled()) {
likeButton.setTooltip(null);
dislikeButton.setTooltip(null);
} else {
likeButton.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.center.downloadrequired"));
dislikeButton.setTooltip(new GuiTooltip().setI18nText("replaymod.gui.center.downloadrequired"));
}
likeButton.setI18nLabel("replaymod.gui." + (liked ? "removelike" : "like")); likeButton.setI18nLabel("replaymod.gui." + (liked ? "removelike" : "like"));
dislikeButton.setI18nLabel("replaymod.gui." + (disliked ? "removedislike" : "dislike")); dislikeButton.setI18nLabel("replaymod.gui." + (disliked ? "removedislike" : "dislike"));
} }

View File

@@ -17,7 +17,12 @@ import com.replaymod.replaystudio.studio.ReplayStudio;
import de.johni0702.minecraft.gui.container.GuiContainer; import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel; import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiScreen; import de.johni0702.minecraft.gui.container.GuiScreen;
import de.johni0702.minecraft.gui.element.*; import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiCheckbox;
import de.johni0702.minecraft.gui.element.GuiImage;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTextField;
import de.johni0702.minecraft.gui.element.GuiTooltip;
import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu; import de.johni0702.minecraft.gui.element.advanced.GuiDropdownMenu;
import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar; import de.johni0702.minecraft.gui.element.advanced.GuiProgressBar;
import de.johni0702.minecraft.gui.element.advanced.GuiTextArea; import de.johni0702.minecraft.gui.element.advanced.GuiTextArea;
@@ -234,6 +239,8 @@ public class GuiUploadReplay extends GuiScreen {
}); });
validateInputs(); validateInputs();
name.onTextChanged(s -> validateInputs());
tags.onTextChanged(s -> validateInputs());
} }
public void validateInputs() { public void validateInputs() {

View File

@@ -14,9 +14,11 @@ import net.minecraft.client.gui.GuiMainMenu;
import net.minecraft.client.resources.I18n; import net.minecraft.client.resources.I18n;
import net.minecraftforge.client.event.GuiScreenEvent; import net.minecraftforge.client.event.GuiScreenEvent;
import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import java.io.File; import java.io.File;
import java.util.List;
public class GuiHandler { public class GuiHandler {
private static final int BUTTON_REPLAY_CENTER = 17890236; private static final int BUTTON_REPLAY_CENTER = 17890236;
@@ -28,23 +30,26 @@ public class GuiHandler {
} }
public void register() { public void register() {
FMLCommonHandler.instance().bus().register(this);
MinecraftForge.EVENT_BUS.register(this); MinecraftForge.EVENT_BUS.register(this);
} }
@SubscribeEvent @SubscribeEvent
public void injectIntoMainMenu(GuiScreenEvent.InitGuiEvent event) { public void injectIntoMainMenu(GuiScreenEvent.InitGuiEvent event) {
if (!(event.getGui() instanceof GuiMainMenu)) { if (!(event.gui instanceof GuiMainMenu)) {
return; return;
} }
GuiButton button = new GuiButton(BUTTON_REPLAY_CENTER, event.getGui().width / 2 - 100, @SuppressWarnings("unchecked")
event.getGui().height / 4 + 10 + 4 * 24, I18n.format("replaymod.gui.replaycenter")); List<GuiButton> buttonList = event.buttonList;
event.getButtonList().add(button); GuiButton button = new GuiButton(BUTTON_REPLAY_CENTER, event.gui.width / 2 - 100,
event.gui.height / 4 + 10 + 4 * 24, I18n.format("replaymod.gui.replaycenter"));
buttonList.add(button);
} }
@SubscribeEvent @SubscribeEvent
public void injectIntoReplayViewer(GuiScreenEvent.InitGuiEvent.Post event) { public void injectIntoReplayViewer(GuiScreenEvent.InitGuiEvent.Post event) {
AbstractGuiScreen guiScreen = GuiScreen.from(event.getGui()); AbstractGuiScreen guiScreen = GuiScreen.from(event.gui);
if (!(guiScreen instanceof GuiReplayViewer)) { if (!(guiScreen instanceof GuiReplayViewer)) {
return; return;
} }
@@ -70,15 +75,15 @@ public class GuiHandler {
@SubscribeEvent @SubscribeEvent
public void onButton(GuiScreenEvent.ActionPerformedEvent.Pre event) { public void onButton(GuiScreenEvent.ActionPerformedEvent.Pre event) {
if(!event.getButton().enabled) return; if(!event.button.enabled) return;
if (event.getGui() instanceof GuiMainMenu) { if (event.gui instanceof GuiMainMenu) {
if (event.getButton().id == BUTTON_REPLAY_CENTER) { if (event.button.id == BUTTON_REPLAY_CENTER) {
GuiReplayCenter replayCenter = new GuiReplayCenter(mod); GuiReplayCenter replayCenter = new GuiReplayCenter(mod);
if (mod.isLoggedIn()) { if (mod.isLoggedIn()) {
replayCenter.display(); replayCenter.display();
} else { } else {
new GuiLoginPrompt(mod.getApiClient(), GuiScreen.wrap(event.getGui()), replayCenter, true).display(); new GuiLoginPrompt(mod.getApiClient(), GuiScreen.wrap(event.gui), replayCenter, true).display();
} }
} }
} }

View File

@@ -4,6 +4,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import com.replaymod.core.ReplayMod; import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Utils;
import com.replaymod.replay.ReplayModReplay; import com.replaymod.replay.ReplayModReplay;
import com.replaymod.replaystudio.pathing.PathingRegistry; import com.replaymod.replaystudio.pathing.PathingRegistry;
import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.Path;
@@ -11,7 +12,11 @@ import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ReplayFile;
import de.johni0702.minecraft.gui.GuiRenderer; import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo; import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.*; import de.johni0702.minecraft.gui.container.AbstractGuiClickableContainer;
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.container.GuiVerticalList;
import de.johni0702.minecraft.gui.element.GuiButton; import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiLabel; import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTextField; import de.johni0702.minecraft.gui.element.GuiTextField;
@@ -41,9 +46,16 @@ public class GuiKeyframeRepository extends GuiScreen implements Closeable {
public final GuiButton overwriteButton = new GuiButton(buttonPanel).onClick(new Runnable() { public final GuiButton overwriteButton = new GuiButton(buttonPanel).onClick(new Runnable() {
@Override @Override
public void run() { public void run() {
timelines.put(selectedEntry.name, currentTimeline); GuiYesNoPopup popup = GuiYesNoPopup.open(GuiKeyframeRepository.this,
overwriteButton.setDisabled(); new GuiLabel().setI18nText("replaymod.gui.keyframerepo.overwrite").setColor(Colors.BLACK)
save(); ).setYesI18nLabel("gui.yes").setNoI18nLabel("gui.no");
Utils.addCallback(popup.getFuture(), doIt -> {
if (doIt) {
timelines.put(selectedEntry.name, currentTimeline);
overwriteButton.setDisabled();
save();
}
}, Throwable::printStackTrace);
} }
}).setSize(75, 20).setI18nLabel("replaymod.gui.overwrite").setDisabled(); }).setSize(75, 20).setI18nLabel("replaymod.gui.overwrite").setDisabled();
public final GuiButton saveAsButton = new GuiButton(buttonPanel).onClick(new Runnable() { public final GuiButton saveAsButton = new GuiButton(buttonPanel).onClick(new Runnable() {

View File

@@ -11,7 +11,7 @@ import com.replaymod.replaystudio.pathing.path.Keyframe;
import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.Path;
import com.replaymod.replaystudio.pathing.path.Timeline; import com.replaymod.replaystudio.pathing.path.Timeline;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@@ -65,7 +65,7 @@ public abstract class AbstractTimelinePlayer {
} }
replayHandler.getReplaySender().setSyncModeAndWait(); replayHandler.getReplaySender().setSyncModeAndWait();
MinecraftForge.EVENT_BUS.register(this); FMLCommonHandler.instance().bus().register(this);
lastTime = 0; lastTime = 0;
mc.timer = new ReplayTimer(mc.timer); mc.timer = new ReplayTimer(mc.timer);
mc.timer.timerSpeed = 1; mc.timer.timerSpeed = 1;
@@ -87,7 +87,7 @@ public abstract class AbstractTimelinePlayer {
mc.timer = ((ReplayTimer) mc.timer).getWrapped(); mc.timer = ((ReplayTimer) mc.timer).getWrapped();
replayHandler.getReplaySender().setReplaySpeed(0); replayHandler.getReplaySender().setReplaySpeed(0);
replayHandler.getReplaySender().setAsyncMode(true); replayHandler.getReplaySender().setAsyncMode(true);
MinecraftForge.EVENT_BUS.unregister(this); FMLCommonHandler.instance().bus().unregister(this);
return; return;
} }
long time = getTimePassed(); long time = getTimePassed();

View File

@@ -2,7 +2,7 @@ package com.replaymod.pathing.player;
import com.replaymod.core.utils.WrappedTimer; import com.replaymod.core.utils.WrappedTimer;
import net.minecraft.util.Timer; import net.minecraft.util.Timer;
import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.Event; import net.minecraftforge.fml.common.eventhandler.Event;
/** /**
@@ -20,7 +20,7 @@ public class ReplayTimer extends WrappedTimer {
copy(this, state); // Save our current state copy(this, state); // Save our current state
super.updateTimer(); // Update current state super.updateTimer(); // Update current state
copy(state, this); // Restore our old state copy(state, this); // Restore our old state
MinecraftForge.EVENT_BUS.post(new UpdatedEvent()); FMLCommonHandler.instance().bus().post(new UpdatedEvent());
} }
public Timer getWrapped() { public Timer getWrapped() {

View File

@@ -4,8 +4,10 @@ import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.Restrictions; import com.replaymod.core.utils.Restrictions;
import com.replaymod.recording.handler.ConnectionEventHandler; import com.replaymod.recording.handler.ConnectionEventHandler;
import com.replaymod.recording.packet.PacketListener; import com.replaymod.recording.packet.PacketListener;
import net.minecraftforge.common.MinecraftForge; import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandler;
import net.minecraft.network.NetworkManager; import net.minecraft.network.NetworkManager;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLInitializationEvent; import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
@@ -14,15 +16,17 @@ import net.minecraftforge.fml.common.network.NetworkRegistry;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.lwjgl.input.Keyboard; import org.lwjgl.input.Keyboard;
@Mod(modid = ReplayModRecording.MOD_ID, useMetadata = true) @Mod(modid = ReplayModRecording.MOD_ID,
version = "@MOD_VERSION@",
acceptedMinecraftVersions = "@MC_VERSION@",
useMetadata = true)
public class ReplayModRecording { public class ReplayModRecording {
public static final String MOD_ID = "replaymod-recording"; public static final String MOD_ID = "replaymod-recording";
@Mod.Instance(MOD_ID) @Mod.Instance(MOD_ID)
public static ReplayModRecording instance; public static ReplayModRecording instance;
@Mod.Instance(ReplayMod.MOD_ID) private ReplayMod core;
private static ReplayMod core;
private Logger logger; private Logger logger;
@@ -31,6 +35,7 @@ public class ReplayModRecording {
@Mod.EventHandler @Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) { public void preInit(FMLPreInitializationEvent event) {
logger = event.getModLog(); logger = event.getModLog();
core = ReplayMod.instance;
core.getSettingsRegistry().register(Setting.class); core.getSettingsRegistry().register(Setting.class);
@@ -48,12 +53,15 @@ public class ReplayModRecording {
@Mod.EventHandler @Mod.EventHandler
public void init(FMLInitializationEvent event) { public void init(FMLInitializationEvent event) {
EventBus bus = MinecraftForge.EVENT_BUS; EventBus bus = FMLCommonHandler.instance().bus();
bus.register(connectionEventHandler = new ConnectionEventHandler(logger, core)); bus.register(connectionEventHandler = new ConnectionEventHandler(logger, core));
NetworkRegistry.INSTANCE.newSimpleChannel(Restrictions.PLUGIN_CHANNEL); NetworkRegistry.INSTANCE.newChannel(Restrictions.PLUGIN_CHANNEL, new RestrictionsChannelHandler());
} }
@ChannelHandler.Sharable
private static class RestrictionsChannelHandler extends ChannelDuplexHandler {}
public void initiateRecording(NetworkManager networkManager) { public void initiateRecording(NetworkManager networkManager) {
connectionEventHandler.onConnectedToServerEvent(networkManager); connectionEventHandler.onConnectedToServerEvent(networkManager);
} }

View File

@@ -40,7 +40,7 @@ public class GuiRecordingOverlay {
*/ */
@SubscribeEvent @SubscribeEvent
public void renderRecordingIndicator(RenderGameOverlayEvent.Post event) { public void renderRecordingIndicator(RenderGameOverlayEvent.Post event) {
if (event.getType() != RenderGameOverlayEvent.ElementType.ALL) return; if (event.type != RenderGameOverlayEvent.ElementType.ALL) return;
if (settingsRegistry.get(Setting.INDICATOR)) { if (settingsRegistry.get(Setting.INDICATOR)) {
FontRenderer fontRenderer = mc.fontRendererObj; FontRenderer fontRenderer = mc.fontRendererObj;
fontRenderer.drawString(I18n.format("replaymod.gui.recording").toUpperCase(), 30, 18 - (fontRenderer.FONT_HEIGHT / 2), 0xffffffff); fontRenderer.drawString(I18n.format("replaymod.gui.recording").toUpperCase(), 30, 18 - (fontRenderer.FONT_HEIGHT / 2), 0xffffffff);

View File

@@ -0,0 +1,25 @@
package com.replaymod.recording.handler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import net.minecraftforge.fml.common.network.handshake.FMLHandshakeMessage;
/**
* 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<FMLHandshakeMessage> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FMLHandshakeMessage msg) throws Exception {
if (!(msg instanceof FMLHandshakeMessage.RegistryData)) {
// Pass on everything but RegistryData messages
ctx.fireChannelRead(msg);
}
}
}

View File

@@ -1,22 +1,24 @@
package com.replaymod.recording.handler; package com.replaymod.recording.handler;
import com.replaymod.recording.packet.PacketListener; import com.replaymod.recording.packet.PacketListener;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.EntityEquipmentSlot; import net.minecraft.item.Item;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.network.Packet; import net.minecraft.network.Packet;
import net.minecraft.network.datasync.EntityDataManager; import net.minecraft.network.PacketBuffer;
import net.minecraft.network.play.server.*; import net.minecraft.network.play.server.*;
import net.minecraft.network.play.server.S14PacketEntity.S17PacketEntityLookMove;
import net.minecraft.server.integrated.IntegratedServer; import net.minecraft.server.integrated.IntegratedServer;
import net.minecraft.util.EnumHand; import net.minecraft.util.BlockPos;
import net.minecraft.util.SoundCategory; import net.minecraft.util.MathHelper;
import net.minecraft.util.SoundEvent;
import net.minecraft.util.math.BlockPos;
import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.entity.minecart.MinecartInteractEvent; import net.minecraftforge.event.entity.minecart.MinecartInteractEvent;
import net.minecraftforge.event.entity.player.PlayerSleepInBedEvent; import net.minecraftforge.event.entity.player.PlayerSleepInBedEvent;
import net.minecraftforge.event.entity.player.PlayerUseItemEvent;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.PlayerEvent.ItemPickupEvent; import net.minecraftforge.fml.common.gameevent.PlayerEvent.ItemPickupEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent; import net.minecraftforge.fml.common.gameevent.TickEvent;
@@ -30,24 +32,24 @@ public class RecordingEventHandler {
private final PacketListener packetListener; private final PacketListener packetListener;
private Double lastX, lastY, lastZ; private Double lastX, lastY, lastZ;
private ItemStack[] playerItems = new ItemStack[6]; private ItemStack[] playerItems = new ItemStack[5];
private int ticksSinceLastCorrection; private int ticksSinceLastCorrection;
private boolean wasSleeping; private boolean wasSleeping;
private int lastRiding = -1; private int lastRiding = -1;
private Integer rotationYawHeadBefore; private Integer rotationYawHeadBefore;
private boolean wasHandActive;
private EnumHand lastActiveHand;
public RecordingEventHandler(PacketListener packetListener) { public RecordingEventHandler(PacketListener packetListener) {
this.packetListener = packetListener; this.packetListener = packetListener;
} }
public void register() { public void register() {
FMLCommonHandler.instance().bus().register(this);
MinecraftForge.EVENT_BUS.register(this); MinecraftForge.EVENT_BUS.register(this);
((RecordingEventSender) mc.renderGlobal).setRecordingEventHandler(this); ((RecordingEventSender) mc.renderGlobal).setRecordingEventHandler(this);
} }
public void unregister() { public void unregister() {
FMLCommonHandler.instance().bus().unregister(this);
MinecraftForge.EVENT_BUS.unregister(this); MinecraftForge.EVENT_BUS.unregister(this);
RecordingEventSender recordingEventSender = ((RecordingEventSender) mc.renderGlobal); RecordingEventSender recordingEventSender = ((RecordingEventSender) mc.renderGlobal);
if (recordingEventSender.getRecordingEventHandler() == this) { if (recordingEventSender.getRecordingEventHandler() == this) {
@@ -57,7 +59,7 @@ public class RecordingEventHandler {
public void onPlayerJoin() { public void onPlayerJoin() {
try { try {
packetListener.save(new SPacketSpawnPlayer(mc.thePlayer)); packetListener.save(spawnPlayer(mc.thePlayer));
} catch(Exception e1) { } catch(Exception e1) {
e1.printStackTrace(); e1.printStackTrace();
} }
@@ -65,28 +67,39 @@ public class RecordingEventHandler {
public void onPlayerRespawn() { public void onPlayerRespawn() {
try { try {
packetListener.save(new SPacketSpawnPlayer(mc.thePlayer)); packetListener.save(spawnPlayer(mc.thePlayer));
} catch(Exception e) { } catch(Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
public void onClientSound(SoundEvent sound, SoundCategory category, private S0CPacketSpawnPlayer spawnPlayer(EntityPlayer player) {
double x, double y, double z, float volume, float pitch) {
try { try {
// Send to all other players in ServerWorldEventHandler#playSoundToAllNearExcept S0CPacketSpawnPlayer packet = new S0CPacketSpawnPlayer();
packetListener.save(new SPacketSoundEffect(sound, category, x, y, z, volume, pitch));
} catch(Exception e) {
e.printStackTrace();
}
}
public void onClientEffect(int type, BlockPos pos, int data) { ByteBuf bb = Unpooled.buffer();
try { PacketBuffer pb = new PacketBuffer(bb);
// Send to all other players in ServerWorldEventHandler#playEvent
packetListener.save(new SPacketEffect(type, pos, data, false)); pb.writeVarIntToBuffer(player.getEntityId());
pb.writeUuid(EntityPlayer.getUUID(player.getGameProfile()));
pb.writeInt(MathHelper.floor_double(player.posX * 32.0D));
pb.writeInt(MathHelper.floor_double(player.posY * 32.0D));
pb.writeInt(MathHelper.floor_double(player.posZ * 32.0D));
pb.writeByte((byte) ((int) (player.rotationYaw * 256.0F / 360.0F)));
pb.writeByte((byte) ((int) (player.rotationPitch * 256.0F / 360.0F)));
ItemStack itemstack = player.inventory.getCurrentItem();
pb.writeShort(itemstack == null ? 0 : Item.getIdFromItem(itemstack.getItem()));
player.getDataWatcher().writeTo(pb);
packet.readPacketData(pb);
return packet;
} catch(Exception e) { } catch(Exception e) {
e.printStackTrace(); e.printStackTrace();
return null;
} }
} }
@@ -118,14 +131,19 @@ public class RecordingEventHandler {
lastZ = e.player.posZ; lastZ = e.player.posZ;
Packet packet; Packet packet;
if (force || Math.abs(dx) > 8.0 || Math.abs(dy) > 8.0 || Math.abs(dz) > 8.0) { if(force || Math.abs(dx) > 4.0 || Math.abs(dy) > 4.0 || Math.abs(dz) > 4.0) {
packet = new SPacketEntityTeleport(e.player); int x = MathHelper.floor_double(e.player.posX * 32.0D);
int y = MathHelper.floor_double(e.player.posY * 32.0D);
int z = MathHelper.floor_double(e.player.posZ * 32.0D);
byte yaw = (byte) ((int) (e.player.rotationYaw * 256.0F / 360.0F));
byte pitch = (byte) ((int) (e.player.rotationPitch * 256.0F / 360.0F));
packet = new S18PacketEntityTeleport(e.player.getEntityId(), x, y, z, yaw, pitch, e.player.onGround);
} else { } else {
byte newYaw = (byte) ((int) (e.player.rotationYaw * 256.0F / 360.0F)); byte newYaw = (byte) ((int) (e.player.rotationYaw * 256.0F / 360.0F));
byte newPitch = (byte) ((int) (e.player.rotationPitch * 256.0F / 360.0F)); byte newPitch = (byte) ((int) (e.player.rotationPitch * 256.0F / 360.0F));
packet = new SPacketEntity.S17PacketEntityLookMove(e.player.getEntityId(), packet = new S17PacketEntityLookMove(e.player.getEntityId(),
(short) Math.round(dx * 4096), (short) Math.round(dy * 4096), (short) Math.round(dz * 4096), (byte) Math.round(dx * 32), (byte) Math.round(dy * 32), (byte) Math.round(dz * 32),
newYaw, newPitch, e.player.onGround); newYaw, newPitch, e.player.onGround);
} }
@@ -135,16 +153,37 @@ public class RecordingEventHandler {
int rotationYawHead = ((int)(e.player.rotationYawHead * 256.0F / 360.0F)); int rotationYawHead = ((int)(e.player.rotationYawHead * 256.0F / 360.0F));
if(!Objects.equals(rotationYawHead, rotationYawHeadBefore)) { if(!Objects.equals(rotationYawHead, rotationYawHeadBefore)) {
packetListener.save(new SPacketEntityHeadLook(e.player, (byte) rotationYawHead)); S19PacketEntityHeadLook head = new S19PacketEntityHeadLook();
ByteBuf bb1 = Unpooled.buffer();
PacketBuffer pb1 = new PacketBuffer(bb1);
pb1.writeVarIntToBuffer(e.player.getEntityId());
pb1.writeByte(rotationYawHead);
head.readPacketData(pb1);
packetListener.save(head);
rotationYawHeadBefore = rotationYawHead; rotationYawHeadBefore = rotationYawHead;
} }
packetListener.save(new SPacketEntityVelocity(e.player.getEntityId(), e.player.motionX, e.player.motionY, e.player.motionZ)); S12PacketEntityVelocity vel = new S12PacketEntityVelocity(e.player.getEntityId(), e.player.motionX, e.player.motionY, e.player.motionZ);
packetListener.save(vel);
//Animation Packets //Animation Packets
//Swing Animation //Swing Animation
if (e.player.isSwingInProgress && e.player.swingProgressInt == -1) { if(e.player.swingProgressInt == 1) {
packetListener.save(new SPacketAnimation(e.player, e.player.swingingHand == EnumHand.MAIN_HAND ? 0 : 3)); S0BPacketAnimation pac = new S0BPacketAnimation();
ByteBuf bb = Unpooled.buffer();
PacketBuffer pb = new PacketBuffer(bb);
pb.writeVarIntToBuffer(e.player.getEntityId());
pb.writeByte(0);
pac.readPacketData(pb);
packetListener.save(pac);
} }
/* /*
@@ -168,62 +207,75 @@ public class RecordingEventHandler {
*/ */
//Inventory Handling //Inventory Handling
if (playerItems[0] != mc.thePlayer.getHeldItem(EnumHand.MAIN_HAND)) { if(playerItems[0] != mc.thePlayer.getHeldItem()) {
playerItems[0] = mc.thePlayer.getHeldItem(EnumHand.MAIN_HAND); playerItems[0] = mc.thePlayer.getHeldItem();
packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.MAINHAND, playerItems[0])); S04PacketEntityEquipment pee = new S04PacketEntityEquipment(e.player.getEntityId(), 0, playerItems[0]);
packetListener.save(pee);
} }
if (playerItems[1] != mc.thePlayer.getHeldItem(EnumHand.OFF_HAND)) { if(playerItems[1] != mc.thePlayer.inventory.armorInventory[0]) {
playerItems[1] = mc.thePlayer.getHeldItem(EnumHand.OFF_HAND); playerItems[1] = mc.thePlayer.inventory.armorInventory[0];
packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.OFFHAND, playerItems[1])); S04PacketEntityEquipment pee = new S04PacketEntityEquipment(e.player.getEntityId(), 1, playerItems[1]);
packetListener.save(pee);
} }
if (playerItems[2] != mc.thePlayer.inventory.armorInventory[0]) { if(playerItems[2] != mc.thePlayer.inventory.armorInventory[1]) {
playerItems[2] = mc.thePlayer.inventory.armorInventory[0]; playerItems[2] = mc.thePlayer.inventory.armorInventory[1];
packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.FEET, playerItems[2])); S04PacketEntityEquipment pee = new S04PacketEntityEquipment(e.player.getEntityId(), 2, playerItems[2]);
packetListener.save(pee);
} }
if (playerItems[3] != mc.thePlayer.inventory.armorInventory[1]) { if(playerItems[3] != mc.thePlayer.inventory.armorInventory[2]) {
playerItems[3] = mc.thePlayer.inventory.armorInventory[1]; playerItems[3] = mc.thePlayer.inventory.armorInventory[2];
packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.LEGS, playerItems[3])); S04PacketEntityEquipment pee = new S04PacketEntityEquipment(e.player.getEntityId(), 3, playerItems[3]);
packetListener.save(pee);
} }
if (playerItems[4] != mc.thePlayer.inventory.armorInventory[2]) { if(playerItems[4] != mc.thePlayer.inventory.armorInventory[3]) {
playerItems[4] = mc.thePlayer.inventory.armorInventory[2]; playerItems[4] = mc.thePlayer.inventory.armorInventory[3];
packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.CHEST, playerItems[4])); S04PacketEntityEquipment pee = new S04PacketEntityEquipment(e.player.getEntityId(), 4, playerItems[4]);
} packetListener.save(pee);
if (playerItems[5] != mc.thePlayer.inventory.armorInventory[3]) {
playerItems[5] = mc.thePlayer.inventory.armorInventory[3];
packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.HEAD, playerItems[5]));
} }
//Leaving Ride //Leaving Ride
if((!mc.thePlayer.isRiding() && lastRiding != -1) || if((!mc.thePlayer.isRiding() && lastRiding != -1) ||
(mc.thePlayer.isRiding() && lastRiding != mc.thePlayer.getRidingEntity().getEntityId())) { (mc.thePlayer.isRiding() && lastRiding != mc.thePlayer.ridingEntity.getEntityId())) {
if(!mc.thePlayer.isRiding()) { if(!mc.thePlayer.isRiding()) {
lastRiding = -1; lastRiding = -1;
} else { } else {
lastRiding = mc.thePlayer.getRidingEntity().getEntityId(); lastRiding = mc.thePlayer.ridingEntity.getEntityId();
} }
packetListener.save(new SPacketEntityAttach(e.player, e.player.getRidingEntity()));
S1BPacketEntityAttach pea = new S1BPacketEntityAttach();
ByteBuf buf = Unpooled.buffer();
PacketBuffer pbuf = new PacketBuffer(buf);
pbuf.writeInt(e.player.getEntityId());
pbuf.writeInt(lastRiding);
pbuf.writeBoolean(false);
pea.readPacketData(pbuf);
packetListener.save(pea);
} }
//Sleeping //Sleeping
if(!mc.thePlayer.isPlayerSleeping() && wasSleeping) { if(!mc.thePlayer.isPlayerSleeping() && wasSleeping) {
packetListener.save(new SPacketAnimation(e.player, 2)); S0BPacketAnimation pac = new S0BPacketAnimation();
wasSleeping = false;
}
// Active hand (e.g. eating, drinking, blocking) ByteBuf bb = Unpooled.buffer();
if (mc.thePlayer.isHandActive() ^ wasHandActive || mc.thePlayer.getActiveHand() != lastActiveHand) { PacketBuffer pb = new PacketBuffer(bb);
wasHandActive = mc.thePlayer.isHandActive();
lastActiveHand = mc.thePlayer.getActiveHand(); pb.writeVarIntToBuffer(e.player.getEntityId());
EntityDataManager dataManager = new EntityDataManager(null); pb.writeByte(2);
int state = (wasHandActive ? 1 : 0) | (lastActiveHand == EnumHand.OFF_HAND ? 2 : 0);
dataManager.register(EntityLiving.HAND_STATES, (byte) state); pac.readPacketData(pb);
packetListener.save(new SPacketEntityMetadata(mc.thePlayer.getEntityId(), dataManager, true));
packetListener.save(pac);
wasSleeping = false;
} }
} catch(Exception e1) { } catch(Exception e1) {
@@ -234,7 +286,27 @@ public class RecordingEventHandler {
@SubscribeEvent @SubscribeEvent
public void onPickupItem(ItemPickupEvent event) { public void onPickupItem(ItemPickupEvent event) {
try { try {
packetListener.save(new SPacketCollectItem(event.pickedUp.getEntityId(), event.player.getEntityId())); packetListener.save(new S0DPacketCollectItem(event.pickedUp.getEntityId(), event.player.getEntityId()));
} catch(Exception e) {
e.printStackTrace();
}
}
@SubscribeEvent
public void onStartEating(PlayerUseItemEvent.Start event) {
try {
if(!event.entityPlayer.isEating()) return;
S0BPacketAnimation packet = new S0BPacketAnimation();
ByteBuf bb = Unpooled.buffer();
PacketBuffer pb = new PacketBuffer(bb);
pb.writeVarIntToBuffer(event.entityPlayer.getEntityId());
pb.writeByte(3);
packet.readPacketData(pb);
packetListener.save(packet);
} catch(Exception e) { } catch(Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
@@ -243,11 +315,21 @@ public class RecordingEventHandler {
@SubscribeEvent @SubscribeEvent
public void onSleep(PlayerSleepInBedEvent event) { public void onSleep(PlayerSleepInBedEvent event) {
try { try {
if (event.getEntityPlayer() != mc.thePlayer) { if(event.entityPlayer != mc.thePlayer) {
return; return;
} }
packetListener.save(new SPacketUseBed(event.getEntityPlayer(), event.getPos())); S0APacketUseBed pub = new S0APacketUseBed();
ByteBuf buf = Unpooled.buffer();
PacketBuffer pbuf = new PacketBuffer(buf);
pbuf.writeVarIntToBuffer(event.entityPlayer.getEntityId());
pbuf.writeBlockPos(event.pos);
pub.readPacketData(pbuf);
packetListener.save(pub);
wasSleeping = true; wasSleeping = true;
@@ -259,13 +341,24 @@ public class RecordingEventHandler {
@SubscribeEvent @SubscribeEvent
public void enterMinecart(MinecartInteractEvent event) { public void enterMinecart(MinecartInteractEvent event) {
try { try {
if(event.getEntity() != mc.thePlayer) { if(event.player != mc.thePlayer) {
return; return;
} }
packetListener.save(new SPacketEntityAttach(event.getPlayer(), event.getMinecart())); S1BPacketEntityAttach pea = new S1BPacketEntityAttach();
lastRiding = event.getMinecart().getEntityId(); ByteBuf buf = Unpooled.buffer();
PacketBuffer pbuf = new PacketBuffer(buf);
pbuf.writeInt(event.player.getEntityId());
pbuf.writeInt(event.minecart.getEntityId());
pbuf.writeBoolean(false);
pea.readPacketData(pbuf);
packetListener.save(pea);
lastRiding = event.minecart.getEntityId();
} catch(Exception e) { } catch(Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
@@ -274,7 +367,7 @@ public class RecordingEventHandler {
public void onBlockBreakAnim(int breakerId, BlockPos pos, int progress) { public void onBlockBreakAnim(int breakerId, BlockPos pos, int progress) {
EntityPlayer thePlayer = mc.thePlayer; EntityPlayer thePlayer = mc.thePlayer;
if (thePlayer != null && breakerId == thePlayer.getEntityId()) { if (thePlayer != null && breakerId == thePlayer.getEntityId()) {
packetListener.save(new SPacketBlockBreakAnim(breakerId, pos, progress)); packetListener.save(new S25PacketBlockBreakAnim(breakerId, pos, progress));
} }
} }

View File

@@ -4,14 +4,15 @@ import com.replaymod.recording.handler.RecordingEventHandler;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.client.network.NetHandlerPlayClient;
import net.minecraft.client.network.NetworkPlayerInfo; import net.minecraft.client.network.NetworkPlayerInfo;
import net.minecraft.network.play.server.SPacketPlayerListItem; import net.minecraft.network.play.server.S07PacketRespawn;
import net.minecraft.network.play.server.SPacketRespawn; import net.minecraft.network.play.server.S38PacketPlayerListItem;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@@ -36,16 +37,18 @@ public abstract class MixinNetHandlerPlayClient {
* @param ci Callback info * @param ci Callback info
*/ */
@Inject(method = "handlePlayerListItem", at=@At("HEAD")) @Inject(method = "handlePlayerListItem", at=@At("HEAD"))
public void recordOwnJoin(SPacketPlayerListItem packet, CallbackInfo ci) { public void recordOwnJoin(S38PacketPlayerListItem packet, CallbackInfo ci) {
if (gameController.thePlayer == null) return; if (gameController.thePlayer == null) return;
RecordingEventHandler handler = getRecordingEventHandler(); RecordingEventHandler handler = getRecordingEventHandler();
if (handler != null && packet.getAction() == SPacketPlayerListItem.Action.ADD_PLAYER) { if (handler != null && packet.func_179768_b() == S38PacketPlayerListItem.Action.ADD_PLAYER) {
for (SPacketPlayerListItem.AddPlayerData data : packet.getEntries()) { @SuppressWarnings("unchecked")
if (data.getProfile() == null || data.getProfile().getId() == null) continue; List<S38PacketPlayerListItem.AddPlayerData> dataList = packet.func_179767_a();
for (S38PacketPlayerListItem.AddPlayerData data : dataList) {
if (data.func_179962_a() == null || data.func_179962_a().getId() == null) continue;
// Only add spawn packet for our own player and only if he isn't known yet // Only add spawn packet for our own player and only if he isn't known yet
if (data.getProfile().getId().equals(Minecraft.getMinecraft().thePlayer.getGameProfile().getId()) if (data.func_179962_a().getId().equals(Minecraft.getMinecraft().thePlayer.getGameProfile().getId())
&& !playerInfoMap.containsKey(data.getProfile().getId())) { && !playerInfoMap.containsKey(data.func_179962_a().getId())) {
handler.onPlayerJoin(); handler.onPlayerJoin();
} }
} }
@@ -55,12 +58,12 @@ public abstract class MixinNetHandlerPlayClient {
/** /**
* Record the own player entity respawning. * Record the own player entity respawning.
* We cannot use the {@link net.minecraftforge.event.entity.EntityJoinWorldEvent} because that would also include * We cannot use the {@link net.minecraftforge.event.entity.EntityJoinWorldEvent} because that would also include
* the first spawn which is already handled by {@link #recordOwnJoin(SPacketPlayerListItem, CallbackInfo)}. * the first spawn which is already handled by {@link #recordOwnJoin(S38PacketPlayerListItem, CallbackInfo)}.
* @param packet The packet * @param packet The packet
* @param ci Callback info * @param ci Callback info
*/ */
@Inject(method = "handleRespawn", at=@At("RETURN")) @Inject(method = "handleRespawn", at=@At("RETURN"))
public void recordOwnRespawn(SPacketRespawn packet, CallbackInfo ci) { public void recordOwnRespawn(S07PacketRespawn packet, CallbackInfo ci) {
RecordingEventHandler handler = getRecordingEventHandler(); RecordingEventHandler handler = getRecordingEventHandler();
if (handler != null) { if (handler != null) {
handler.onPlayerRespawn(); handler.onPlayerRespawn();

View File

@@ -1,7 +1,11 @@
package com.replaymod.recording.mixin; package com.replaymod.recording.mixin;
import com.replaymod.recording.handler.FMLHandshakeFilter;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.embedded.EmbeddedChannel; import io.netty.channel.embedded.EmbeddedChannel;
import net.minecraftforge.fml.common.network.handshake.FMLHandshakeCodec;
import net.minecraftforge.fml.common.network.handshake.NetworkDispatcher; import net.minecraftforge.fml.common.network.handshake.NetworkDispatcher;
import net.minecraftforge.fml.relauncher.Side;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
@@ -11,17 +15,32 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(value = NetworkDispatcher.class, remap = false) @Mixin(value = NetworkDispatcher.class, remap = false)
public abstract class MixinNetworkDispatcher { public abstract class MixinNetworkDispatcher {
@Shadow
private Side side;
@Shadow @Shadow
private EmbeddedChannel handshakeChannel; private EmbeddedChannel handshakeChannel;
/** /**
* Always sets fml:isLocal to false. * Always sets fml:isLocal to false on the server side.
* This effectively removes the difference in the FML handshake between SP and MP * This effectively removes the difference in the FML handshake between SP and MP
* and forces the block/item ids, etc. to always be send. * and forces the block/item ids, etc. to always be send.
* This might have undesired side effects but at least it works at all. * Injects a {@link FMLHandshakeFilter} on the client side to filter out
* those extra, unexpected packets.
*/ */
@Inject(method = "insertIntoChannel", at=@At("HEAD")) @Inject(method = "insertIntoChannel", at=@At("HEAD"))
public void replayModRecording_forceIsLocalToFalse(CallbackInfo cb) { public void replayModRecording_setupForLocalRecording(CallbackInfo cb) {
handshakeChannel.attr(NetworkDispatcher.IS_LOCAL).set(false); // 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());
}
} }
} }

View File

@@ -1,28 +0,0 @@
package com.replaymod.recording.mixin;
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;
@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(mc.thePlayer, type, pos, data);
}
}

View File

@@ -2,7 +2,7 @@ package com.replaymod.recording.mixin;
import com.replaymod.recording.handler.RecordingEventHandler; import com.replaymod.recording.handler.RecordingEventHandler;
import net.minecraft.client.renderer.RenderGlobal; import net.minecraft.client.renderer.RenderGlobal;
import net.minecraft.util.math.BlockPos; import net.minecraft.util.BlockPos;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;

View File

@@ -1,62 +0,0 @@
package com.replaymod.recording.mixin;
import com.replaymod.recording.handler.RecordingEventHandler;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.WorldClient;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.profiler.Profiler;
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;
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;
@Mixin(WorldClient.class)
public abstract class MixinWorldClient extends World implements RecordingEventHandler.RecordingEventSender {
@Shadow
private Minecraft mc;
protected MixinWorldClient(ISaveHandler saveHandlerIn, WorldInfo info, WorldProvider providerIn, Profiler profilerIn, boolean client) {
super(saveHandlerIn, info, providerIn, profilerIn, client);
}
private RecordingEventHandler replayModRecording_getRecordingEventHandler() {
return ((RecordingEventHandler.RecordingEventSender) mc.renderGlobal).getRecordingEventHandler();
}
// Sounds that are emitted by thePlayer no longer take the long way over the server
// but are instead played directly by the client. The server only sends these sounds to
// other clients so we have to record them manually.
// E.g. Block place sounds
@Inject(method = "playSound", at = @At("HEAD"))
public void replayModRecording_recordClientSound(EntityPlayer player, double x, double y, double z, SoundEvent sound, SoundCategory category,
float volume, float pitch, CallbackInfo ci) {
if (player == mc.thePlayer) {
RecordingEventHandler handler = replayModRecording_getRecordingEventHandler();
if (handler != null) {
handler.onClientSound(sound, category, x, y, z, volume, pitch);
}
}
}
// Same goes for level events (also called effects). E.g. door open, block break, etc.
// These are handled in the World class, so we override the method in WorldClient and add our special handling.
@Override
public void playEvent(EntityPlayer player, int type, BlockPos pos, int data) {
if (player == mc.thePlayer) {
// We caused this event, the server won't send it to us
RecordingEventHandler handler = replayModRecording_getRecordingEventHandler();
if (handler != null) {
handler.onClientEffect(type, pos, data);
}
}
super.playEvent(player, type, pos, data);
}
}

View File

@@ -9,24 +9,21 @@ import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInboundHandlerAdapter;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.entity.DataWatcher;
import net.minecraft.entity.Entity; import net.minecraft.entity.Entity;
import net.minecraft.network.EnumConnectionState; import net.minecraft.network.EnumConnectionState;
import net.minecraft.network.EnumPacketDirection; import net.minecraft.network.EnumPacketDirection;
import net.minecraft.network.Packet; import net.minecraft.network.Packet;
import net.minecraft.network.PacketBuffer; import net.minecraft.network.PacketBuffer;
import net.minecraft.network.datasync.EntityDataManager;
import net.minecraft.network.play.server.*; import net.minecraft.network.play.server.*;
import net.minecraft.util.text.TextComponentString; import net.minecraft.util.ChatComponentText;
import net.minecraftforge.fml.common.network.internal.FMLProxyPacket; import net.minecraftforge.fml.common.network.internal.FMLProxyPacket;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.*;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -87,8 +84,8 @@ public class PacketListener extends ChannelInboundHandlerAdapter {
public void save(Packet packet) { public void save(Packet packet) {
try { try {
if(packet instanceof SPacketSpawnPlayer) { if(packet instanceof S0CPacketSpawnPlayer) {
UUID uuid = ((SPacketSpawnPlayer) packet).getUniqueId(); UUID uuid = ((S0CPacketSpawnPlayer) packet).func_179819_c();
Set<String> uuids = new HashSet<>(Arrays.asList(metaData.getPlayers())); Set<String> uuids = new HashSet<>(Arrays.asList(metaData.getPlayers()));
uuids.add(uuid.toString()); uuids.add(uuid.toString());
metaData.setPlayers(uuids.toArray(new String[uuids.size()])); metaData.setPlayers(uuids.toArray(new String[uuids.size()]));
@@ -154,16 +151,16 @@ public class PacketListener extends ChannelInboundHandlerAdapter {
try { try {
Packet packet = (Packet) msg; Packet packet = (Packet) msg;
if(packet instanceof SPacketCollectItem) { if(packet instanceof S0DPacketCollectItem) {
if(mc.thePlayer != null || if(mc.thePlayer != null ||
((SPacketCollectItem) packet).getEntityID() == mc.thePlayer.getEntityId()) { ((S0DPacketCollectItem) packet).func_149353_d() == mc.thePlayer.getEntityId()) {
super.channelRead(ctx, msg); super.channelRead(ctx, msg);
return; return;
} }
} }
if (packet instanceof SPacketResourcePackSend) { if (packet instanceof S48PacketResourcePackSend) {
save(resourcePackRecorder.handleResourcePack((SPacketResourcePackSend) packet)); save(resourcePackRecorder.handleResourcePack((S48PacketResourcePackSend) packet));
return; return;
} }
@@ -176,10 +173,10 @@ public class PacketListener extends ChannelInboundHandlerAdapter {
save(packet); save(packet);
if (packet instanceof SPacketCustomPayload) { if (packet instanceof S3FPacketCustomPayload) {
SPacketCustomPayload p = (SPacketCustomPayload) packet; S3FPacketCustomPayload p = (S3FPacketCustomPayload) packet;
if (Restrictions.PLUGIN_CHANNEL.equals(p.getChannelName())) { if (Restrictions.PLUGIN_CHANNEL.equals(p.getChannelName())) {
packet = new SPacketDisconnect(new TextComponentString("Please update to view this replay.")); packet = new S40PacketDisconnect(new ChatComponentText("Please update to view this replay."));
save(packet); save(packet);
} }
} }
@@ -192,31 +189,27 @@ public class PacketListener extends ChannelInboundHandlerAdapter {
super.channelRead(ctx, msg); super.channelRead(ctx, msg);
} }
private <T> void DataManager_set(EntityDataManager dataManager, EntityDataManager.DataEntry<T> entry) {
dataManager.register(entry.getKey(), entry.getValue());
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private byte[] getPacketData(Packet packet) throws IOException { private byte[] getPacketData(Packet packet) throws IOException {
if (packet instanceof SPacketSpawnMob) { if(packet instanceof S0FPacketSpawnMob) {
SPacketSpawnMob p = (SPacketSpawnMob) packet; S0FPacketSpawnMob p = (S0FPacketSpawnMob) packet;
if (p.dataManager == null) { if (p.field_149043_l == null) {
p.dataManager = new EntityDataManager(null); p.field_149043_l = new DataWatcher(null);
if (p.getDataManagerEntries() != null) { if(p.func_149027_c() != null) {
for (EntityDataManager.DataEntry<?> entry : p.getDataManagerEntries()) { for(DataWatcher.WatchableObject wo : (List<DataWatcher.WatchableObject>) p.func_149027_c()) {
DataManager_set(p.dataManager, entry); p.field_149043_l.addObject(wo.getDataValueId(), wo.getObject());
} }
} }
} }
} }
if (packet instanceof SPacketSpawnPlayer) { if(packet instanceof S0CPacketSpawnPlayer) {
SPacketSpawnPlayer p = (SPacketSpawnPlayer) packet; S0CPacketSpawnPlayer p = (S0CPacketSpawnPlayer) packet;
if (p.watcher == null) { if (p.field_148960_i == null) {
p.watcher = new EntityDataManager(null); p.field_148960_i = new DataWatcher(null);
if (p.getDataManagerEntries() != null) { if(p.func_148944_c() != null) {
for (EntityDataManager.DataEntry<?> entry : p.getDataManagerEntries()) { for(DataWatcher.WatchableObject wo : (List<DataWatcher.WatchableObject>) p.func_148944_c()) {
DataManager_set(p.watcher, entry); p.field_148960_i.addObject(wo.getDataValueId(), wo.getObject());
} }
} }
} }

View File

@@ -9,14 +9,15 @@ import com.replaymod.replaystudio.replay.ReplayFile;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiScreenWorking; import net.minecraft.client.gui.GuiScreenWorking;
import net.minecraft.client.gui.GuiYesNo; import net.minecraft.client.gui.GuiYesNo;
import net.minecraft.client.gui.GuiYesNoCallback;
import net.minecraft.client.multiplayer.ServerData; import net.minecraft.client.multiplayer.ServerData;
import net.minecraft.client.multiplayer.ServerList; import net.minecraft.client.multiplayer.ServerList;
import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.client.network.NetHandlerPlayClient;
import net.minecraft.client.resources.I18n; import net.minecraft.client.resources.I18n;
import net.minecraft.client.resources.ResourcePackRepository; import net.minecraft.client.resources.ResourcePackRepository;
import net.minecraft.network.NetworkManager; import net.minecraft.network.NetworkManager;
import net.minecraft.network.play.client.CPacketResourcePackStatus; import net.minecraft.network.play.client.C19PacketResourcePackStatus;
import net.minecraft.network.play.server.SPacketResourcePackSend; import net.minecraft.network.play.server.S48PacketResourcePackSend;
import net.minecraft.util.HttpUtil; import net.minecraft.util.HttpUtil;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
@@ -26,6 +27,7 @@ import javax.annotation.Nonnull;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
/** /**
@@ -52,6 +54,9 @@ public class ResourcePackRecorder {
boolean doWrite = false; // Whether we are the first and have to write it boolean doWrite = false; // Whether we are the first and have to write it
synchronized (replayFile) { // Need to read, modify and write the resource pack index atomically synchronized (replayFile) { // Need to read, modify and write the resource pack index atomically
Map<Integer, String> index = replayFile.getResourcePackIndex(); Map<Integer, String> index = replayFile.getResourcePackIndex();
if (index == null) {
index = new HashMap<>();
}
if (!index.containsValue(hash)) { if (!index.containsValue(hash)) {
// Hash is unknown, we have to write the resource pack ourselves // Hash is unknown, we have to write the resource pack ourselves
doWrite = true; doWrite = true;
@@ -70,12 +75,12 @@ public class ResourcePackRecorder {
} }
} }
public synchronized SPacketResourcePackSend handleResourcePack(SPacketResourcePackSend packet) { public synchronized S48PacketResourcePackSend handleResourcePack(S48PacketResourcePackSend packet) {
final int requestId = nextRequestId++; final int requestId = nextRequestId++;
final NetHandlerPlayClient netHandler = mc.getConnection(); final NetHandlerPlayClient netHandler = mc.getNetHandler();
final NetworkManager netManager = netHandler.getNetworkManager(); final NetworkManager netManager = netHandler.getNetworkManager();
final String url = packet.getURL(); final String url = packet.func_179783_a();
final String hash = packet.getHash(); final String hash = packet.func_179784_b();
if (url.startsWith("level://")) { if (url.startsWith("level://")) {
String levelName = url.substring("level://".length()); String levelName = url.substring("level://".length());
@@ -83,60 +88,65 @@ public class ResourcePackRecorder {
final File levelDir = new File(savesDir, levelName); final File levelDir = new File(savesDir, levelName);
if (levelDir.isFile()) { if (levelDir.isFile()) {
netManager.sendPacket(new CPacketResourcePackStatus(hash, CPacketResourcePackStatus.Action.ACCEPTED)); netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.ACCEPTED));
Futures.addCallback(mc.getResourcePackRepository().setResourcePackInstance(levelDir), new FutureCallback<Object>() { Futures.addCallback(mc.getResourcePackRepository().func_177319_a(levelDir), new FutureCallback() {
@Override @Override
public void onSuccess(Object result) { public void onSuccess(Object result) {
recordResourcePack(levelDir, requestId); recordResourcePack(levelDir, requestId);
netManager.sendPacket(new CPacketResourcePackStatus(hash, CPacketResourcePackStatus.Action.SUCCESSFULLY_LOADED)); netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.SUCCESSFULLY_LOADED));
} }
@Override @Override
public void onFailure(@Nonnull Throwable throwable) { public void onFailure(@Nonnull Throwable throwable) {
netManager.sendPacket(new CPacketResourcePackStatus(hash, CPacketResourcePackStatus.Action.FAILED_DOWNLOAD)); netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.FAILED_DOWNLOAD));
} }
}); });
} else { } else {
netManager.sendPacket(new CPacketResourcePackStatus(hash, CPacketResourcePackStatus.Action.FAILED_DOWNLOAD)); netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.FAILED_DOWNLOAD));
} }
} else { } else {
final ServerData serverData = mc.getCurrentServerData(); final ServerData serverData = mc.getCurrentServerData();
if (serverData != null && serverData.getResourceMode() == ServerData.ServerResourceMode.ENABLED) { if (serverData != null && serverData.getResourceMode() == ServerData.ServerResourceMode.ENABLED) {
netManager.sendPacket(new CPacketResourcePackStatus(hash, CPacketResourcePackStatus.Action.ACCEPTED)); netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.ACCEPTED));
downloadResourcePackFuture(requestId, url, hash); downloadResourcePackFuture(requestId, url, hash);
} else if (serverData != null && serverData.getResourceMode() != ServerData.ServerResourceMode.PROMPT) { } else if (serverData != null && serverData.getResourceMode() != ServerData.ServerResourceMode.PROMPT) {
netManager.sendPacket(new CPacketResourcePackStatus(hash, CPacketResourcePackStatus.Action.DECLINED)); netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.DECLINED));
} else { } else {
mc.addScheduledTask(() -> mc.displayGuiScreen(new GuiYesNo((result, id) -> { // Lambdas MUST NOT be used with methods that need re-obfuscation in FG prior to 2.2 (will result in AbstractMethodError)
if (serverData != null) { //noinspection Convert2Lambda
serverData.setResourceMode(result ? ServerData.ServerResourceMode.ENABLED : ServerData.ServerResourceMode.DISABLED); mc.addScheduledTask(() -> mc.displayGuiScreen(new GuiYesNo(new GuiYesNoCallback() {
} @Override
if (result) { public void confirmClicked(boolean result, int id) {
netManager.sendPacket(new CPacketResourcePackStatus(hash, CPacketResourcePackStatus.Action.ACCEPTED)); if (serverData != null) {
downloadResourcePackFuture(requestId, url, hash); serverData.setResourceMode(result ? ServerData.ServerResourceMode.ENABLED : ServerData.ServerResourceMode.DISABLED);
} else { }
netManager.sendPacket(new CPacketResourcePackStatus(hash, CPacketResourcePackStatus.Action.DECLINED)); if (result) {
} netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.ACCEPTED));
ResourcePackRecorder.this.downloadResourcePackFuture(requestId, url, hash);
} else {
netManager.sendPacket(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.DECLINED));
}
ServerList.saveSingleServer(serverData); ServerList.func_147414_b(serverData);
mc.displayGuiScreen(null); mc.displayGuiScreen(null);
}
}, I18n.format("multiplayer.texturePrompt.line1"), I18n.format("multiplayer.texturePrompt.line2"), 0))); }, I18n.format("multiplayer.texturePrompt.line1"), I18n.format("multiplayer.texturePrompt.line2"), 0)));
} }
} }
return new SPacketResourcePackSend("replay://" + requestId, ""); return new S48PacketResourcePackSend("replay://" + requestId, "");
} }
private void downloadResourcePackFuture(int requestId, String url, final String hash) { private void downloadResourcePackFuture(int requestId, String url, final String hash) {
Futures.addCallback(downloadResourcePack(requestId, url, hash), new FutureCallback() { Futures.addCallback(downloadResourcePack(requestId, url, hash), new FutureCallback() {
@Override @Override
public void onSuccess(Object result) { public void onSuccess(Object result) {
mc.getConnection().sendPacket(new CPacketResourcePackStatus(hash, CPacketResourcePackStatus.Action.SUCCESSFULLY_LOADED)); mc.getNetHandler().addToSendQueue(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.SUCCESSFULLY_LOADED));
} }
@Override @Override
public void onFailure(@Nonnull Throwable throwable) { public void onFailure(@Nonnull Throwable throwable) {
mc.getConnection().sendPacket(new CPacketResourcePackStatus(hash, CPacketResourcePackStatus.Action.FAILED_DOWNLOAD)); mc.getNetHandler().addToSendQueue(new C19PacketResourcePackStatus(hash, C19PacketResourcePackStatus.Action.FAILED_DOWNLOAD));
} }
}); });
} }
@@ -161,16 +171,16 @@ public class ResourcePackRecorder {
} }
final File file = new File(repo.dirServerResourcepacks, fileName); final File file = new File(repo.dirServerResourcepacks, fileName);
repo.lock.lock(); repo.field_177321_h.lock();
try { try {
repo.clearResourcePack(); repo.func_148529_f();
if (file.exists() && hash.length() == 40) { if (file.exists() && hash.length() == 40) {
try { try {
String fileHash = Hashing.sha1().hashBytes(Files.toByteArray(file)).toString(); String fileHash = Hashing.sha1().hashBytes(Files.toByteArray(file)).toString();
if (fileHash.equals(hash)) { if (fileHash.equals(hash)) {
recordResourcePack(file, requestId); recordResourcePack(file, requestId);
return repo.setResourcePackInstance(file); return repo.func_177319_a(file);
} }
logger.warn("File " + file + " had wrong hash (expected " + hash + ", found " + fileHash + "). Deleting it."); logger.warn("File " + file + " had wrong hash (expected " + hash + ", found " + fileHash + "). Deleting it.");
@@ -186,13 +196,13 @@ public class ResourcePackRecorder {
Futures.getUnchecked(mc.addScheduledTask(() -> mc.displayGuiScreen(guiScreen))); Futures.getUnchecked(mc.addScheduledTask(() -> mc.displayGuiScreen(guiScreen)));
Map<String, String> sessionInfo = Minecraft.getSessionInfo(); Map sessionInfo = Minecraft.getSessionInfo();
repo.downloadingPacks = HttpUtil.downloadResourcePack(file, url, sessionInfo, 50 * 1024 * 1024, guiScreen, mc.getProxy()); repo.field_177322_i = HttpUtil.func_180192_a(file, url, sessionInfo, 50 * 1024 * 1024, guiScreen, mc.getProxy());
Futures.addCallback(repo.downloadingPacks, new FutureCallback<Object>() { Futures.addCallback(repo.field_177322_i, new FutureCallback() {
@Override @Override
public void onSuccess(Object value) { public void onSuccess(Object value) {
recordResourcePack(file, requestId); recordResourcePack(file, requestId);
repo.setResourcePackInstance(file); repo.func_177319_a(file);
} }
@Override @Override
@@ -200,9 +210,9 @@ public class ResourcePackRecorder {
throwable.printStackTrace(); throwable.printStackTrace();
} }
}); });
return repo.downloadingPacks; return repo.field_177322_i;
} finally { } finally {
repo.lock.unlock(); repo.field_177321_h.unlock();
} }
} }

View File

@@ -1,6 +1,8 @@
package com.replaymod.render; package com.replaymod.render;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.Getter;
import net.minecraft.client.resources.I18n; import net.minecraft.client.resources.I18n;
import org.lwjgl.util.ReadableColor; import org.lwjgl.util.ReadableColor;
@@ -45,7 +47,7 @@ public class RenderSettings {
} }
public String getValue() { public String getValue() {
return "-y -f rawvideo -pix_fmt rgb24 -s %WIDTH%x%HEIGHT% -r %FPS% -i - " + preset; return "-y -f rawvideo -pix_fmt rgb24 -s %WIDTH%x%HEIGHT% -r %FPS% -i - %FILTERS%" + preset;
} }
public String getFileExtension() { public String getFileExtension() {
@@ -64,6 +66,19 @@ public class RenderSettings {
} }
} }
@AllArgsConstructor
public enum AntiAliasing {
NONE(1), X2(2), X4(4), X8(8);
@Getter
private final int factor;
@Override
public String toString() {
return I18n.format("replaymod.gui.rendersettings.antialiasing." + name().toLowerCase());
}
}
private final RenderMethod renderMethod; private final RenderMethod renderMethod;
private final EncodingPreset encodingPreset; private final EncodingPreset encodingPreset;
private final int videoWidth; private final int videoWidth;
@@ -78,9 +93,48 @@ public class RenderSettings {
private final boolean stabilizeRoll; private final boolean stabilizeRoll;
private final ReadableColor chromaKeyingColor; private final ReadableColor chromaKeyingColor;
private final boolean inject360Metadata; private final boolean inject360Metadata;
private final AntiAliasing antiAliasing;
private final String exportCommand; private final String exportCommand;
private final String exportArguments; private final String exportArguments;
private final boolean highPerformance; private final boolean highPerformance;
/**
* @return the width of the output video during rendering, including the upscale for Anti-Aliasing.
*/
public int getVideoWidth() {
return videoWidth * antiAliasing.getFactor();
}
/**
* @return the height of the output video during rendering, including the upscale for Anti-Aliasing.
*/
public int getVideoHeight() {
return videoHeight * antiAliasing.getFactor();
}
/**
* @return the actual width of the output video.
*/
public int getTargetVideoWidth() {
return videoWidth;
}
/**
* @return the actual height of the output video.
*/
public int getTargetVideoHeight() {
return videoHeight;
}
public String getVideoFilters() {
if (antiAliasing == AntiAliasing.NONE) {
return "";
} else {
double factor = 1.0 / antiAliasing.getFactor();
return String.format("-vf scale=iw*%1$s:ih*%1$s ", factor);
}
}
} }

View File

@@ -1,30 +1,39 @@
package com.replaymod.render; package com.replaymod.render;
import com.replaymod.core.ReplayMod; 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.crash.CrashReport;
import net.minecraft.util.ReportedException; import net.minecraft.util.ReportedException;
import net.minecraftforge.common.config.Configuration; import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.fml.common.FMLCommonHandler;
import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Mod(modid = ReplayModRender.MOD_ID, useMetadata = true) @Mod(modid = ReplayModRender.MOD_ID,
version = "@MOD_VERSION@",
acceptedMinecraftVersions = "@MC_VERSION@",
useMetadata = true)
public class ReplayModRender { public class ReplayModRender {
public static final String MOD_ID = "replaymod-render"; public static final String MOD_ID = "replaymod-render";
@Mod.Instance(MOD_ID) @Mod.Instance(MOD_ID)
public static ReplayModRender instance; public static ReplayModRender instance;
@Mod.Instance(ReplayMod.MOD_ID) private ReplayMod core;
private static ReplayMod core;
private Logger logger; public static Logger LOGGER;
private Configuration configuration; private Configuration configuration;
private final List<RenderJob> renderQueue = new ArrayList<>();
public ReplayMod getCore() { public ReplayMod getCore() {
return core; return core;
@@ -32,12 +41,20 @@ public class ReplayModRender {
@Mod.EventHandler @Mod.EventHandler
public void preInit(FMLPreInitializationEvent event) { public void preInit(FMLPreInitializationEvent event) {
logger = event.getModLog(); LOGGER = event.getModLog();
core = ReplayMod.instance;
configuration = new Configuration(event.getSuggestedConfigurationFile()); configuration = new Configuration(event.getSuggestedConfigurationFile());
FMLCommonHandler.instance().bus().register(this);
core.getSettingsRegistry().register(Setting.class); core.getSettingsRegistry().register(Setting.class);
} }
@SubscribeEvent
public void onReplayClose(ReplayCloseEvent.Post event) {
renderQueue.clear();
}
public File getVideoFolder() { public File getVideoFolder() {
String path = core.getSettingsRegistry().get(Setting.RENDER_PATH); 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("./") ? core.getMinecraft().mcDataDir : null, path);
@@ -52,4 +69,8 @@ public class ReplayModRender {
public Configuration getConfiguration() { public Configuration getConfiguration() {
return configuration; return configuration;
} }
public List<RenderJob> getRenderQueue() {
return renderQueue;
}
} }

View File

@@ -7,17 +7,27 @@ import com.replaymod.render.utils.StreamPipe;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.crash.CrashReport; import net.minecraft.crash.CrashReport;
import net.minecraft.crash.CrashReportCategory; import net.minecraft.crash.CrashReportCategory;
import net.minecraft.util.Util;
import net.minecraftforge.fml.common.versioning.ComparableVersion;
import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.CommandLine;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.TeeOutputStream; import org.apache.commons.io.output.TeeOutputStream;
import org.lwjgl.util.ReadableDimension; import org.lwjgl.util.ReadableDimension;
import java.io.*; import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.channels.Channels; import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel; import java.nio.channels.WritableByteChannel;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static com.replaymod.render.ReplayModRender.LOGGER;
import static org.apache.commons.lang3.Validate.isTrue; import static org.apache.commons.lang3.Validate.isTrue;
public class VideoWriter implements FrameConsumer<RGBFrame> { public class VideoWriter implements FrameConsumer<RGBFrame> {
@@ -43,10 +53,11 @@ public class VideoWriter implements FrameConsumer<RGBFrame> {
.replace("%HEIGHT%", String.valueOf(settings.getVideoHeight())) .replace("%HEIGHT%", String.valueOf(settings.getVideoHeight()))
.replace("%FPS%", String.valueOf(settings.getFramesPerSecond())) .replace("%FPS%", String.valueOf(settings.getFramesPerSecond()))
.replace("%FILENAME%", fileName) .replace("%FILENAME%", fileName)
.replace("%BITRATE%", String.valueOf(settings.getBitRate())); .replace("%BITRATE%", String.valueOf(settings.getBitRate()))
.replace("%FILTERS%", settings.getVideoFilters());
String executable = settings.getExportCommand().isEmpty() ? "ffmpeg" : settings.getExportCommand(); String executable = settings.getExportCommand().isEmpty() ? findFFmpeg() : settings.getExportCommand();
System.out.println("Starting " + settings.getExportCommand() + " with args: " + commandArgs); System.out.println("Starting " + executable + " with args: " + commandArgs);
String[] cmdline = new CommandLine(executable).addArguments(commandArgs).toStrings(); String[] cmdline = new CommandLine(executable).addArguments(commandArgs).toStrings();
process = new ProcessBuilder(cmdline).directory(outputFolder).start(); process = new ProcessBuilder(cmdline).directory(outputFolder).start();
File exportLogFile = new File(Minecraft.getMinecraft().mcDataDir, "export.log"); File exportLogFile = new File(Minecraft.getMinecraft().mcDataDir, "export.log");
@@ -57,6 +68,54 @@ public class VideoWriter implements FrameConsumer<RGBFrame> {
channel = Channels.newChannel(outputStream); 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<File> 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 @Override
public void close() throws IOException { public void close() throws IOException {
IOUtils.closeQuietly(outputStream); IOUtils.closeQuietly(outputStream);

View File

@@ -75,9 +75,9 @@ public class ODSFrameCapturer implements FrameCapturer<ODSOpenGlFrame> {
linkState(1, "lightMapEnabled"); linkState(1, "lightMapEnabled");
linkState(2, "hurtTextureEnabled"); linkState(2, "hurtTextureEnabled");
final Program.Uniform uniform = shaderProgram.getUniformVariable("fogEnabled"); final Program.Uniform uniform = shaderProgram.getUniformVariable("fogEnabled");
previousFogState = GlStateManager.fogState.fog; previousFogState = GlStateManager.fogState.field_179049_a;
uniform.set(previousFogState.currentState); uniform.set(previousFogState.currentState);
GlStateManager.fogState.fog = new BooleanState(previousFogState.capability) { GlStateManager.fogState.field_179049_a = new BooleanState(previousFogState.capability) {
@Override @Override
public void setState(boolean state) { public void setState(boolean state) {
super.setState(state); super.setState(state);
@@ -135,7 +135,7 @@ public class ODSFrameCapturer implements FrameCapturer<ODSOpenGlFrame> {
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
GlStateManager.textureState[i].texture2DState = previousStates[i]; GlStateManager.textureState[i].texture2DState = previousStates[i];
} }
GlStateManager.fogState.fog = previousFogState; GlStateManager.fogState.field_179049_a = previousFogState;
} }
private class CubicStereoFrameCapturer extends CubicPboOpenGlFrameCapturer { private class CubicStereoFrameCapturer extends CubicPboOpenGlFrameCapturer {

View File

@@ -0,0 +1,277 @@
package com.replaymod.render.gui;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.replaymod.render.ReplayModRender;
import com.replaymod.render.rendering.VideoRenderer;
import com.replaymod.render.utils.RenderJob;
import com.replaymod.replay.ReplayHandler;
import com.replaymod.replaystudio.pathing.path.Timeline;
import com.replaymod.replaystudio.util.I18n;
import de.johni0702.minecraft.gui.GuiRenderer;
import de.johni0702.minecraft.gui.RenderInfo;
import de.johni0702.minecraft.gui.container.AbstractGuiClickableContainer;
import de.johni0702.minecraft.gui.container.GuiContainer;
import de.johni0702.minecraft.gui.container.GuiPanel;
import de.johni0702.minecraft.gui.container.GuiVerticalList;
import de.johni0702.minecraft.gui.element.GuiButton;
import de.johni0702.minecraft.gui.element.GuiLabel;
import de.johni0702.minecraft.gui.element.GuiTextField;
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.popup.AbstractGuiPopup;
import de.johni0702.minecraft.gui.popup.GuiYesNoPopup;
import de.johni0702.minecraft.gui.utils.Colors;
import net.minecraft.client.gui.GuiErrorScreen;
import net.minecraft.crash.CrashReport;
import net.minecraft.util.ReportedException;
import org.lwjgl.util.Dimension;
import org.lwjgl.util.ReadableDimension;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.List;
import static com.replaymod.render.ReplayModRender.LOGGER;
public class GuiRenderQueue extends AbstractGuiPopup<GuiRenderQueue> {
private final GuiLabel title = new GuiLabel().setI18nText("replaymod.gui.renderqueue.title").setColor(Colors.BLACK);
private final GuiVerticalList list = new GuiVerticalList().setDrawShadow(true).setDrawSlider(true);
private final GuiButton addButton = new GuiButton().setI18nLabel("replaymod.gui.renderqueue.add").setSize(150, 20);
private final GuiButton renameButton = new GuiButton().setI18nLabel("replaymod.gui.rename").setSize(73, 20);
private final GuiButton removeButton = new GuiButton().setI18nLabel("replaymod.gui.remove").setSize(73, 20);
private final GuiButton renderButton = new GuiButton().setI18nLabel("replaymod.gui.render").setSize(150, 20);
private final GuiButton closeButton = new GuiButton().setI18nLabel("replaymod.gui.close").setSize(150, 20).onClick(this::close);
/*
|---------------------------------|
| Add | Render |
|---------------------------------|
| Rename | Remove | Close |
|---------------------------------|
*/
private final GuiPanel buttonPanel = new GuiPanel()
.setLayout(new GridLayout().setSpacingX(5).setSpacingY(5).setColumns(2))
.addElements(null,
addButton,
renderButton,
new GuiPanel().setLayout(new HorizontalLayout().setSpacing(4)).addElements(null,
renameButton, removeButton),
closeButton);
private final GuiContainer container;
private Entry selectedEntry;
{
popup.setLayout(new CustomLayout<GuiPanel>() {
@Override
protected void layout(GuiPanel container, int width, int height) {
pos(title, width / 2 - width(title) / 2, 0);
pos(list, 0, y(title) + height(title) + 5);
pos(buttonPanel, width / 2 - width(buttonPanel) / 2, height - height(buttonPanel));
size(list, width, y(buttonPanel) - y(list) - 10);
}
@Override
public ReadableDimension calcMinSize(GuiContainer<?> container) {
ReadableDimension screenSize = GuiRenderQueue.this.container.getMinSize();
return new Dimension(screenSize.getWidth() - 40,
screenSize.getHeight() - 20 - buttonPanel.getMinSize().getHeight() - title.getMinSize().getHeight());
}
}).addElements(null, title, list, buttonPanel);
}
public GuiRenderQueue(GuiContainer container, GuiRenderSettings guiRenderSettings, ReplayHandler replayHandler, Timeline timeline) {
super(container);
this.container = container;
LOGGER.trace("Opening render queue popup");
setBackgroundColor(Colors.DARK_TRANSPARENT);
List<RenderJob> queue = ReplayModRender.instance.getRenderQueue();
for (RenderJob renderJob : queue) {
LOGGER.trace("Adding {} to job queue list", renderJob);
list.getListPanel().addElements(null, new Entry(renderJob));
}
addButton.onClick(() -> {
LOGGER.trace("Add button clicked");
// Open popup
GuiYesNoPopup popup = GuiYesNoPopup.open(container)
.setYesI18nLabel("replaymod.gui.add").setNoI18nLabel("replaymod.gui.cancel");
popup.getInfo().setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(5));
// Add content
GuiLabel label = new GuiLabel().setI18nText("replaymod.gui.renderqueue.jobname").setColor(Colors.BLACK);
GuiTextField nameField = new GuiTextField().setSize(150, 20).setFocused(true);
popup.getInfo().addElements(new HorizontalLayout.Data(0.5), label, nameField);
// Disable "Yes" button while name is empty
nameField.onTextChanged(old -> popup.getYesButton().setEnabled(!nameField.getText().isEmpty())).onEnter(() -> {
if (popup.getYesButton().isEnabled()) {
popup.getYesButton().onClick();
}
});
popup.getYesButton().setDisabled();
// Register callback
Futures.addCallback(popup.getFuture(), new FutureCallback<Boolean>() {
@Override
public void onSuccess(@Nullable Boolean result) {
if (result == Boolean.TRUE) {
RenderJob newJob = new RenderJob();
newJob.setName(nameField.getText());
newJob.setSettings(guiRenderSettings.save(false));
newJob.setTimeline(timeline);
LOGGER.trace("Adding new job: {}", newJob);
queue.add(newJob);
list.getListPanel().addElements(null, new Entry(newJob));
} else {
LOGGER.trace("Adding cancelled");
}
}
@Override
public void onFailure(Throwable t) {
LOGGER.error("Add Job popup:", t);
}
});
});
if (guiRenderSettings != null) {
addButton.setEnabled(guiRenderSettings.renderButton.isEnabled());
}
renameButton.onClick(() -> {
LOGGER.trace("Rename button clicked for {}", selectedEntry.job);
// Open popup
GuiYesNoPopup popup = GuiYesNoPopup.open(container)
.setYesI18nLabel("replaymod.gui.rename").setNoI18nLabel("replaymod.gui.cancel");
popup.getInfo().setLayout(new HorizontalLayout(HorizontalLayout.Alignment.CENTER).setSpacing(5));
// Add content
GuiLabel label = new GuiLabel().setI18nText("replaymod.gui.renderqueue.jobname").setColor(Colors.BLACK);
GuiTextField nameField = new GuiTextField().setSize(150, 20).setFocused(true)
.setText(selectedEntry.job.getName());
popup.getInfo().addElements(new HorizontalLayout.Data(0.5), label, nameField);
// Disable "Yes" button while name is empty
nameField.onTextChanged(old -> popup.getYesButton().setEnabled(!nameField.getText().isEmpty())).onEnter(() -> {
if (popup.getYesButton().isEnabled()) {
popup.getYesButton().onClick();
}
});
// Register callback
Futures.addCallback(popup.getFuture(), new FutureCallback<Boolean>() {
@Override
public void onSuccess(@Nullable Boolean result) {
if (result == Boolean.TRUE) {
LOGGER.trace("Renaming {} to \"{}\"", selectedEntry.job, nameField.getText());
selectedEntry.setName(nameField.getText());
} else {
LOGGER.trace("Renaming cancelled");
}
}
@Override
public void onFailure(Throwable t) {
LOGGER.error("Rename Job popup:", t);
}
});
});
removeButton.onClick(() -> {
LOGGER.trace("Remove button clicked for {}", selectedEntry.job);
list.getListPanel().removeElement(selectedEntry);
queue.remove(selectedEntry.job);
selectedEntry = null;
updateButtons();
});
renderButton.onClick(() -> {
LOGGER.trace("Render button clicked");
// Close all GUIs (so settings in GuiRenderSettings are saved)
getMinecraft().displayGuiScreen(null);
// Start rendering
for (RenderJob renderJob : queue) {
LOGGER.info("Starting render job {}", renderJob);
try {
VideoRenderer videoRenderer = new VideoRenderer(renderJob.getSettings(), replayHandler, renderJob.getTimeline());
videoRenderer.renderVideo();
} catch (IOException e) {
LOGGER.error("Rendering video:", e);
GuiErrorScreen errorScreen = new GuiErrorScreen(I18n.format("replaymod.gui.rendering.error.title"),
I18n.format("replaymod.gui.rendering.error.message"));
getMinecraft().displayGuiScreen(errorScreen);
return;
} catch (Throwable t) {
CrashReport crashReport = CrashReport.makeCrashReport(t, "Rendering video");
throw new ReportedException(crashReport);
}
}
});
updateButtons();
}
@Override
public void open() {
super.open();
}
@Override
protected GuiRenderQueue getThis() {
return this;
}
public void updateButtons() {
renameButton.setEnabled(selectedEntry != null);
removeButton.setEnabled(selectedEntry != null);
renderButton.setEnabled(!list.getListPanel().getChildren().isEmpty());
}
public class Entry extends AbstractGuiClickableContainer<Entry> {
public final GuiLabel label = new GuiLabel(this);
public final RenderJob job;
public Entry(RenderJob job) {
this.job = job;
setLayout(new CustomLayout<Entry>() {
@Override
protected void layout(Entry container, int width, int height) {
pos(label, 5, height / 2 - height(label) / 2);
}
@Override
public ReadableDimension calcMinSize(GuiContainer<?> container) {
return new Dimension(buttonPanel.calcMinSize().getWidth(), 16);
}
});
label.setText(job.getName());
}
@Override
protected void onClick() {
selectedEntry = this;
updateButtons();
}
@Override
public void draw(GuiRenderer renderer, ReadableDimension size, RenderInfo renderInfo) {
if (selectedEntry == this) {
renderer.drawRect(0, 0, size.getWidth(), size.getHeight(), Colors.BLACK);
renderer.drawRect(0, 0, 2, size.getHeight(), Colors.WHITE);
}
super.draw(renderer, size, renderInfo);
}
public void setName(String name) {
job.setName(name);
label.setText(name);
}
@Override
protected Entry getThis() {
return this;
}
}
}

View File

@@ -32,13 +32,14 @@ import net.minecraft.crash.CrashReport;
import net.minecraft.util.ReportedException; import net.minecraft.util.ReportedException;
import net.minecraftforge.common.config.Configuration; import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.common.config.Property; import net.minecraftforge.common.config.Property;
import org.lwjgl.util.Color;
import org.lwjgl.util.Dimension; import org.lwjgl.util.Dimension;
import org.lwjgl.util.ReadableColor;
import org.lwjgl.util.ReadableDimension; import org.lwjgl.util.ReadableDimension;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Type;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Map; import java.util.Map;
@@ -79,8 +80,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
} }
}).setMinSize(new Dimension(0, 20)).setValues(RenderSettings.EncodingPreset.values()); }).setMinSize(new Dimension(0, 20)).setValues(RenderSettings.EncodingPreset.values());
public final GuiNumberField videoWidth = new GuiNumberField().setSize(50, 20).setMinValue(1); public final GuiNumberField videoWidth = new GuiNumberField().setSize(50, 20).setMinValue(1).setValidateOnFocusChange(true);
public final GuiNumberField videoHeight = new GuiNumberField().setSize(50, 20).setMinValue(1); public final GuiNumberField videoHeight = new GuiNumberField().setSize(50, 20).setMinValue(1).setValidateOnFocusChange(true);
public final GuiSlider frameRateSlider = new GuiSlider().onValueChanged(new Runnable() { public final GuiSlider frameRateSlider = new GuiSlider().onValueChanged(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -92,7 +93,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
.setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(2)) .setLayout(new HorizontalLayout(HorizontalLayout.Alignment.RIGHT).setSpacing(2))
.addElements(new HorizontalLayout.Data(0.5), videoWidth, new GuiLabel().setText("*"), videoHeight); .addElements(new HorizontalLayout.Data(0.5), videoWidth, new GuiLabel().setText("*"), videoHeight);
public final GuiNumberField bitRateField = new GuiNumberField().setValue(10).setSize(50, 20); public final GuiNumberField bitRateField = new GuiNumberField().setValue(10).setSize(50, 20).setValidateOnFocusChange(true);
public final GuiDropdownMenu<String> bitRateUnit = new GuiDropdownMenu<String>() public final GuiDropdownMenu<String> bitRateUnit = new GuiDropdownMenu<String>()
.setSize(50, 20).setValues("bps", "kbps", "mbps").setSelected("mbps"); .setSize(50, 20).setValues("bps", "kbps", "mbps").setSelected("mbps");
@@ -149,13 +150,18 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
public final GuiCheckbox inject360Metadata = new GuiCheckbox() public final GuiCheckbox inject360Metadata = new GuiCheckbox()
.setI18nLabel("replaymod.gui.rendersettings.360metadata"); .setI18nLabel("replaymod.gui.rendersettings.360metadata");
public final GuiDropdownMenu<RenderSettings.AntiAliasing> antiAliasingDropdown = new GuiDropdownMenu<RenderSettings.AntiAliasing>()
.setSize(200, 20).setValues(RenderSettings.AntiAliasing.values()).setSelected(RenderSettings.AntiAliasing.NONE);
public final GuiPanel advancedPanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(15)) public final GuiPanel advancedPanel = new GuiPanel().setLayout(new VerticalLayout().setSpacing(15))
.addElements(null, nametagCheckbox, new GuiPanel().setLayout( .addElements(null, nametagCheckbox, new GuiPanel().setLayout(
new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(5).setSpacingY(15)) new GridLayout().setCellsEqualSize(false).setColumns(2).setSpacingX(5).setSpacingY(15))
.addElements(new GridLayout.Data(0, 0.5), .addElements(new GridLayout.Data(0, 0.5),
new GuiLabel().setI18nText("replaymod.gui.rendersettings.stabilizecamera"), stabilizePanel, new GuiLabel().setI18nText("replaymod.gui.rendersettings.stabilizecamera"), stabilizePanel,
chromaKeyingCheckbox, chromaKeyingColor, chromaKeyingCheckbox, chromaKeyingColor,
inject360Metadata)); inject360Metadata,
new GuiLabel(), // to show the anti-aliasing options in a new line
new GuiLabel().setI18nText("replaymod.gui.rendersettings.antialiasing"), antiAliasingDropdown));
public final GuiTextField exportCommand = new GuiTextField().setI18nHint("replaymod.gui.rendersettings.command") public final GuiTextField exportCommand = new GuiTextField().setI18nHint("replaymod.gui.rendersettings.command")
.setSize(55, 20).setMaxLength(100); .setSize(55, 20).setMaxLength(100);
@@ -179,6 +185,12 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
public final GuiPanel buttonPanel = new GuiPanel(contentPanel).setLayout(new HorizontalLayout().setSpacing(4)); public final GuiPanel buttonPanel = new GuiPanel(contentPanel).setLayout(new HorizontalLayout().setSpacing(4));
public final GuiButton queueButton = new GuiButton(buttonPanel).onClick(new Runnable() {
@Override
public void run() {
new GuiRenderQueue(GuiRenderSettings.this, GuiRenderSettings.this, replayHandler, timeline).open();
}
}).setSize(100, 20).setI18nLabel("replaymod.gui.renderqueue.open");
public final GuiButton renderButton = new GuiButton(buttonPanel).onClick(new Runnable() { public final GuiButton renderButton = new GuiButton(buttonPanel).onClick(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -198,13 +210,13 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
throw new ReportedException(crashReport); throw new ReportedException(crashReport);
} }
} }
}).setSize(148, 20).setI18nLabel("replaymod.gui.render"); }).setSize(100, 20).setI18nLabel("replaymod.gui.render");
public final GuiButton cancelButton = new GuiButton(buttonPanel).onClick(new Runnable() { public final GuiButton cancelButton = new GuiButton(buttonPanel).onClick(new Runnable() {
@Override @Override
public void run() { public void run() {
getMinecraft().displayGuiScreen(null); getMinecraft().displayGuiScreen(null);
} }
}).setSize(148, 20).setI18nLabel("replaymod.gui.cancel"); }).setSize(100, 20).setI18nLabel("replaymod.gui.cancel");
{ {
setBackground(Background.NONE); setBackground(Background.NONE);
@@ -261,12 +273,10 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
this.timeline = timeline; this.timeline = timeline;
String json = getConfigProperty(ReplayModRender.instance.getConfiguration()).getString(); String json = getConfigProperty(ReplayModRender.instance.getConfiguration()).getString();
RenderSettings settings = new GsonBuilder().registerTypeAdapter(RenderSettings.class, new InstanceCreator<RenderSettings>() { RenderSettings settings = new GsonBuilder()
@Override .registerTypeAdapter(RenderSettings.class, (InstanceCreator<RenderSettings>) type -> getDefaultRenderSettings())
public RenderSettings createInstance(Type type) { .registerTypeAdapter(ReadableColor.class, new Gson().getAdapter(Color.class))
return getDefaultRenderSettings(); .create().fromJson(json, RenderSettings.class);
}
}).create().fromJson(json, RenderSettings.class);
load(settings); load(settings);
} }
@@ -347,8 +357,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
public void load(RenderSettings settings) { public void load(RenderSettings settings) {
renderMethodDropdown.setSelected(settings.getRenderMethod()); renderMethodDropdown.setSelected(settings.getRenderMethod());
encodingPresetDropdown.setSelected(settings.getEncodingPreset()); encodingPresetDropdown.setSelected(settings.getEncodingPreset());
videoWidth.setValue(settings.getVideoWidth()); videoWidth.setValue(settings.getTargetVideoWidth());
videoHeight.setValue(settings.getVideoHeight()); videoHeight.setValue(settings.getTargetVideoHeight());
frameRateSlider.setValue(settings.getFramesPerSecond() - 10); frameRateSlider.setValue(settings.getFramesPerSecond() - 10);
if (settings.getBitRate() % (1 << 20) == 0) { if (settings.getBitRate() % (1 << 20) == 0) {
bitRateField.setValue(settings.getBitRate() >> 20); bitRateField.setValue(settings.getBitRate() >> 20);
@@ -380,6 +390,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
chromaKeyingColor.setColor(settings.getChromaKeyingColor()); chromaKeyingColor.setColor(settings.getChromaKeyingColor());
} }
inject360Metadata.setChecked(settings.isInject360Metadata()); inject360Metadata.setChecked(settings.isInject360Metadata());
antiAliasingDropdown.setSelected(settings.getAntiAliasing());
exportCommand.setText(settings.getExportCommand()); exportCommand.setText(settings.getExportCommand());
exportArguments.setText(settings.getExportArguments()); exportArguments.setText(settings.getExportArguments());
@@ -401,6 +412,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
stabilizeRoll.isChecked() && (serialize || stabilizeRoll.isEnabled()), stabilizeRoll.isChecked() && (serialize || stabilizeRoll.isEnabled()),
chromaKeyingCheckbox.isChecked() ? chromaKeyingColor.getColor() : null, chromaKeyingCheckbox.isChecked() ? chromaKeyingColor.getColor() : null,
inject360Metadata.isChecked() && (serialize || inject360Metadata.isEnabled()), inject360Metadata.isChecked() && (serialize || inject360Metadata.isEnabled()),
antiAliasingDropdown.getSelectedValue(),
exportCommand.getText(), exportCommand.getText(),
exportArguments.getText(), exportArguments.getText(),
net.minecraft.client.gui.GuiScreen.isCtrlKeyDown() net.minecraft.client.gui.GuiScreen.isCtrlKeyDown()
@@ -415,7 +427,7 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
private RenderSettings getDefaultRenderSettings() { private RenderSettings getDefaultRenderSettings() {
return new RenderSettings(RenderSettings.RenderMethod.DEFAULT, RenderSettings.EncodingPreset.MP4_DEFAULT, 1920, 1080, 60, 10 << 20, null, return new RenderSettings(RenderSettings.RenderMethod.DEFAULT, RenderSettings.EncodingPreset.MP4_DEFAULT, 1920, 1080, 60, 10 << 20, null,
true, false, false, false, null, false, "", RenderSettings.EncodingPreset.MP4_DEFAULT.getValue(), false); true, false, false, false, null, false, RenderSettings.AntiAliasing.NONE, "", RenderSettings.EncodingPreset.MP4_DEFAULT.getValue(), false);
} }
@Override @Override
@@ -431,4 +443,8 @@ public class GuiRenderSettings extends GuiScreen implements Closeable {
return configuration.get("rendersettings", "settings", "{}", return configuration.get("rendersettings", "settings", "{}",
"Last state of the render settings GUI. Internal use only."); "Last state of the render settings GUI. Internal use only.");
} }
public ReplayHandler getReplayHandler() {
return replayHandler;
}
} }

View File

@@ -10,13 +10,13 @@ import net.minecraft.client.renderer.chunk.RenderChunk;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.Iterator; import java.util.Iterator;
import java.util.concurrent.PriorityBlockingQueue; import java.util.concurrent.BlockingQueue;
public class ChunkLoadingRenderGlobal { public class ChunkLoadingRenderGlobal {
private final RenderGlobal hooked; private final RenderGlobal hooked;
private final ChunkRenderDispatcher renderDispatcher; private final ChunkRenderDispatcher renderDispatcher;
private final JailingQueue<ChunkCompileTaskGenerator> workerJailingQueue; private final JailingQueue<?> workerJailingQueue;
private final CustomChunkRenderWorker renderWorker; private final CustomChunkRenderWorker renderWorker;
private int frame; private int frame;
@@ -27,10 +27,10 @@ public class ChunkLoadingRenderGlobal {
this.renderWorker = new CustomChunkRenderWorker(renderDispatcher, new RegionRenderCacheBuilder()); this.renderWorker = new CustomChunkRenderWorker(renderDispatcher, new RegionRenderCacheBuilder());
int workerThreads = renderDispatcher.listThreadedWorkers.size(); int workerThreads = renderDispatcher.listThreadedWorkers.size();
PriorityBlockingQueue<ChunkCompileTaskGenerator> queueChunkUpdates = renderDispatcher.queueChunkUpdates; BlockingQueue<Object> queueChunkUpdates = renderDispatcher.queueChunkUpdates;
workerJailingQueue = new JailingQueue<>(queueChunkUpdates); workerJailingQueue = new JailingQueue<>(queueChunkUpdates);
renderDispatcher.queueChunkUpdates = workerJailingQueue; renderDispatcher.queueChunkUpdates = workerJailingQueue;
ChunkCompileTaskGenerator element = new ChunkCompileTaskGenerator(null, null, 0); ChunkCompileTaskGenerator element = new ChunkCompileTaskGenerator(null, null);
element.finish(); element.finish();
for (int i = 0; i < workerThreads; i++) { for (int i = 0; i < workerThreads; i++) {
queueChunkUpdates.add(element); queueChunkUpdates.add(element);
@@ -51,6 +51,7 @@ public class ChunkLoadingRenderGlobal {
} }
} }
@SuppressWarnings("unchecked")
public void updateChunks() { public void updateChunks() {
while (renderDispatcher.runChunkUploads(0)) { while (renderDispatcher.runChunkUploads(0)) {
hooked.displayListEntitiesDirty = true; hooked.displayListEntitiesDirty = true;
@@ -58,7 +59,7 @@ public class ChunkLoadingRenderGlobal {
while (!renderDispatcher.queueChunkUpdates.isEmpty()) { while (!renderDispatcher.queueChunkUpdates.isEmpty()) {
try { try {
renderWorker.processTask(renderDispatcher.queueChunkUpdates.poll()); renderWorker.processTask((ChunkCompileTaskGenerator) renderDispatcher.queueChunkUpdates.poll());
} catch (InterruptedException ignored) { } } catch (InterruptedException ignored) { }
} }
@@ -68,7 +69,7 @@ public class ChunkLoadingRenderGlobal {
renderDispatcher.updateChunkNow(renderchunk); renderDispatcher.updateChunkNow(renderchunk);
renderchunk.clearNeedsUpdate(); renderchunk.setNeedsUpdate(false);
iterator.remove(); iterator.remove();
} }
} }

View File

@@ -0,0 +1,47 @@
package com.replaymod.render.mixin;
import com.replaymod.render.hooks.EntityRendererHandler;
import net.minecraft.client.Minecraft;
import net.minecraft.client.particle.EffectRenderer;
import net.minecraft.client.particle.EntityFX;
import net.minecraft.client.renderer.WorldRenderer;
import net.minecraft.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(EffectRenderer.class)
public abstract class MixinEffectRenderer {
@Redirect(method = "renderParticles", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/EntityFX;func_180434_a(Lnet/minecraft/client/renderer/WorldRenderer;Lnet/minecraft/entity/Entity;FFFFFF)V"))
private void renderNormalParticle(EntityFX fx, WorldRenderer worldRenderer, Entity view, float partialTicks,
float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
renderParticle(fx, worldRenderer, view, partialTicks, rotX, rotXZ, rotZ, rotYZ, rotXY);
}
@Redirect(method = "renderLitParticles", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/EntityFX;func_180434_a(Lnet/minecraft/client/renderer/WorldRenderer;Lnet/minecraft/entity/Entity;FFFFFF)V"))
private void renderLitParticle(EntityFX fx, WorldRenderer worldRenderer, Entity view, float partialTicks,
float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
renderParticle(fx, worldRenderer, view, partialTicks, rotX, rotXZ, rotZ, rotYZ, rotXY);
}
private void renderParticle(EntityFX fx, WorldRenderer worldRenderer, Entity view, float partialTicks,
float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) Minecraft.getMinecraft().entityRenderer).replayModRender_getHandler();
if (handler != null && handler.omnidirectional) {
// Align all particles towards the camera
double dx = fx.prevPosX + (fx.posX - fx.prevPosX) * partialTicks - view.posX;
double dy = fx.prevPosY + (fx.posY - fx.prevPosY) * partialTicks - view.posY;
double dz = fx.prevPosZ + (fx.posZ - fx.prevPosZ) * partialTicks - view.posZ;
double pitch = -Math.atan2(dy, Math.sqrt(dx * dx + dz * dz));
double yaw = -Math.atan2(dx, dz);
rotX = (float) Math.cos(yaw);
rotZ = (float) Math.sin(yaw);
rotXZ = (float) Math.cos(pitch);
rotYZ = (float) (-rotZ * Math.sin(pitch));
rotXY = (float) (rotX * Math.sin(pitch));
}
fx.func_180434_a(worldRenderer, view, partialTicks, rotX, rotXZ, rotZ, rotYZ, rotXY);
}
}

View File

@@ -12,7 +12,7 @@ import net.minecraft.client.renderer.RenderGlobal;
import net.minecraft.client.settings.GameSettings; import net.minecraft.client.settings.GameSettings;
import net.minecraft.entity.Entity; import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.math.RayTraceResult; import net.minecraft.util.MovingObjectPosition;
import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL11;
import org.lwjgl.util.ReadableColor; import org.lwjgl.util.ReadableColor;
import org.lwjgl.util.glu.Project; import org.lwjgl.util.glu.Project;
@@ -118,10 +118,10 @@ public abstract class MixinEntityRenderer implements EntityRendererHandler.IEnti
@Shadow @Shadow
public abstract void renderHand(float partialTicks, int renderPass); public abstract void renderHand(float partialTicks, int renderPass);
@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")) @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"))
private void replayModRender_drawSelectionBox(RenderGlobal instance, EntityPlayer player, RayTraceResult rtr, int alwaysZero, float partialTicks) { private void replayModRender_drawSelectionBox(RenderGlobal instance, EntityPlayer player, MovingObjectPosition mop, int alwaysZero, float partialTicks) {
if (replayModRender_handler == null) { if (replayModRender_handler == null) {
instance.drawSelectionBox(player, rtr, alwaysZero, partialTicks); instance.drawSelectionBox(player, mop, alwaysZero, partialTicks);
} }
} }

View File

@@ -1,47 +0,0 @@
package com.replaymod.render.mixin;
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.client.renderer.VertexBuffer;
import net.minecraft.entity.Entity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ParticleManager.class)
public abstract class MixinParticleManager {
@Redirect(method = "renderParticles", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/Particle;renderParticle(Lnet/minecraft/client/renderer/VertexBuffer;Lnet/minecraft/entity/Entity;FFFFFF)V"))
private void renderNormalParticle(Particle particle, VertexBuffer vertexBuffer, Entity view, float partialTicks,
float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
renderParticle(particle, vertexBuffer, view, partialTicks, rotX, rotXZ, rotZ, rotYZ, rotXY);
}
@Redirect(method = "renderLitParticles", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/Particle;renderParticle(Lnet/minecraft/client/renderer/VertexBuffer;Lnet/minecraft/entity/Entity;FFFFFF)V"))
private void renderLitParticle(Particle particle, VertexBuffer vertexBuffer, Entity view, float partialTicks,
float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
renderParticle(particle, vertexBuffer, view, partialTicks, rotX, rotXZ, rotZ, rotYZ, rotXY);
}
private void renderParticle(Particle particle, VertexBuffer vertexBuffer, Entity view, float partialTicks,
float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) Minecraft.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;
double dy = particle.prevPosY + (particle.posY - particle.prevPosY) * partialTicks - view.posY;
double dz = particle.prevPosZ + (particle.posZ - particle.prevPosZ) * partialTicks - view.posZ;
double pitch = -Math.atan2(dy, Math.sqrt(dx * dx + dz * dz));
double yaw = -Math.atan2(dx, dz);
rotX = (float) Math.cos(yaw);
rotZ = (float) Math.sin(yaw);
rotXZ = (float) Math.cos(pitch);
rotYZ = (float) (-rotZ * Math.sin(pitch));
rotXY = (float) (rotX * Math.sin(pitch));
}
particle.renderParticle(vertexBuffer, view, partialTicks, rotX, rotXZ, rotZ, rotYZ, rotXY);
}
}

View File

@@ -7,14 +7,15 @@ import net.minecraft.entity.Entity;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(Render.class) @Mixin(Render.class)
public abstract class MixinRender { public abstract class MixinRender {
@Inject(method = "renderLivingLabel", at = @At("HEAD"), cancellable = true) @Inject(method = "canRenderName", at = @At("HEAD"), cancellable = true)
private void replayModRender_areAllNamesHidden(Entity entityIn, String str, double x, double y, double z, int maxDistance, CallbackInfo ci) { private void replayModRender_areAllNamesHidden(Entity entity, CallbackInfoReturnable<Boolean> ci) {
EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) Minecraft.getMinecraft().entityRenderer).replayModRender_getHandler(); EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) Minecraft.getMinecraft().entityRenderer).replayModRender_getHandler();
if (handler != null && !handler.getSettings().isRenderNameTags()) { if (handler != null && !handler.getSettings().isRenderNameTags()) {
ci.setReturnValue(false);
ci.cancel(); ci.cancel();
} }
} }

View File

@@ -3,13 +3,16 @@ package com.replaymod.render.mixin;
import com.replaymod.render.hooks.ChunkLoadingRenderGlobal; import com.replaymod.render.hooks.ChunkLoadingRenderGlobal;
import net.minecraft.client.renderer.RenderGlobal; import net.minecraft.client.renderer.RenderGlobal;
import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher; import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher;
import net.minecraft.client.renderer.chunk.RenderChunk;
import net.minecraft.client.renderer.culling.ICamera; import net.minecraft.client.renderer.culling.ICamera;
import net.minecraft.entity.Entity; import net.minecraft.entity.Entity;
import net.minecraft.util.BlockPos;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(RenderGlobal.class) @Mixin(RenderGlobal.class)
public abstract class MixinRenderGlobal { public abstract class MixinRenderGlobal {
@@ -34,8 +37,6 @@ public abstract class MixinRenderGlobal {
do { do {
setupTerrain(viewEntity, partialTicks, camera, replayModRender_hook.nextFrameId(), playerSpectator); setupTerrain(viewEntity, partialTicks, camera, replayModRender_hook.nextFrameId(), playerSpectator);
replayModRender_hook.updateChunks();
} while (displayListEntitiesDirty); } while (displayListEntitiesDirty);
displayListEntitiesDirty = true; displayListEntitiesDirty = true;
@@ -45,6 +46,13 @@ public abstract class MixinRenderGlobal {
} }
} }
@Inject(method = "isPositionInRenderChunk", at = @At("HEAD"), cancellable = true)
public void replayModRender_isPositionInRenderChunk(BlockPos pos, RenderChunk chunk, CallbackInfoReturnable<Boolean> ci) {
if (replayModRender_hook != null) {
ci.setReturnValue(true);
}
}
@Inject(method = "updateChunks", at = @At("HEAD"), cancellable = true) @Inject(method = "updateChunks", at = @At("HEAD"), cancellable = true)
public void replayModRender_updateChunks(long finishTimeNano, CallbackInfo ci) { public void replayModRender_updateChunks(long finishTimeNano, CallbackInfo ci) {
if (replayModRender_hook != null) { if (replayModRender_hook != null) {

View File

@@ -8,7 +8,7 @@ import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(RenderManager.class) @Mixin(RenderManager.class)
public abstract class MixinRenderManager { public abstract class MixinRenderManager {
@@ -19,7 +19,7 @@ public abstract class MixinRenderManager {
private float playerViewY; private float playerViewY;
@Inject(method = "doRenderEntity", at = @At("HEAD")) @Inject(method = "doRenderEntity", at = @At("HEAD"))
private void replayModRender_reorientForCubicRendering(Entity entity, double dx, double dy, double dz, float iDoNotKnow, float partialTicks, boolean iDoNotCare, CallbackInfo ci) { private void replayModRender_reorientForCubicRendering(Entity entity, double dx, double dy, double dz, float iDoNotKnow, float partialTicks, boolean iDoNotCare, CallbackInfoReturnable ci) {
EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) Minecraft.getMinecraft().entityRenderer).replayModRender_getHandler(); EntityRendererHandler handler = ((EntityRendererHandler.IEntityRenderer) Minecraft.getMinecraft().entityRenderer).replayModRender_getHandler();
if (handler != null && handler.omnidirectional) { if (handler != null && handler.omnidirectional) {
double pitch = -Math.atan2(dy, Math.sqrt(dx * dx + dz * dz)); double pitch = -Math.atan2(dy, Math.sqrt(dx * dx + dz * dz));

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