Compare commits

...

94 Commits

Author SHA1 Message Date
Jonas Herzig
6109960d4a Fix integration test failing because of conflicting keybindings 2017-06-15 20:44:10 +02:00
Jonas Herzig
4dcecc5697 Do not show OpenEye offer gui if there's no OpenEye for the current MC version 2017-06-15 16:28:44 +02:00
Jonas Herzig
bef5e41a8d Update mappings 2017-06-15 16:15:01 +02:00
Jonas Herzig
6ecb87e8dd Add workaround for race condition preventing us from joining SP (see MinecraftForge/MinecraftForge#3974)
This is done by preventing all calls to ChannelConfig.setAutoRead(false) during
the call to FMLNetworkHandler.fmlClientHandshake (the two @Redirects in
MixinNetworkDispatcher) and manually issuing the call before handleLoginSuccess
returns (at which point the NetHandlerPlayClient has been set).

This commit should be reverted once the issue mentioned above has been fixed.
2017-06-15 16:15:01 +02:00
Jonas Herzig
921c9e0098 Update to 1.12 2017-06-15 16:15:01 +02:00
Jonas Herzig
5dd31f4cac Merge branch '1.11' into 1.11.2
2a47f57 Merge branch '1.10.2' into 1.11
96b2904 Merge branch '1.9.4' into 1.10.2
6c8c779 Merge branch '1.8.9' into 1.9.4
aa16e42 Add workaround for setupCIWorkspace not being sufficient
ed56673 Merge branch '1.8' into 1.8.9
91573d9 Update jGui, ReplayStudio and Translations
d71358b Add confirmation dialog before overwriting path presets (fixes #65)
1a55983 Move addCallback method from integration test Utils to main Utils
18c5bcd Be more cooperative with other mods on the ingame menu (fixes #42)
c054fe8 Register keys for simplepathing only once (fixes #63)
f13297c Fix default interpolator handling in keyframe gui and after loading (fixes #64)
391f304 Show error popup if entity tracker fails to load
0e0eaaa Fix entity tracker being set even if it failed loading (fixes #50)
a34bbbc Fix NPE when spectating a player without a camera entity
60879fb Change local class to be an inner class because Srg2Source breaks with it
dfafbec Load translations from github repo, only reload language not all resource packs
8a6ec51 Add missing tooltips to player overview and various missing chat messages
5677fc5 Re-add warning to replay editor
748a91e Fix missing tooltips on fav/like/dislike buttons in replay center
ee24866 Fix upload replay button not being updated when name/tags change
dba085c Move translations into separate repo
3f0e3e7 Fix NPE when receiving Replay|Restrict messages (fixes #16 GH)
40e1d85 Fix crash when saving the last keyframe in the edit gui (fixes #61)
03aada1 Update Mixin to 0.6.8 (fixes #9 GH)
0c1dc65 Fix interpolator being lost when moving keyframe to the end (fixes #62)
fcbbbc9 Add integration test. Run with ./gradlew runIntegrationTest
fe6ded0 Fix movement of keyframe via GuiEditKeyframe not updating selected keyframe
f58fa8f Fix Login GUI not respecting other GUIs opened on startup
4fc3a31 Fix OpenEye being installed into working dir instead of mcDataDir
2017-06-01 15:32:37 +02:00
Jonas Herzig
2a47f579a9 Merge branch '1.10.2' into 1.11
96b2904 Merge branch '1.9.4' into 1.10.2
6c8c779 Merge branch '1.8.9' into 1.9.4
aa16e42 Add workaround for setupCIWorkspace not being sufficient
ed56673 Merge branch '1.8' into 1.8.9
91573d9 Update jGui, ReplayStudio and Translations
d71358b Add confirmation dialog before overwriting path presets (fixes #65)
1a55983 Move addCallback method from integration test Utils to main Utils
18c5bcd Be more cooperative with other mods on the ingame menu (fixes #42)
c054fe8 Register keys for simplepathing only once (fixes #63)
f13297c Fix default interpolator handling in keyframe gui and after loading (fixes #64)
391f304 Show error popup if entity tracker fails to load
0e0eaaa Fix entity tracker being set even if it failed loading (fixes #50)
a34bbbc Fix NPE when spectating a player without a camera entity
60879fb Change local class to be an inner class because Srg2Source breaks with it
dfafbec Load translations from github repo, only reload language not all resource packs
8a6ec51 Add missing tooltips to player overview and various missing chat messages
5677fc5 Re-add warning to replay editor
748a91e Fix missing tooltips on fav/like/dislike buttons in replay center
ee24866 Fix upload replay button not being updated when name/tags change
dba085c Move translations into separate repo
3f0e3e7 Fix NPE when receiving Replay|Restrict messages (fixes #16 GH)
40e1d85 Fix crash when saving the last keyframe in the edit gui (fixes #61)
03aada1 Update Mixin to 0.6.8 (fixes #9 GH)
0c1dc65 Fix interpolator being lost when moving keyframe to the end (fixes #62)
fcbbbc9 Add integration test. Run with ./gradlew runIntegrationTest
fe6ded0 Fix movement of keyframe via GuiEditKeyframe not updating selected keyframe
f58fa8f Fix Login GUI not respecting other GUIs opened on startup
4fc3a31 Fix OpenEye being installed into working dir instead of mcDataDir
2017-05-31 21:18:22 +02:00
Jonas Herzig
96b2904e4b Merge branch '1.9.4' into 1.10.2
6c8c779 Merge branch '1.8.9' into 1.9.4
aa16e42 Add workaround for setupCIWorkspace not being sufficient
ed56673 Merge branch '1.8' into 1.8.9
91573d9 Update jGui, ReplayStudio and Translations
d71358b Add confirmation dialog before overwriting path presets (fixes #65)
1a55983 Move addCallback method from integration test Utils to main Utils
18c5bcd Be more cooperative with other mods on the ingame menu (fixes #42)
c054fe8 Register keys for simplepathing only once (fixes #63)
f13297c Fix default interpolator handling in keyframe gui and after loading (fixes #64)
391f304 Show error popup if entity tracker fails to load
0e0eaaa Fix entity tracker being set even if it failed loading (fixes #50)
a34bbbc Fix NPE when spectating a player without a camera entity
60879fb Change local class to be an inner class because Srg2Source breaks with it
dfafbec Load translations from github repo, only reload language not all resource packs
8a6ec51 Add missing tooltips to player overview and various missing chat messages
5677fc5 Re-add warning to replay editor
748a91e Fix missing tooltips on fav/like/dislike buttons in replay center
ee24866 Fix upload replay button not being updated when name/tags change
dba085c Move translations into separate repo
3f0e3e7 Fix NPE when receiving Replay|Restrict messages (fixes #16 GH)
40e1d85 Fix crash when saving the last keyframe in the edit gui (fixes #61)
03aada1 Update Mixin to 0.6.8 (fixes #9 GH)
0c1dc65 Fix interpolator being lost when moving keyframe to the end (fixes #62)
fcbbbc9 Add integration test. Run with ./gradlew runIntegrationTest
fe6ded0 Fix movement of keyframe via GuiEditKeyframe not updating selected keyframe
f58fa8f Fix Login GUI not respecting other GUIs opened on startup
4fc3a31 Fix OpenEye being installed into working dir instead of mcDataDir
2017-05-31 21:10:35 +02:00
Jonas Herzig
6c8c779983 Merge branch '1.8.9' into 1.9.4
ed56673 Merge branch '1.8' into 1.8.9
91573d9 Update jGui, ReplayStudio and Translations
d71358b Add confirmation dialog before overwriting path presets (fixes #65)
1a55983 Move addCallback method from integration test Utils to main Utils
18c5bcd Be more cooperative with other mods on the ingame menu (fixes #42)
c054fe8 Register keys for simplepathing only once (fixes #63)
f13297c Fix default interpolator handling in keyframe gui and after loading (fixes #64)
391f304 Show error popup if entity tracker fails to load
0e0eaaa Fix entity tracker being set even if it failed loading (fixes #50)
a34bbbc Fix NPE when spectating a player without a camera entity
60879fb Change local class to be an inner class because Srg2Source breaks with it
dfafbec Load translations from github repo, only reload language not all resource packs
8a6ec51 Add missing tooltips to player overview and various missing chat messages
5677fc5 Re-add warning to replay editor
748a91e Fix missing tooltips on fav/like/dislike buttons in replay center
ee24866 Fix upload replay button not being updated when name/tags change
dba085c Move translations into separate repo
3f0e3e7 Fix NPE when receiving Replay|Restrict messages (fixes #16 GH)
40e1d85 Fix crash when saving the last keyframe in the edit gui (fixes #61)
03aada1 Update Mixin to 0.6.8 (fixes #9 GH)
0c1dc65 Fix interpolator being lost when moving keyframe to the end (fixes #62)
fcbbbc9 Add integration test. Run with ./gradlew runIntegrationTest
fe6ded0 Fix movement of keyframe via GuiEditKeyframe not updating selected keyframe
f58fa8f Fix Login GUI not respecting other GUIs opened on startup
4fc3a31 Fix OpenEye being installed into working dir instead of mcDataDir
2017-05-31 20:49:04 +02:00
Jonas Herzig
aa16e42a4f Add workaround for setupCIWorkspace not being sufficient 2017-05-31 11:19:22 +02:00
Jonas Herzig
ed566732df Merge branch '1.8' into 1.8.9
91573d9 Update jGui, ReplayStudio and Translations
d71358b Add confirmation dialog before overwriting path presets (fixes #65)
1a55983 Move addCallback method from integration test Utils to main Utils
18c5bcd Be more cooperative with other mods on the ingame menu (fixes #42)
c054fe8 Register keys for simplepathing only once (fixes #63)
f13297c Fix default interpolator handling in keyframe gui and after loading (fixes #64)
391f304 Show error popup if entity tracker fails to load
0e0eaaa Fix entity tracker being set even if it failed loading (fixes #50)
a34bbbc Fix NPE when spectating a player without a camera entity
60879fb Change local class to be an inner class because Srg2Source breaks with it
dfafbec Load translations from github repo, only reload language not all resource packs
8a6ec51 Add missing tooltips to player overview and various missing chat messages
5677fc5 Re-add warning to replay editor
748a91e Fix missing tooltips on fav/like/dislike buttons in replay center
ee24866 Fix upload replay button not being updated when name/tags change
dba085c Move translations into separate repo
3f0e3e7 Fix NPE when receiving Replay|Restrict messages (fixes #16 GH)
40e1d85 Fix crash when saving the last keyframe in the edit gui (fixes #61)
03aada1 Update Mixin to 0.6.8 (fixes #9 GH)
0c1dc65 Fix interpolator being lost when moving keyframe to the end (fixes #62)
fcbbbc9 Add integration test. Run with ./gradlew runIntegrationTest
fe6ded0 Fix movement of keyframe via GuiEditKeyframe not updating selected keyframe
f58fa8f Fix Login GUI not respecting other GUIs opened on startup
4fc3a31 Fix OpenEye being installed into working dir instead of mcDataDir
2017-05-31 09:36:46 +02:00
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
cda8493df1 Merge branch '1.11' into 1.11.2
0c36022 Merge branch '1.10.2' into 1.11
325d041 Merge branch '1.9.4' into 1.10.2
d9d57e2 Merge branch '1.8.9' into 1.9.4
24c58cb Merge branch '1.8' into 1.8.9
1ffeb01 Update jGui
c122d30 Merge branch '1.8-path-segments' into 1.8
27fd25a Fix crash when server responds with garbage to language query (fixes #51)
db6b0ff Fix / by 0 when two spectator keyframes are closer than 50ms (fixes #60)
d61d0ac Fix hand of invisible spectated player being visible (fixes #59)
f7460f9 Re-enable time path after path playback (fixes #55)
bdbffd5 Add Catmull-Rom spline interpolator to simplepathing
5cbfd28 Generate interpolator settings from InterpolatorType enum
6095aef Fix loading and storing of default interpolator with settings
3f52336 Revert "Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)"
c6293bc Update to FG2.2
c8126ed Merge '1.8' into 1.8.9
64898ce Fix NPE when saving recorded resource pack
70e3e54 Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)
3f2b3d6 Fix replacing source file in Replay Editor (fixes #57)
d2def94 Fix duration of replays corrupted because of JVM/OS crash
2c64030 Refactor simple pathing timeline logic into dedicated class with unit tests Fix logic for user-set interpolators
4deb374 Enable validation on focus change for all number input fields
5302bca Fix id and text of Replay Editor button in docs
057edcc Manually inject root cert for cross-signed LetsEncrypt cert in requests
cd16211 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-29 13:46:06 +01:00
johni0702
0c36022b47 Merge branch '1.10.2' into 1.11
325d041 Merge branch '1.9.4' into 1.10.2
d9d57e2 Merge branch '1.8.9' into 1.9.4
24c58cb Merge branch '1.8' into 1.8.9
1ffeb01 Update jGui
c122d30 Merge branch '1.8-path-segments' into 1.8
27fd25a Fix crash when server responds with garbage to language query (fixes #51)
db6b0ff Fix / by 0 when two spectator keyframes are closer than 50ms (fixes #60)
d61d0ac Fix hand of invisible spectated player being visible (fixes #59)
f7460f9 Re-enable time path after path playback (fixes #55)
bdbffd5 Add Catmull-Rom spline interpolator to simplepathing
5cbfd28 Generate interpolator settings from InterpolatorType enum
6095aef Fix loading and storing of default interpolator with settings
3f52336 Revert "Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)"
c6293bc Update to FG2.2
c8126ed Merge '1.8' into 1.8.9
64898ce Fix NPE when saving recorded resource pack
70e3e54 Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)
3f2b3d6 Fix replacing source file in Replay Editor (fixes #57)
d2def94 Fix duration of replays corrupted because of JVM/OS crash
2c64030 Refactor simple pathing timeline logic into dedicated class with unit tests Fix logic for user-set interpolators
4deb374 Enable validation on focus change for all number input fields
5302bca Fix id and text of Replay Editor button in docs
057edcc Manually inject root cert for cross-signed LetsEncrypt cert in requests
cd16211 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-29 13:25:19 +01:00
johni0702
325d041bd9 Merge branch '1.9.4' into 1.10.2
d9d57e2 Merge branch '1.8.9' into 1.9.4
24c58cb Merge branch '1.8' into 1.8.9
1ffeb01 Update jGui
c122d30 Merge branch '1.8-path-segments' into 1.8
27fd25a Fix crash when server responds with garbage to language query (fixes #51)
db6b0ff Fix / by 0 when two spectator keyframes are closer than 50ms (fixes #60)
d61d0ac Fix hand of invisible spectated player being visible (fixes #59)
f7460f9 Re-enable time path after path playback (fixes #55)
bdbffd5 Add Catmull-Rom spline interpolator to simplepathing
5cbfd28 Generate interpolator settings from InterpolatorType enum
6095aef Fix loading and storing of default interpolator with settings
3f52336 Revert "Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)"
c6293bc Update to FG2.2
c8126ed Merge '1.8' into 1.8.9
64898ce Fix NPE when saving recorded resource pack
70e3e54 Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)
3f2b3d6 Fix replacing source file in Replay Editor (fixes #57)
d2def94 Fix duration of replays corrupted because of JVM/OS crash
2c64030 Refactor simple pathing timeline logic into dedicated class with unit tests Fix logic for user-set interpolators
4deb374 Enable validation on focus change for all number input fields
5302bca Fix id and text of Replay Editor button in docs
057edcc Manually inject root cert for cross-signed LetsEncrypt cert in requests
cd16211 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-29 13:01:09 +01:00
johni0702
d9d57e283c Merge branch '1.8.9' into 1.9.4
24c58cb Merge branch '1.8' into 1.8.9
1ffeb01 Update jGui
c122d30 Merge branch '1.8-path-segments' into 1.8
27fd25a Fix crash when server responds with garbage to language query (fixes #51)
db6b0ff Fix / by 0 when two spectator keyframes are closer than 50ms (fixes #60)
d61d0ac Fix hand of invisible spectated player being visible (fixes #59)
f7460f9 Re-enable time path after path playback (fixes #55)
bdbffd5 Add Catmull-Rom spline interpolator to simplepathing
5cbfd28 Generate interpolator settings from InterpolatorType enum
6095aef Fix loading and storing of default interpolator with settings
3f52336 Revert "Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)"
c6293bc Update to FG2.2
c8126ed Merge '1.8' into 1.8.9
64898ce Fix NPE when saving recorded resource pack
70e3e54 Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)
3f2b3d6 Fix replacing source file in Replay Editor (fixes #57)
d2def94 Fix duration of replays corrupted because of JVM/OS crash
2c64030 Refactor simple pathing timeline logic into dedicated class with unit tests Fix logic for user-set interpolators
4deb374 Enable validation on focus change for all number input fields
5302bca Fix id and text of Replay Editor button in docs
057edcc Manually inject root cert for cross-signed LetsEncrypt cert in requests
cd16211 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-29 12:53:31 +01:00
johni0702
24c58cbbe7 Merge branch '1.8' into 1.8.9
1ffeb01 Update jGui
c122d30 Merge branch '1.8-path-segments' into 1.8
27fd25a Fix crash when server responds with garbage to language query (fixes #51)
db6b0ff Fix / by 0 when two spectator keyframes are closer than 50ms (fixes #60)
d61d0ac Fix hand of invisible spectated player being visible (fixes #59)
f7460f9 Re-enable time path after path playback (fixes #55)
bdbffd5 Add Catmull-Rom spline interpolator to simplepathing
5cbfd28 Generate interpolator settings from InterpolatorType enum
6095aef Fix loading and storing of default interpolator with settings
2c64030 Refactor simple pathing timeline logic into dedicated class with unit tests Fix logic for user-set interpolators
cd16211 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-29 11:25:30 +01: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
3f52336852 Revert "Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)"
This reverts commit 70e3e54038.

As of c6293bc, this branch uses FG2.2 which fully supports lambdas.
2017-01-24 13:29:09 +01:00
johni0702
c6293bcbb0 Update to FG2.2 2017-01-24 13:27:57 +01:00
johni0702
c8126edbb2 Merge '1.8' into 1.8.9
64898ce Fix NPE when saving recorded resource pack
70e3e54 Fix AbstractMethodError caused by FG2.0 not supporting lambdas (fixes #58)
3f2b3d6 Fix replacing source file in Replay Editor (fixes #57)
d2def94 Fix duration of replays corrupted because of JVM/OS crash
4deb374 Enable validation on focus change for all number input fields
5302bca Fix id and text of Replay Editor button in docs
057edcc Manually inject root cert for cross-signed LetsEncrypt cert in requests
2017-01-24 13:25:53 +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
33cb1405d5 Update to 1.11.2 2017-01-15 01:23:44 +01:00
johni0702
b6cf42c3be Merge branch '1.10.2' into 1.11
c6dec95 Merge branch '1.9.4' into 1.10.2
4fdfa9e Merge branch '1.8.9' into 1.9.4
44d4a25 Merge branch '1.8' into 1.9.4
01acbc6 Update to 1.8.9
1c60687 Update ReplayStudio and jGui
d9b796d Mixin'd the Shader's mods ShadersRender class to only render hands if the Forge event isn't cancelled
6bbff3b Updated Mixin to 0.6.4-SNAPSHOT and set mixin target level
6f3ac71 Add CustomMainMenu compatibility info to docs
b0a1cdf Search for ffmpeg executable in common locations to simplify setup
d465891 Fixed GuiEditPositionKeyframe's width to fit on all screen sizes Centered all rows of GuiEditPositionKeyframe's VerticalLayout
1b099f9 Add render queue
03534c6 Update docs
4388d69 Only try to create the downloads folder if it does not yet exist
7ad19aa Add mod version and accepted MC version directly to the @Mod annotation
655bea0 Replace @Mod.Instance with direct static references
458dad6 Merge pull request #4 from ReplayMod/1.8-editor
eb5ccb2 Add Replay Editor (for now, trimming only)
6498d62 Fix server game data snapshot being restored in singleplayer (fixes #44)
2c898a8 Prevent rendering without sufficient keyframes (fixes #45)
5d520a5 Fix deserialization of chroma key color (fixes #43)
c681742 Use caching maven proxy for faster and more stable drone builds
fbf165e Added Anti-Aliasing to the Render Settings
2017-01-15 00:20:20 +01:00
johni0702
c6dec95a57 Merge branch '1.9.4' into 1.10.2
4fdfa9e Merge branch '1.8.9' into 1.9.4
44d4a25 Merge branch '1.8' into 1.9.4
01acbc6 Update to 1.8.9
1c60687 Update ReplayStudio and jGui
d9b796d Mixin'd the Shader's mods ShadersRender class to only render hands if the Forge event isn't cancelled
6bbff3b Updated Mixin to 0.6.4-SNAPSHOT and set mixin target level
6f3ac71 Add CustomMainMenu compatibility info to docs
b0a1cdf Search for ffmpeg executable in common locations to simplify setup
d465891 Fixed GuiEditPositionKeyframe's width to fit on all screen sizes Centered all rows of GuiEditPositionKeyframe's VerticalLayout
1b099f9 Add render queue
03534c6 Update docs
4388d69 Only try to create the downloads folder if it does not yet exist
7ad19aa Add mod version and accepted MC version directly to the @Mod annotation
655bea0 Replace @Mod.Instance with direct static references
458dad6 Merge pull request #4 from ReplayMod/1.8-editor
eb5ccb2 Add Replay Editor (for now, trimming only)
6498d62 Fix server game data snapshot being restored in singleplayer (fixes #44)
2c898a8 Prevent rendering without sufficient keyframes (fixes #45)
5d520a5 Fix deserialization of chroma key color (fixes #43)
c681742 Use caching maven proxy for faster and more stable drone builds
fbf165e Added Anti-Aliasing to the Render Settings
2017-01-14 23:56:14 +01:00
johni0702
4fdfa9ec87 Merge branch '1.8.9' into 1.9.4 2017-01-14 23:48:02 +01:00
johni0702
44d4a250eb Merge branch '1.8' into 1.9.4
d9b796d Mixin'd the Shader's mods ShadersRender class to only render hands if the Forge event isn't cancelled
6bbff3b Updated Mixin to 0.6.4-SNAPSHOT and set mixin target level
6f3ac71 Add CustomMainMenu compatibility info to docs
b0a1cdf Search for ffmpeg executable in common locations to simplify setup
d465891 Fixed GuiEditPositionKeyframe's width to fit on all screen sizes Centered all rows of GuiEditPositionKeyframe's VerticalLayout
1b099f9 Add render queue
03534c6 Update docs
4388d69 Only try to create the downloads folder if it does not yet exist
7ad19aa Add mod version and accepted MC version directly to the @Mod annotation
655bea0 Replace @Mod.Instance with direct static references
458dad6 Merge pull request #4 from ReplayMod/1.8-editor
eb5ccb2 Add Replay Editor (for now, trimming only)
6498d62 Fix server game data snapshot being restored in singleplayer (fixes #44)
2c898a8 Prevent rendering without sufficient keyframes (fixes #45)
5d520a5 Fix deserialization of chroma key color (fixes #43)
c681742 Use caching maven proxy for faster and more stable drone builds
fbf165e Added Anti-Aliasing to the Render Settings
2017-01-14 23:28:28 +01:00
johni0702
01acbc6f1f Update to 1.8.9 2017-01-14 22:52:53 +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
johni0702
503b308043 Replace @Mod.Instance with direct static references
(cherry picked from commit 4b605cc)
2017-01-13 17:02:18 +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
7b491d188d Enabled recording on Realms Servers 2016-12-20 15:00:35 +01:00
CrushedPixel
fbf165e618 Added Anti-Aliasing to the Render Settings 2016-12-17 03:22:37 +01:00
johni0702
fb4140a123 Merge branch '1.10.2' into 1.11
905ae75 Merge branch '1.9.4' into 1.10.2
72e1fe0 Merge branch '1.8' into 1.9.4
6163585 Update ReplayStudio to fix dependencies
2016-12-16 22:09:16 +01:00
johni0702
905ae75330 Merge branch '1.9.4' into 1.10.2
72e1fe0 Merge branch '1.8' into 1.9.4
6163585 Update ReplayStudio to fix dependencies
2016-12-16 21:47:30 +01:00
johni0702
72e1fe0d6c Merge branch '1.8' into 1.9.4
6163585 Update ReplayStudio to fix dependencies
2016-12-16 21:27:43 +01:00
johni0702
6163585fe5 Update ReplayStudio to fix dependencies 2016-12-16 20:48:43 +01:00
johni0702
2428c01c9c Merge branch '1.10.2' into 1.11
372206f Merge branch '1.9.4' into 1.10.2
4a1ccea Merge branch '1.8' into 1.9.4
fb39a06 Add drone ci config
030c71c Automatically disable and re-enable Optifine's Fast Render setting before rendering a video
1fef12d Include FML handshake in recording
e86c66d Move README.md, LICENSE.md and user docs from master branch to 1.8
2016-12-10 20:28:23 +01:00
johni0702
260720d5ba Update Forge to latest recommended build (2188) 2016-12-10 20:25:33 +01:00
johni0702
58b7c44a33 Update to 1.11 2016-12-03 21:12:08 +01:00
123 changed files with 4996 additions and 1348 deletions

View File

@@ -3,9 +3,10 @@ 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 :build - ./gradlew :build
archive: archive:
image: plugins/s3 image: plugins/s3

View File

@@ -1 +1 @@
eyJhbGciOiJIUzI1NiJ9.cGlwZWxpbmU6CiAgY2xvbmU6CiAgICBpbWFnZTogcGx1Z2lucy9naXQKICAgIHRhZ3M6IHRydWUKICBidWlsZDoKICAgIGltYWdlOiBvcGVuamRrOjgKICAgIGNvbW1hbmRzOgogICAgICAtIEpBVkFfT1BUUz0tWG14MTUxNk0gLi9ncmFkbGV3IHNldHVwRGVjb21wV29ya3NwYWNlCiAgICAgIC0gLi9ncmFkbGV3IDpidWlsZAogIGFyY2hpdmU6CiAgICBpbWFnZTogcGx1Z2lucy9zMwogICAgYWNsOiBwdWJsaWMtcmVhZAogICAgcmVnaW9uOiAidXMtZWFzdC0xIgogICAgYnVja2V0OiAicmVwbGF5bW9kIgogICAgcGF0aF9zdHlsZTogdHJ1ZQogICAgc291cmNlOiBidWlsZC9saWJzLyoKICAgIHN0cmlwX3ByZWZpeDogYnVpbGQvbGlicy8KICAgIHRhcmdldDogLwo.Q_jQxd73CuNh8ZTGTw31yMmBIzWprIagmaddFYovDrc eyJhbGciOiJIUzI1NiJ9.cGlwZWxpbmU6CiAgY2xvbmU6CiAgICBpbWFnZTogcGx1Z2lucy9naXQKICAgIHRhZ3M6IHRydWUKICBidWlsZDoKICAgIGltYWdlOiBtYXZlbjozLjMuOS1qZGstOAogICAgY29tbWFuZHM6CiAgICAgIC0gY3VybCAtLWZhaWwgLS1jcmVhdGUtZGlycyAtbyAkSE9NRS8uZ3JhZGxlL2luaXQuZC9taXJyb3IuZ3JhZGxlIGh0dHBzOi8vbWF2ZW4uam9obmkwNzAyLmRlL2NvbmZpZy9ncmFkbGUKICAgICAgLSBjdXJsIC0tZmFpbCAtLWNyZWF0ZS1kaXJzIC1vICRIT01FLy5tMi9zZXR0aW5ncy54bWwgaHR0cHM6Ly9tYXZlbi5qb2huaTA3MDIuZGUvY29uZmlnL21hdmVuCiAgICAgIC0gLi9ncmFkbGV3IDpidWlsZAogIGFyY2hpdmU6CiAgICBpbWFnZTogcGx1Z2lucy9zMwogICAgYWNsOiBwdWJsaWMtcmVhZAogICAgcmVnaW9uOiAidXMtZWFzdC0xIgogICAgYnVja2V0OiAicmVwbGF5bW9kIgogICAgcGF0aF9zdHlsZTogdHJ1ZQogICAgc291cmNlOiBidWlsZC9saWJzLyoKICAgIHN0cmlwX3ByZWZpeDogYnVpbGQvbGlicy8KICAgIHRhcmdldDogLwo.Z6rpaLplJKQ4pzB8AjCnplgci42qEu7L4LeS0fGm4SY

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,7 +17,7 @@ buildscript {
} }
} }
dependencies { dependencies {
classpath 'net.minecraftforge.gradle:ForgeGradle:2.2-SNAPSHOT' classpath 'net.minecraftforge.gradle:ForgeGradle:2.3-SNAPSHOT'
classpath 'org.spongepowered:mixingradle:0.4-SNAPSHOT' classpath 'org.spongepowered:mixingradle:0.4-SNAPSHOT'
} }
} }
@@ -36,9 +34,11 @@ archivesBaseName = "replaymod"
minecraft { minecraft {
coreMod = 'com.replaymod.core.LoadingPlugin' coreMod = 'com.replaymod.core.LoadingPlugin'
version = '1.10.2-12.18.2.2099' version = '1.12-14.21.0.2327'
runDir = "eclipse" runDir = "eclipse"
mappings = "snapshot_20160518" mappings = "snapshot_20170615"
replace '@MOD_VERSION@', project.version
replace '@MC_VERSION@', project.minecraft.version
} }
repositories { repositories {
@@ -46,9 +46,6 @@ repositories {
name = "SpongePowered Repo" name = "SpongePowered Repo"
url = "http://repo.spongepowered.org/maven/" url = "http://repo.spongepowered.org/maven/"
} }
maven {
url 'https://repo.spacehq.org/content/repositories/snapshots/'
}
} }
configurations { configurations {
@@ -59,7 +56,9 @@ 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.11-SNAPSHOT') {
exclude group: 'org.apache.logging.log4j'
}
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 +100,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
@@ -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,81 @@ setupDecompWorkspace.dependsOn copySrg
setupDevWorkspace.dependsOn copySrg setupDevWorkspace.dependsOn copySrg
project.tasks.idea.dependsOn copySrg project.tasks.idea.dependsOn copySrg
import org.objectweb.asm.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
import static org.objectweb.asm.Opcodes.ASM5
// MC binaries were complied with a java version that produces invalid class files under certain circumstances
// This causes setupCIWorkspace to be insufficient for compiling.
// Related JDK bug: https://bugs.openjdk.java.net/browse/JDK-8066725
// As a workaround, to use setupCIWorkspace on Drone, we modify the bin jar in-place and remove all parameter annotations.
// WARNING: This piece of code ignores any and all gradle conventions and will probably fail horribly when run outside
// of a single-use environment (e.g. Drone). Use setupDecompWorkspace for normal use.
tasks.deobfMcMCP.doLast {
println "Applying RuntimeInvisibleParameterAnnotations workaround..."
File jar = getOutJar()
File tmp = new File((File) getTemporaryDir(), "workaround.jar")
tmp.withOutputStream {
new ZipOutputStream(it).withStream { dst ->
new ZipFile(jar).withCloseable { src ->
src.entries().each {
if (it.name.startsWith("net/minecraft/") && it.name.endsWith(".class")) {
def cw = new ClassWriter(0)
def cv = new ClassVisitor(ASM5, cw) {
@Override
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return new MethodVisitor(ASM5, cv.visitMethod(access, name, desc, signature, exceptions)) {
@Override
AnnotationVisitor visitParameterAnnotation(int parameter, String pdesc, boolean visible) {
return null // Strip all parameter annotations
}
}
}
}
new ClassReader(src.getInputStream(it)).accept(cv, 0)
dst.putNextEntry(new ZipEntry(it.name))
dst.write(cw.toByteArray())
} else {
dst.putNextEntry(it)
dst.write(src.getInputStream(it).bytes)
}
}
}
}
}
jar.delete()
tmp.renameTo(jar)
}
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...e5fcc65032

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,340 @@
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();
MinecraftForge.EVENT_BUS.register(this);
addCallback(future, success -> {
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) {
MinecraftForge.EVENT_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);
}
}
}
}
MinecraftForge.EVENT_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) {
MinecraftForge.EVENT_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);
}
}
}
}
MinecraftForge.EVENT_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;
}
MinecraftForge.EVENT_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));
}
}
MinecraftForge.EVENT_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.x + 5, button.y + 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,78 @@
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;
// Vanilla uses our keys
mc.gameSettings.keyBindAdvancements.setKeyCode(0);
mc.gameSettings.keyBindLoadToolbar.setKeyCode(0);
mc.gameSettings.keyBindSaveToolbar.setKeyCode(0);
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,29 @@
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() {
if ("1".equals(System.getenv("RM_INTEGRATION_TEST_NO_OPENEYE"))) {
runLater(() -> future.set(null));
return;
}
expectGui(OpenEyeExtra.OfferGui.class, offerGui -> {
click(offerGui.yesButton);
expectGuiClosed(20 * 1000, () -> {
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.GuiWorldSelection;
import net.minecraftforge.client.event.RenderGameOverlayEvent;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
public class CreateSPWorld extends AbstractTask {
@Override
protected void init() {
expectGui(GuiMainMenu.class, mainMenu -> {
click("Singleplayer");
expectGui(GuiWorldSelection.class, selectWorld -> {
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.player) {
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,19 +2,18 @@ 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.common.MinecraftForge; 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 = MinecraftForge.EVENT_BUS;

View File

@@ -30,7 +30,7 @@ public class ShaderBeginRender {
// check if Shaders are enabled // check if Shaders are enabled
if (!(boolean) (ShaderReflection.config_isShaders.invoke(null))) return; if (!(boolean) (ShaderReflection.config_isShaders.invoke(null))) return;
ShaderReflection.shaders_beginRender.invoke(null, mc, mc.timer.elapsedPartialTicks, 0); ShaderReflection.shaders_beginRender.invoke(null, mc, mc.timer.renderPartialTicks, 0);
} catch (IllegalAccessException | InvocationTargetException e) { } catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace(); e.printStackTrace();
} }

View File

@@ -20,6 +20,7 @@ public abstract class MixinShaderRenderChunk {
*/ */
@Inject(method = "isPlayerUpdate", at = @At("HEAD"), cancellable = true) @Inject(method = "isPlayerUpdate", at = @At("HEAD"), cancellable = true)
private void replayModCompat_disableIsPlayerUpdate(CallbackInfoReturnable<Boolean> ci) { private void replayModCompat_disableIsPlayerUpdate(CallbackInfoReturnable<Boolean> ci) {
// TODO: Update to 1.12 once optifine is available
if (((EntityRendererHandler.IEntityRenderer) mc.entityRenderer).replayModRender_getHandler() == null) return; if (((EntityRendererHandler.IEntityRenderer) mc.entityRenderer).replayModRender_getHandler() == null) return;
ci.setReturnValue(true); ci.setReturnValue(true);
} }

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

@@ -85,7 +85,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.addDetail("Handler", runnable::toString);
throw new ReportedException(crashReport); throw new ReportedException(crashReport);
} }
} }
@@ -100,7 +100,7 @@ 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.addDetail("Handler", runnable::toString);
throw new ReportedException(crashReport); throw new ReportedException(crashReport);
} }
} }

View File

@@ -34,6 +34,8 @@ import java.util.List;
@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 {

View File

@@ -14,9 +14,19 @@ public class GuiFactory implements IModGuiFactory {
} }
@Override
public boolean hasConfigGui() {
return true;
}
@Override
public GuiScreen createConfigGui(GuiScreen parentScreen) {
return new GuiReplaySettings(parentScreen, ReplayMod.instance.getSettingsRegistry()).toMinecraft();
}
@Override @Override
public Class<? extends GuiScreen> mainConfigGuiClass() { public Class<? extends GuiScreen> mainConfigGuiClass() {
return ConfigGuiWrapper.class; return null;
} }
@Override @Override
@@ -28,17 +38,4 @@ public class GuiFactory implements IModGuiFactory {
public RuntimeOptionGuiHandler getHandlerFor(RuntimeOptionCategoryElement element) { public RuntimeOptionGuiHandler getHandlerFor(RuntimeOptionCategoryElement element) {
return null; return null;
} }
public static class ConfigGuiWrapper extends GuiScreen {
private final GuiScreen parent;
public ConfigGuiWrapper(GuiScreen parent) {
this.parent = parent;
}
@Override
public void initGui() {
new GuiReplaySettings(parent, ReplayMod.instance.getSettingsRegistry()).display();
}
}
} }

View File

@@ -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

@@ -19,12 +19,12 @@ public class MainMenuHandler {
if (event.getGui() instanceof GuiMainMenu) { if (event.getGui() instanceof GuiMainMenu) {
for (GuiButton button : event.getButtonList()) { for (GuiButton button : event.getButtonList()) {
// 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.x + button.width < event.getGui().width / 2 - 100
|| button.xPosition > event.getGui().width / 2 + 100 || button.x > event.getGui().width / 2 + 100
|| button.yPosition > event.getGui().height / 4 + 10 + 4 * 24) continue; || button.y > event.getGui().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.y -= 2 * 24 - 10;
} }
} }
} }

View File

@@ -1,8 +1,8 @@
package com.replaymod.core.utils; package com.replaymod.core.utils;
import net.minecraft.client.renderer.BufferBuilder;
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.vertex.DefaultVertexFormats; 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,7 +35,7 @@ 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(); BufferBuilder vertexBuffer = tessellator.getBuffer();
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); vertexBuffer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX);

View File

@@ -17,7 +17,7 @@ public class Restrictions {
public String handle(SPacketCustomPayload packet) { public String handle(SPacketCustomPayload packet) {
PacketBuffer buffer = packet.getBufferData(); PacketBuffer buffer = packet.getBufferData();
while (buffer.isReadable()) { while (buffer.isReadable()) {
String name = buffer.readStringFromBuffer(64); String name = buffer.readString(64);
boolean active = buffer.readBoolean(); boolean active = buffer.readBoolean();
// if ("no_xray".equals(name)) { // if ("no_xray".equals(name)) {
// noXray = active; // noXray = active;

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().getConnection().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

@@ -3,6 +3,8 @@ package com.replaymod.core.utils;
import net.minecraft.util.Timer; import net.minecraft.util.Timer;
public class WrappedTimer extends Timer { public class WrappedTimer extends Timer {
public static final float DEFAULT_MS_PER_TICK = 1000 / 20;
protected final Timer wrapped; protected final Timer wrapped;
public WrappedTimer(Timer wrapped) { public WrappedTimer(Timer wrapped) {
@@ -19,15 +21,10 @@ public class WrappedTimer extends Timer {
} }
protected void copy(Timer from, Timer to) { protected void copy(Timer from, Timer to) {
to.ticksPerSecond = from.ticksPerSecond;
to.lastHRTime = from.lastHRTime;
to.elapsedTicks = from.elapsedTicks; to.elapsedTicks = from.elapsedTicks;
to.renderPartialTicks = from.renderPartialTicks; to.renderPartialTicks = from.renderPartialTicks;
to.timerSpeed = from.timerSpeed;
to.elapsedPartialTicks = from.elapsedPartialTicks;
to.lastSyncSysClock = from.lastSyncSysClock; to.lastSyncSysClock = from.lastSyncSysClock;
to.lastSyncHRClock = from.lastSyncHRClock; to.elapsedPartialTicks = from.elapsedPartialTicks;
to.counter = from.counter; to.tickLength = from.tickLength;
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,48 @@
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.eventhandler.SubscribeEvent;
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() {
MinecraftForge.EVENT_BUS.register(this);
}
@SubscribeEvent
public void injectIntoMainMenu(GuiScreenEvent.InitGuiEvent event) {
if (!(event.getGui() instanceof GuiMainMenu)) {
return;
}
GuiButton button = new GuiButton(BUTTON_REPLAY_EDITOR, event.getGui().width / 2 + 2,
event.getGui().height / 4 + 10 + 3 * 24, I18n.format("replaymod.gui.replayeditor"));
button.width = button.width / 2 - 2;
event.getButtonList().add(button);
}
@SubscribeEvent
public void onButton(GuiScreenEvent.ActionPerformedEvent.Pre event) {
if(!event.getButton().enabled) return;
if (event.getGui() instanceof GuiMainMenu) {
if (event.getButton().id == BUTTON_REPLAY_EDITOR) {
new GuiReplayEditor(GuiScreen.wrap(event.getGui()), mod.getCore()).display();
}
}
}
}

View File

@@ -10,14 +10,12 @@ 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.common.MinecraftForge;
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() {

View File

@@ -1,102 +1,106 @@
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.MetadataSerializer;
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<String> getResourceDomains() {
return ImmutableSet.of("minecraft", "replaymod"); return ImmutableSet.of("replaymod");
} }
@Override @Override

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,11 @@ 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;
import static com.replaymod.extras.ReplayModExtras.LOGGER;
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;
@@ -36,11 +40,29 @@ public class OpenEyeExtra implements Extra {
mod.getSettingsRegistry().register(ASK_FOR_OPEN_EYE); mod.getSettingsRegistry().register(ASK_FOR_OPEN_EYE);
if (!Loader.isModLoaded("OpenEye") && mod.getSettingsRegistry().get(ASK_FOR_OPEN_EYE)) { if (!Loader.isModLoaded("OpenEye") && mod.getSettingsRegistry().get(ASK_FOR_OPEN_EYE)) {
mod.runLater(() -> new OfferGui(GuiScreen.wrap(mod.getMinecraft().currentScreen)).display()); new Thread(() -> {
try {
LOGGER.trace("Checking for OpenEye availability");
HttpsURLConnection connection = (HttpsURLConnection) new URL(DOWNLOAD_URL).openConnection();
connection.setSSLSocketFactory(SSL_SOCKET_FACTORY);
connection.setRequestMethod("HEAD");
connection.connect();
LOGGER.trace("Got response code: {}", connection.getResponseCode());
if (connection.getResponseCode() == 200) {
mod.runLater(() -> new OfferGui(GuiScreen.wrap(mod.getMinecraft().currentScreen)).display());
} else {
LOGGER.info("Cannot offer OpenEye, server returned: {} {}",
connection.getResponseCode(), connection.getResponseMessage());
}
connection.disconnect();
} catch (Throwable e) {
LOGGER.error("Failed to check for OpenEye availability:", e);
}
}).start();
} }
} }
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 +88,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 +126,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,10 @@ 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.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import org.lwjgl.input.Keyboard; import org.lwjgl.input.Keyboard;
@@ -21,20 +22,21 @@ 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() {
if (module.getReplayHandler() != null) { if (module.getReplayHandler() != null) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<EntityPlayer> players = mod.getMinecraft().theWorld.getPlayers(EntityPlayer.class, new Predicate() { List<EntityPlayer> players = mod.getMinecraft().world.getPlayers(EntityPlayer.class, new Predicate() {
@Override @Override
public boolean apply(Object input) { public boolean apply(Object input) {
return !(input instanceof CameraEntity); // Exclude the camera entity return !(input instanceof CameraEntity); // Exclude the camera entity
@@ -92,6 +94,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;
@@ -37,6 +42,7 @@ 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
@@ -44,14 +50,14 @@ public class PlayerOverviewGui extends GuiScreen implements Closeable {
getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(SoundEvents.UI_BUTTON_CLICK, 1.0F)); getMinecraft().getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(SoundEvents.UI_BUTTON_CLICK, 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.getMasterRecord(SoundEvents.UI_BUTTON_CLICK, 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);

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

@@ -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,8 +67,10 @@ 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();
@@ -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

@@ -4,7 +4,10 @@ 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"), MC_1_9_4("Minecraft 1.9.4", "1.9.4"),
MC_1_10_2("Minecraft 1.10.2", "1.10.2"); MC_1_10_2("Minecraft 1.10.2", "1.10.2"),
MC_1_11("Minecraft 1.11", "1.11"),
MC_1_11_2("Minecraft 1.11.2", "1.11.2"),
MC_1_12("Minecraft 1.12", "1.12");
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

@@ -55,7 +55,7 @@ public class GuiRegister extends AbstractGuiScreen<GuiRegister> {
pos(cancelButton, width / 2 + 2, 170); pos(cancelButton, width / 2 + 2, 170);
pos(statusLabel, width / 2 - statusLabel.getMinSize().getWidth() / 2, 152); pos(statusLabel, width / 2 - statusLabel.getMinSize().getWidth() / 2, 152);
FontRenderer font = getMinecraft().fontRendererObj; FontRenderer font = getMinecraft().fontRenderer;
int lineCount = font.listFormattedStringToWidth(disclaimerLabel.getText(), width - 10).size(); int lineCount = font.listFormattedStringToWidth(disclaimerLabel.getText(), width - 10).size();
Dimension dim = new Dimension(width - 10, font.FONT_HEIGHT * lineCount); Dimension dim = new Dimension(width - 10, font.FONT_HEIGHT * lineCount);
disclaimerLabel.setSize(dim); disclaimerLabel.setSize(dim);

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;
@@ -38,7 +39,7 @@ import de.johni0702.minecraft.gui.utils.Colors;
import de.johni0702.minecraft.gui.utils.Consumer; import de.johni0702.minecraft.gui.utils.Consumer;
import net.minecraftforge.fml.common.FMLLog; import net.minecraftforge.fml.common.FMLLog;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.core.helpers.Strings; import org.apache.logging.log4j.util.Strings;
import org.lwjgl.util.Dimension; import org.lwjgl.util.Dimension;
import org.lwjgl.util.ReadableDimension; import org.lwjgl.util.ReadableDimension;
@@ -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

@@ -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

@@ -6,6 +6,7 @@ import com.google.common.collect.Ordering;
import com.google.common.primitives.Longs; import com.google.common.primitives.Longs;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import com.replaymod.core.utils.WrappedTimer;
import com.replaymod.replay.ReplayHandler; import com.replaymod.replay.ReplayHandler;
import com.replaymod.replaystudio.pathing.path.Keyframe; import com.replaymod.replaystudio.pathing.path.Keyframe;
import com.replaymod.replaystudio.pathing.path.Path; import com.replaymod.replaystudio.pathing.path.Path;
@@ -68,8 +69,8 @@ public abstract class AbstractTimelinePlayer {
MinecraftForge.EVENT_BUS.register(this); MinecraftForge.EVENT_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.tickLength = WrappedTimer.DEFAULT_MS_PER_TICK;
mc.timer.elapsedPartialTicks = mc.timer.elapsedTicks = 0; mc.timer.renderPartialTicks = mc.timer.elapsedTicks = 0;
return future = settableFuture = SettableFuture.create(); return future = settableFuture = SettableFuture.create();
} }
@@ -107,10 +108,9 @@ public abstract class AbstractTimelinePlayer {
float timeInTicks = replayTime / 50f; float timeInTicks = replayTime / 50f;
float previousTimeInTicks = lastTime / 50f; float previousTimeInTicks = lastTime / 50f;
float passedTicks = timeInTicks - previousTimeInTicks; float passedTicks = timeInTicks - previousTimeInTicks;
mc.timer.elapsedPartialTicks += passedTicks; mc.timer.renderPartialTicks += passedTicks;
mc.timer.elapsedTicks = (int) mc.timer.elapsedPartialTicks; mc.timer.elapsedTicks = (int) mc.timer.renderPartialTicks;
mc.timer.elapsedPartialTicks -= mc.timer.elapsedTicks; mc.timer.renderPartialTicks -= mc.timer.elapsedTicks;
mc.timer.renderPartialTicks = mc.timer.elapsedPartialTicks;
lastTime = replayTime; lastTime = replayTime;

View File

@@ -4,6 +4,8 @@ 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 io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandler;
import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.MinecraftForge;
import net.minecraft.network.NetworkManager; import net.minecraft.network.NetworkManager;
import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod;
@@ -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);
@@ -51,9 +56,12 @@ public class ReplayModRecording {
EventBus bus = MinecraftForge.EVENT_BUS; EventBus bus = MinecraftForge.EVENT_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

@@ -42,7 +42,7 @@ public class GuiRecordingOverlay {
public void renderRecordingIndicator(RenderGameOverlayEvent.Post event) { public void renderRecordingIndicator(RenderGameOverlayEvent.Post event) {
if (event.getType() != RenderGameOverlayEvent.ElementType.ALL) return; if (event.getType() != RenderGameOverlayEvent.ElementType.ALL) return;
if (settingsRegistry.get(Setting.INDICATOR)) { if (settingsRegistry.get(Setting.INDICATOR)) {
FontRenderer fontRenderer = mc.fontRendererObj; FontRenderer fontRenderer = mc.fontRenderer;
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);
mc.renderEngine.bindTexture(TEXTURE); mc.renderEngine.bindTexture(TEXTURE);
GlStateManager.resetColor(); GlStateManager.resetColor();

View File

@@ -46,7 +46,7 @@ public class ConnectionEventHandler {
try { try {
boolean local = networkManager.isLocalChannel(); boolean local = networkManager.isLocalChannel();
if (local) { if (local) {
if (mc.getIntegratedServer().getEntityWorld().getWorldType() == WorldType.DEBUG_WORLD) { if (mc.getIntegratedServer().getEntityWorld().getWorldType() == WorldType.DEBUG_ALL_BLOCK_STATES) {
logger.info("Debug World recording is not supported."); logger.info("Debug World recording is not supported.");
return; return;
} }
@@ -66,6 +66,9 @@ public class ConnectionEventHandler {
worldName = mc.getIntegratedServer().getWorldName(); worldName = mc.getIntegratedServer().getWorldName();
} else if (Minecraft.getMinecraft().getCurrentServerData() != null) { } else if (Minecraft.getMinecraft().getCurrentServerData() != null) {
worldName = Minecraft.getMinecraft().getCurrentServerData().serverIP; worldName = Minecraft.getMinecraft().getCurrentServerData().serverIP;
} else if (Minecraft.getMinecraft().isConnectedToRealms()) {
// we can't access the server name without tapping too deep in the Realms Library
worldName = "A Realms Server";
} else { } else {
logger.info("Recording not started as the world is neither local nor remote (probably a replay)."); logger.info("Recording not started as the world is neither local nor remote (probably a replay).");
return; return;

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

@@ -57,7 +57,7 @@ public class RecordingEventHandler {
public void onPlayerJoin() { public void onPlayerJoin() {
try { try {
packetListener.save(new SPacketSpawnPlayer(mc.thePlayer)); packetListener.save(new SPacketSpawnPlayer(mc.player));
} catch(Exception e1) { } catch(Exception e1) {
e1.printStackTrace(); e1.printStackTrace();
} }
@@ -65,7 +65,7 @@ public class RecordingEventHandler {
public void onPlayerRespawn() { public void onPlayerRespawn() {
try { try {
packetListener.save(new SPacketSpawnPlayer(mc.thePlayer)); packetListener.save(new SPacketSpawnPlayer(mc.player));
} catch(Exception e) { } catch(Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
@@ -93,7 +93,7 @@ public class RecordingEventHandler {
@SubscribeEvent @SubscribeEvent
public void onPlayerTick(PlayerTickEvent e) { public void onPlayerTick(PlayerTickEvent e) {
try { try {
if(e.player != mc.thePlayer) return; if(e.player != mc.player) return;
boolean force = false; boolean force = false;
if(lastX == null || lastY == null || lastZ == null) { if(lastX == null || lastY == null || lastZ == null) {
@@ -168,62 +168,62 @@ public class RecordingEventHandler {
*/ */
//Inventory Handling //Inventory Handling
if (playerItems[0] != mc.thePlayer.getHeldItem(EnumHand.MAIN_HAND)) { if (playerItems[0] != mc.player.getHeldItem(EnumHand.MAIN_HAND)) {
playerItems[0] = mc.thePlayer.getHeldItem(EnumHand.MAIN_HAND); playerItems[0] = mc.player.getHeldItem(EnumHand.MAIN_HAND);
packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.MAINHAND, playerItems[0])); packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.MAINHAND, playerItems[0]));
} }
if (playerItems[1] != mc.thePlayer.getHeldItem(EnumHand.OFF_HAND)) { if (playerItems[1] != mc.player.getHeldItem(EnumHand.OFF_HAND)) {
playerItems[1] = mc.thePlayer.getHeldItem(EnumHand.OFF_HAND); playerItems[1] = mc.player.getHeldItem(EnumHand.OFF_HAND);
packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.OFFHAND, playerItems[1])); packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.OFFHAND, playerItems[1]));
} }
if (playerItems[2] != mc.thePlayer.inventory.armorInventory[0]) { if (playerItems[2] != mc.player.getItemStackFromSlot(EntityEquipmentSlot.FEET)) {
playerItems[2] = mc.thePlayer.inventory.armorInventory[0]; playerItems[2] = mc.player.getItemStackFromSlot(EntityEquipmentSlot.FEET);
packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.FEET, playerItems[2])); packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.FEET, playerItems[2]));
} }
if (playerItems[3] != mc.thePlayer.inventory.armorInventory[1]) { if (playerItems[3] != mc.player.getItemStackFromSlot(EntityEquipmentSlot.LEGS)) {
playerItems[3] = mc.thePlayer.inventory.armorInventory[1]; playerItems[3] = mc.player.getItemStackFromSlot(EntityEquipmentSlot.LEGS);
packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.LEGS, playerItems[3])); packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.LEGS, playerItems[3]));
} }
if (playerItems[4] != mc.thePlayer.inventory.armorInventory[2]) { if (playerItems[4] != mc.player.getItemStackFromSlot(EntityEquipmentSlot.CHEST)) {
playerItems[4] = mc.thePlayer.inventory.armorInventory[2]; playerItems[4] = mc.player.getItemStackFromSlot(EntityEquipmentSlot.CHEST);
packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.CHEST, playerItems[4])); packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.CHEST, playerItems[4]));
} }
if (playerItems[5] != mc.thePlayer.inventory.armorInventory[3]) { if (playerItems[5] != mc.player.getItemStackFromSlot(EntityEquipmentSlot.HEAD)) {
playerItems[5] = mc.thePlayer.inventory.armorInventory[3]; playerItems[5] = mc.player.getItemStackFromSlot(EntityEquipmentSlot.HEAD);
packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.HEAD, playerItems[5])); packetListener.save(new SPacketEntityEquipment(e.player.getEntityId(), EntityEquipmentSlot.HEAD, playerItems[5]));
} }
//Leaving Ride //Leaving Ride
if((!mc.thePlayer.isRiding() && lastRiding != -1) || if((!mc.player.isRiding() && lastRiding != -1) ||
(mc.thePlayer.isRiding() && lastRiding != mc.thePlayer.getRidingEntity().getEntityId())) { (mc.player.isRiding() && lastRiding != mc.player.getRidingEntity().getEntityId())) {
if(!mc.thePlayer.isRiding()) { if(!mc.player.isRiding()) {
lastRiding = -1; lastRiding = -1;
} else { } else {
lastRiding = mc.thePlayer.getRidingEntity().getEntityId(); lastRiding = mc.player.getRidingEntity().getEntityId();
} }
packetListener.save(new SPacketEntityAttach(e.player, e.player.getRidingEntity())); packetListener.save(new SPacketEntityAttach(e.player, e.player.getRidingEntity()));
} }
//Sleeping //Sleeping
if(!mc.thePlayer.isPlayerSleeping() && wasSleeping) { if(!mc.player.isPlayerSleeping() && wasSleeping) {
packetListener.save(new SPacketAnimation(e.player, 2)); packetListener.save(new SPacketAnimation(e.player, 2));
wasSleeping = false; wasSleeping = false;
} }
// Active hand (e.g. eating, drinking, blocking) // Active hand (e.g. eating, drinking, blocking)
if (mc.thePlayer.isHandActive() ^ wasHandActive || mc.thePlayer.getActiveHand() != lastActiveHand) { if (mc.player.isHandActive() ^ wasHandActive || mc.player.getActiveHand() != lastActiveHand) {
wasHandActive = mc.thePlayer.isHandActive(); wasHandActive = mc.player.isHandActive();
lastActiveHand = mc.thePlayer.getActiveHand(); lastActiveHand = mc.player.getActiveHand();
EntityDataManager dataManager = new EntityDataManager(null); EntityDataManager dataManager = new EntityDataManager(null);
int state = (wasHandActive ? 1 : 0) | (lastActiveHand == EnumHand.OFF_HAND ? 2 : 0); int state = (wasHandActive ? 1 : 0) | (lastActiveHand == EnumHand.OFF_HAND ? 2 : 0);
dataManager.register(EntityLiving.HAND_STATES, (byte) state); dataManager.register(EntityLiving.HAND_STATES, (byte) state);
packetListener.save(new SPacketEntityMetadata(mc.thePlayer.getEntityId(), dataManager, true)); packetListener.save(new SPacketEntityMetadata(mc.player.getEntityId(), dataManager, true));
} }
} catch(Exception e1) { } catch(Exception e1) {
@@ -234,7 +234,8 @@ 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 SPacketCollectItem(event.pickedUp.getEntityId(), event.player.getEntityId(),
event.pickedUp.getItem().getMaxStackSize()));
} catch(Exception e) { } catch(Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
@@ -243,7 +244,7 @@ public class RecordingEventHandler {
@SubscribeEvent @SubscribeEvent
public void onSleep(PlayerSleepInBedEvent event) { public void onSleep(PlayerSleepInBedEvent event) {
try { try {
if (event.getEntityPlayer() != mc.thePlayer) { if (event.getEntityPlayer() != mc.player) {
return; return;
} }
@@ -259,7 +260,7 @@ public class RecordingEventHandler {
@SubscribeEvent @SubscribeEvent
public void enterMinecart(MinecartInteractEvent event) { public void enterMinecart(MinecartInteractEvent event) {
try { try {
if(event.getEntity() != mc.thePlayer) { if(event.getEntity() != mc.player) {
return; return;
} }
@@ -272,7 +273,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.player;
if (thePlayer != null && breakerId == thePlayer.getEntityId()) { if (thePlayer != null && breakerId == thePlayer.getEntityId()) {
packetListener.save(new SPacketBlockBreakAnim(breakerId, pos, progress)); packetListener.save(new SPacketBlockBreakAnim(breakerId, pos, progress));
} }

View File

@@ -24,4 +24,9 @@ public abstract class MixinNetHandlerLoginClient {
public void replayModRecording_initiateRecording(CallbackInfo cb) { public void replayModRecording_initiateRecording(CallbackInfo cb) {
ReplayModRecording.instance.initiateRecording(networkManager); ReplayModRecording.instance.initiateRecording(networkManager);
} }
@Inject(method = "handleLoginSuccess", at=@At("RETURN"))
public void replayModRecording_raceConditionWorkAround(CallbackInfo cb) {
networkManager.channel().config().setAutoRead(true);
}
} }

View File

@@ -37,14 +37,14 @@ public abstract class MixinNetHandlerPlayClient {
*/ */
@Inject(method = "handlePlayerListItem", at=@At("HEAD")) @Inject(method = "handlePlayerListItem", at=@At("HEAD"))
public void recordOwnJoin(SPacketPlayerListItem packet, CallbackInfo ci) { public void recordOwnJoin(SPacketPlayerListItem packet, CallbackInfo ci) {
if (gameController.thePlayer == null) return; if (gameController.player == null) return;
RecordingEventHandler handler = getRecordingEventHandler(); RecordingEventHandler handler = getRecordingEventHandler();
if (handler != null && packet.getAction() == SPacketPlayerListItem.Action.ADD_PLAYER) { if (handler != null && packet.getAction() == SPacketPlayerListItem.Action.ADD_PLAYER) {
for (SPacketPlayerListItem.AddPlayerData data : packet.getEntries()) { for (SPacketPlayerListItem.AddPlayerData data : packet.getEntries()) {
if (data.getProfile() == null || data.getProfile().getId() == null) continue; if (data.getProfile() == null || data.getProfile().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.getProfile().getId().equals(Minecraft.getMinecraft().player.getGameProfile().getId())
&& !playerInfoMap.containsKey(data.getProfile().getId())) { && !playerInfoMap.containsKey(data.getProfile().getId())) {
handler.onPlayerJoin(); handler.onPlayerJoin();
} }

View File

@@ -1,27 +1,63 @@
package com.replaymod.recording.mixin; package com.replaymod.recording.mixin;
import com.replaymod.recording.handler.FMLHandshakeFilter;
import io.netty.channel.ChannelConfig;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.embedded.EmbeddedChannel; import io.netty.channel.embedded.EmbeddedChannel;
import net.minecraft.network.EnumConnectionState;
import net.minecraft.network.NetworkManager;
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;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 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());
}
}
@Redirect(method = "clientListenForServerHandshake", at = @At(value = "INVOKE", remap = true, target =
"Lnet/minecraft/network/NetworkManager;setConnectionState(Lnet/minecraft/network/EnumConnectionState;)V"))
public void replayModRecording_raceConditionWorkAround1(NetworkManager self, EnumConnectionState ignored) { }
@Redirect(method = "insertIntoChannel", at = @At(value = "INVOKE", target =
"Lio/netty/channel/ChannelConfig;setAutoRead(Z)Lio/netty/channel/ChannelConfig;"))
public ChannelConfig replayModRecording_raceConditionWorkAround2(ChannelConfig self, boolean autoRead) {
if (side == Side.CLIENT) {
autoRead = false;
}
return self.setAutoRead(autoRead);
} }
} }

View File

@@ -23,6 +23,6 @@ public abstract class MixinPlayerControllerMP implements RecordingEventHandler.R
@Redirect(method = "onPlayerDestroyBlock", at = @At(value = "INVOKE", @Redirect(method = "onPlayerDestroyBlock", at = @At(value = "INVOKE",
target = "Lnet/minecraft/world/World;playEvent(ILnet/minecraft/util/math/BlockPos;I)V")) 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) { public void replayModRecording_playEvent_fixed(World world, int type, BlockPos pos, int data) {
world.playEvent(mc.thePlayer, type, pos, data); world.playEvent(mc.player, type, pos, data);
} }
} }

View File

@@ -38,7 +38,7 @@ public abstract class MixinWorldClient extends World implements RecordingEventHa
@Inject(method = "playSound", at = @At("HEAD")) @Inject(method = "playSound", at = @At("HEAD"))
public void replayModRecording_recordClientSound(EntityPlayer player, double x, double y, double z, SoundEvent sound, SoundCategory category, public void replayModRecording_recordClientSound(EntityPlayer player, double x, double y, double z, SoundEvent sound, SoundCategory category,
float volume, float pitch, CallbackInfo ci) { float volume, float pitch, CallbackInfo ci) {
if (player == mc.thePlayer) { if (player == mc.player) {
RecordingEventHandler handler = replayModRecording_getRecordingEventHandler(); RecordingEventHandler handler = replayModRecording_getRecordingEventHandler();
if (handler != null) { if (handler != null) {
handler.onClientSound(sound, category, x, y, z, volume, pitch); handler.onClientSound(sound, category, x, y, z, volume, pitch);
@@ -50,7 +50,7 @@ public abstract class MixinWorldClient extends World implements RecordingEventHa
// These are handled in the World class, so we override the method in WorldClient and add our special handling. // These are handled in the World class, so we override the method in WorldClient and add our special handling.
@Override @Override
public void playEvent(EntityPlayer player, int type, BlockPos pos, int data) { public void playEvent(EntityPlayer player, int type, BlockPos pos, int data) {
if (player == mc.thePlayer) { if (player == mc.player) {
// We caused this event, the server won't send it to us // We caused this event, the server won't send it to us
RecordingEventHandler handler = replayModRecording_getRecordingEventHandler(); RecordingEventHandler handler = replayModRecording_getRecordingEventHandler();
if (handler != null) { if (handler != null) {

View File

@@ -155,8 +155,8 @@ public class PacketListener extends ChannelInboundHandlerAdapter {
Packet packet = (Packet) msg; Packet packet = (Packet) msg;
if(packet instanceof SPacketCollectItem) { if(packet instanceof SPacketCollectItem) {
if(mc.thePlayer != null || if(mc.player != null ||
((SPacketCollectItem) packet).getEntityID() == mc.thePlayer.getEntityId()) { ((SPacketCollectItem) packet).getEntityID() == mc.player.getEntityId()) {
super.channelRead(ctx, msg); super.channelRead(ctx, msg);
return; return;
} }
@@ -197,7 +197,7 @@ public class PacketListener extends ChannelInboundHandlerAdapter {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private byte[] getPacketData(Packet packet) throws IOException { private byte[] getPacketData(Packet packet) throws Exception {
if (packet instanceof SPacketSpawnMob) { if (packet instanceof SPacketSpawnMob) {
SPacketSpawnMob p = (SPacketSpawnMob) packet; SPacketSpawnMob p = (SPacketSpawnMob) packet;
if (p.dataManager == null) { if (p.dataManager == null) {
@@ -228,7 +228,7 @@ public class PacketListener extends ChannelInboundHandlerAdapter {
} }
ByteBuf byteBuf = Unpooled.buffer(); ByteBuf byteBuf = Unpooled.buffer();
PacketBuffer packetBuffer = new PacketBuffer(byteBuf); PacketBuffer packetBuffer = new PacketBuffer(byteBuf);
packetBuffer.writeVarIntToBuffer(packetId); packetBuffer.writeVarInt(packetId);
packet.writePacketData(packetBuffer); packet.writePacketData(packetBuffer);
byteBuf.readerIndex(0); byteBuf.readerIndex(0);

View File

@@ -26,6 +26,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 +53,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;
@@ -84,7 +88,7 @@ public class ResourcePackRecorder {
if (levelDir.isFile()) { if (levelDir.isFile()) {
netManager.sendPacket(new CPacketResourcePackStatus(CPacketResourcePackStatus.Action.ACCEPTED)); netManager.sendPacket(new CPacketResourcePackStatus(CPacketResourcePackStatus.Action.ACCEPTED));
Futures.addCallback(mc.getResourcePackRepository().setResourcePackInstance(levelDir), new FutureCallback<Object>() { Futures.addCallback(mc.getResourcePackRepository().setServerResourcePack(levelDir), new FutureCallback<Object>() {
@Override @Override
public void onSuccess(Object result) { public void onSuccess(Object result) {
recordResourcePack(levelDir, requestId); recordResourcePack(levelDir, requestId);
@@ -170,7 +174,7 @@ public class ResourcePackRecorder {
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.setServerResourcePack(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 +190,13 @@ public class ResourcePackRecorder {
Futures.getUnchecked(mc.addScheduledTask(() -> mc.displayGuiScreen(guiScreen))); Futures.getUnchecked(mc.addScheduledTask(() -> mc.displayGuiScreen(guiScreen)));
Map<String, String> sessionInfo = ResourcePackRepository.func_190115_a(); Map<String, String> sessionInfo = ResourcePackRepository.getDownloadHeaders();
repo.downloadingPacks = HttpUtil.downloadResourcePack(file, url, sessionInfo, 50 * 1024 * 1024, guiScreen, mc.getProxy()); repo.downloadingPacks = HttpUtil.downloadResourcePack(file, url, sessionInfo, 50 * 1024 * 1024, guiScreen, mc.getProxy());
Futures.addCallback(repo.downloadingPacks, new FutureCallback<Object>() { Futures.addCallback(repo.downloadingPacks, new FutureCallback<Object>() {
@Override @Override
public void onSuccess(Object value) { public void onSuccess(Object value) {
recordResourcePack(file, requestId); recordResourcePack(file, requestId);
repo.setResourcePackInstance(file); repo.setServerResourcePack(file);
} }
@Override @Override

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.MinecraftForge;
import net.minecraftforge.common.config.Configuration; import net.minecraftforge.common.config.Configuration;
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());
MinecraftForge.EVENT_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

@@ -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

@@ -4,7 +4,7 @@ import com.replaymod.render.hooks.EntityRendererHandler;
import net.minecraft.client.Minecraft; import net.minecraft.client.Minecraft;
import net.minecraft.client.particle.Particle; import net.minecraft.client.particle.Particle;
import net.minecraft.client.particle.ParticleManager; import net.minecraft.client.particle.ParticleManager;
import net.minecraft.client.renderer.VertexBuffer; import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.entity.Entity; 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;
@@ -12,19 +12,19 @@ import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ParticleManager.class) @Mixin(ParticleManager.class)
public abstract class MixinParticleManager { 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")) @Redirect(method = "renderParticles", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/Particle;renderParticle(Lnet/minecraft/client/renderer/BufferBuilder;Lnet/minecraft/entity/Entity;FFFFFF)V"))
private void renderNormalParticle(Particle particle, VertexBuffer vertexBuffer, Entity view, float partialTicks, private void renderNormalParticle(Particle particle, BufferBuilder vertexBuffer, Entity view, float partialTicks,
float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) { float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
renderParticle(particle, vertexBuffer, view, partialTicks, rotX, rotXZ, rotZ, rotYZ, 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")) @Redirect(method = "renderLitParticles", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/particle/Particle;renderParticle(Lnet/minecraft/client/renderer/BufferBuilder;Lnet/minecraft/entity/Entity;FFFFFF)V"))
private void renderLitParticle(Particle particle, VertexBuffer vertexBuffer, Entity view, float partialTicks, private void renderLitParticle(Particle particle, BufferBuilder vertexBuffer, Entity view, float partialTicks,
float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) { float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
renderParticle(particle, vertexBuffer, view, partialTicks, rotX, rotXZ, rotZ, rotYZ, rotXY); renderParticle(particle, vertexBuffer, view, partialTicks, rotX, rotXZ, rotZ, rotYZ, rotXY);
} }
private void renderParticle(Particle particle, VertexBuffer vertexBuffer, Entity view, float partialTicks, private void renderParticle(Particle particle, BufferBuilder vertexBuffer, Entity view, float partialTicks,
float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) { float rotX, float rotXZ, float rotZ, float rotYZ, float rotXY) {
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) {

View File

@@ -1,6 +1,8 @@
package com.replaymod.render.rendering; package com.replaymod.render.rendering;
import com.replaymod.core.utils.WrappedTimer;
import com.replaymod.pathing.player.AbstractTimelinePlayer; import com.replaymod.pathing.player.AbstractTimelinePlayer;
import com.replaymod.pathing.player.ReplayTimer;
import com.replaymod.pathing.properties.TimestampProperty; import com.replaymod.pathing.properties.TimestampProperty;
import com.replaymod.render.RenderSettings; import com.replaymod.render.RenderSettings;
import com.replaymod.render.ReplayModRender; import com.replaymod.render.ReplayModRender;
@@ -55,6 +57,7 @@ public class VideoRenderer implements RenderInfo {
private boolean debugInfoWasShown; private boolean debugInfoWasShown;
private Map<SoundCategory, Float> originalSoundLevels; private Map<SoundCategory, Float> originalSoundLevels;
private TimelinePlayer timelinePlayer;
private Future<Void> timelinePlayerFuture; private Future<Void> timelinePlayerFuture;
private ChunkLoadingRenderGlobal chunkLoadingRenderGlobal; private ChunkLoadingRenderGlobal chunkLoadingRenderGlobal;
@@ -109,8 +112,8 @@ public class VideoRenderer implements RenderInfo {
if (videoStart > 1000) { if (videoStart > 1000) {
int replayTime = videoStart - 1000; int replayTime = videoStart - 1000;
timer.elapsedPartialTicks = timer.renderPartialTicks = 0; timer.renderPartialTicks = 0;
timer.timerSpeed = 1; timer.tickLength = WrappedTimer.DEFAULT_MS_PER_TICK;
while (replayTime < videoStart) { while (replayTime < videoStart) {
timer.elapsedTicks = 1; timer.elapsedTicks = 1;
replayTime += 50; replayTime += 50;
@@ -174,7 +177,8 @@ public class VideoRenderer implements RenderInfo {
} }
private void setup() { private void setup() {
timelinePlayerFuture = new TimelinePlayer(replayHandler).start(timeline); timelinePlayer = new TimelinePlayer(replayHandler);
timelinePlayerFuture = timelinePlayer.start(timeline);
if (!OpenGlHelper.isFramebufferEnabled()) { if (!OpenGlHelper.isFramebufferEnabled()) {
Display.setResizable(false); Display.setResizable(false);
@@ -226,6 +230,8 @@ public class VideoRenderer implements RenderInfo {
if (!timelinePlayerFuture.isDone()) { if (!timelinePlayerFuture.isDone()) {
timelinePlayerFuture.cancel(false); timelinePlayerFuture.cancel(false);
} }
// Tear down of the timeline player might only happen the next tick after it was cancelled
timelinePlayer.onTick(new ReplayTimer.UpdatedEvent());
if (!OpenGlHelper.isFramebufferEnabled()) { if (!OpenGlHelper.isFramebufferEnabled()) {
Display.setResizable(true); Display.setResizable(true);

View File

@@ -0,0 +1,12 @@
package com.replaymod.render.utils;
import com.replaymod.render.RenderSettings;
import com.replaymod.replaystudio.pathing.path.Timeline;
import lombok.Data;
@Data
public class RenderJob {
private String name;
private Timeline timeline;
private RenderSettings settings;
}

View File

@@ -147,7 +147,7 @@ public class InputReplayTimer extends WrappedTimer {
// TODO: Translate magic values to Keyboard.KEY_ constants // TODO: Translate magic values to Keyboard.KEY_ constants
if (key == 32 && Keyboard.isKeyDown(61) && mc.ingameGUI != null) { if (key == 32 && Keyboard.isKeyDown(61) && mc.ingameGUI != null) {
mc.ingameGUI.getChatGUI().clearChatMessages(); mc.ingameGUI.getChatGUI().clearChatMessages(false);
} }
if (key == 31 && Keyboard.isKeyDown(61)) { if (key == 31 && Keyboard.isKeyDown(61)) {

View File

@@ -3,6 +3,7 @@ package com.replaymod.replay;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.mojang.authlib.GameProfile; import com.mojang.authlib.GameProfile;
import com.replaymod.core.utils.Restrictions; import com.replaymod.core.utils.Restrictions;
import com.replaymod.core.utils.WrappedTimer;
import com.replaymod.replay.camera.CameraEntity; import com.replaymod.replay.camera.CameraEntity;
import com.replaymod.replay.camera.SpectatorCameraController; import com.replaymod.replay.camera.SpectatorCameraController;
import com.replaymod.replay.events.ReplayCloseEvent; import com.replaymod.replay.events.ReplayCloseEvent;
@@ -110,16 +111,16 @@ public class ReplayHandler {
channel.close().awaitUninterruptibly(); channel.close().awaitUninterruptibly();
if (mc.thePlayer instanceof CameraEntity) { if (mc.player instanceof CameraEntity) {
mc.thePlayer.setDead(); mc.player.setDead();
} }
if (mc.theWorld != null) { if (mc.world != null) {
mc.theWorld.sendQuittingDisconnectingPacket(); mc.world.sendQuittingDisconnectingPacket();
mc.loadWorld(null); mc.loadWorld(null);
} }
mc.timer.timerSpeed = 1; mc.timer.tickLength = WrappedTimer.DEFAULT_MS_PER_TICK;
overlay.setVisible(false); overlay.setVisible(false);
ReplayModReplay.instance.replayHandler = null; ReplayModReplay.instance.replayHandler = null;
@@ -128,7 +129,7 @@ public class ReplayHandler {
} }
private void setup() { private void setup() {
mc.ingameGUI.getChatGUI().clearChatMessages(); mc.ingameGUI.getChatGUI().clearChatMessages(true);
NetworkManager networkManager = new NetworkManager(EnumPacketDirection.CLIENTBOUND) { NetworkManager networkManager = new NetworkManager(EnumPacketDirection.CLIENTBOUND) {
@Override @Override
@@ -141,13 +142,14 @@ public class ReplayHandler {
networkManager.setNetHandler(netHandlerPlayClient); networkManager.setNetHandler(netHandlerPlayClient);
FMLClientHandler.instance().setPlayClient(netHandlerPlayClient); FMLClientHandler.instance().setPlayClient(netHandlerPlayClient);
channel = new EmbeddedChannel(networkManager); channel = new EmbeddedChannel();
NetworkDispatcher networkDispatcher = new NetworkDispatcher(networkManager); NetworkDispatcher networkDispatcher = new NetworkDispatcher(networkManager);
channel.attr(NetworkDispatcher.FML_DISPATCHER).set(networkDispatcher); channel.attr(NetworkDispatcher.FML_DISPATCHER).set(networkDispatcher);
channel.pipeline().addFirst("ReplayModReplay_replaySender", replaySender); channel.pipeline().addFirst("ReplayModReplay_replaySender", replaySender);
channel.pipeline().addAfter("ReplayModReplay_replaySender", "fml:packet_handler", networkDispatcher); channel.pipeline().addLast("packet_handler", networkManager);
channel.pipeline().fireChannelActive(); channel.pipeline().fireChannelActive();
networkDispatcher.clientToServerHandshake();
} }
public ReplayFile getReplayFile() { public ReplayFile getReplayFile() {
@@ -209,6 +211,9 @@ public class ReplayHandler {
*/ */
public void spectateEntity(Entity e) { public void spectateEntity(Entity e) {
CameraEntity cameraEntity = getCameraEntity(); CameraEntity cameraEntity = getCameraEntity();
if (cameraEntity == null) {
return; // Cannot spectate if we have no camera
}
if (e == null || e == cameraEntity) { if (e == null || e == cameraEntity) {
spectating = null; spectating = null;
e = cameraEntity; e = cameraEntity;
@@ -241,7 +246,7 @@ public class ReplayHandler {
* @return {@code true} if the camera is the view entity, {@code false} otherwise * @return {@code true} if the camera is the view entity, {@code false} otherwise
*/ */
public boolean isCameraView() { public boolean isCameraView() {
return mc.thePlayer instanceof CameraEntity && mc.thePlayer == mc.getRenderViewEntity(); return mc.player instanceof CameraEntity && mc.player == mc.getRenderViewEntity();
} }
/** /**
@@ -249,7 +254,7 @@ public class ReplayHandler {
* @return The camera entity or {@code null} if it does not yet exist * @return The camera entity or {@code null} if it does not yet exist
*/ */
public CameraEntity getCameraEntity() { public CameraEntity getCameraEntity() {
return mc.thePlayer instanceof CameraEntity ? (CameraEntity) mc.thePlayer : null; return mc.player instanceof CameraEntity ? (CameraEntity) mc.player : null;
} }
public UUID getSpectatedUUID() { public UUID getSpectatedUUID() {
@@ -296,7 +301,7 @@ public class ReplayHandler {
@Override @Override
public void drawScreen(int mouseX, int mouseY, float partialTicks) { public void drawScreen(int mouseX, int mouseY, float partialTicks) {
drawBackground(0); drawBackground(0);
drawCenteredString(fontRendererObj, I18n.format("replaymod.gui.pleasewait"), drawCenteredString(fontRenderer, I18n.format("replaymod.gui.pleasewait"),
width / 2, height / 2, 0xffffffff); width / 2, height / 2, 0xffffffff);
} }
}; };
@@ -329,7 +334,7 @@ public class ReplayHandler {
replaySender.setReplaySpeed(0); replaySender.setReplaySpeed(0);
mc.getConnection().getNetworkManager().processReceivedPackets(); mc.getConnection().getNetworkManager().processReceivedPackets();
for (Entity entity : mc.theWorld.loadedEntityList) { for (Entity entity : mc.world.loadedEntityList) {
if (entity instanceof EntityOtherPlayerMP) { if (entity instanceof EntityOtherPlayerMP) {
EntityOtherPlayerMP e = (EntityOtherPlayerMP) entity; EntityOtherPlayerMP e = (EntityOtherPlayerMP) entity;
e.setPosition(e.otherPlayerMPX, e.otherPlayerMPY, e.otherPlayerMPZ); e.setPosition(e.otherPlayerMPX, e.otherPlayerMPY, e.otherPlayerMPZ);

View File

@@ -6,7 +6,11 @@ import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.replaymod.core.ReplayMod; import com.replaymod.core.ReplayMod;
import com.replaymod.core.utils.ModCompat; import com.replaymod.core.utils.ModCompat;
import com.replaymod.replay.camera.*; import com.replaymod.replay.camera.CameraController;
import com.replaymod.replay.camera.CameraControllerRegistry;
import com.replaymod.replay.camera.CameraEntity;
import com.replaymod.replay.camera.ClassicCameraController;
import com.replaymod.replay.camera.VanillaCameraController;
import com.replaymod.replay.gui.overlay.GuiMarkerTimeline; import com.replaymod.replay.gui.overlay.GuiMarkerTimeline;
import com.replaymod.replay.gui.screen.GuiModCompatWarning; import com.replaymod.replay.gui.screen.GuiModCompatWarning;
import com.replaymod.replay.handler.GuiHandler; import com.replaymod.replay.handler.GuiHandler;
@@ -28,15 +32,17 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Optional; import java.util.Optional;
@Mod(modid = ReplayModReplay.MOD_ID, useMetadata = true) @Mod(modid = ReplayModReplay.MOD_ID,
version = "@MOD_VERSION@",
acceptedMinecraftVersions = "@MC_VERSION@",
useMetadata = true)
public class ReplayModReplay { public class ReplayModReplay {
public static final String MOD_ID = "replaymod-replay"; public static final String MOD_ID = "replaymod-replay";
@Mod.Instance(MOD_ID) @Mod.Instance(MOD_ID)
public static ReplayModReplay instance; public static ReplayModReplay instance;
@Mod.Instance(ReplayMod.MOD_ID) private ReplayMod core;
private static ReplayMod core;
private final CameraControllerRegistry cameraControllerRegistry = new CameraControllerRegistry(); private final CameraControllerRegistry cameraControllerRegistry = new CameraControllerRegistry();
@@ -51,6 +57,7 @@ public class ReplayModReplay {
@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);
@@ -96,6 +103,7 @@ public class ReplayModReplay {
@Override @Override
public void onSuccess(NoGuiScreenshot result) { public void onSuccess(NoGuiScreenshot result) {
try { try {
core.printInfoToChat("replaymod.chat.savingthumb");
replayHandler.getReplayFile().writeThumb(result.getImage()); replayHandler.getReplayFile().writeThumb(result.getImage());
core.printInfoToChat("replaymod.chat.savedthumb"); core.printInfoToChat("replaymod.chat.savedthumb");
} catch (IOException e) { } catch (IOException e) {
@@ -106,6 +114,7 @@ public class ReplayModReplay {
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
t.printStackTrace(); t.printStackTrace();
core.printWarningToChat("replaymod.chat.failedthumb");
} }
}); });
} }

View File

@@ -4,6 +4,7 @@ import com.google.common.base.Preconditions;
import com.google.common.io.Files; import com.google.common.io.Files;
import com.google.common.util.concurrent.ListenableFutureTask; import com.google.common.util.concurrent.ListenableFutureTask;
import com.replaymod.core.utils.Restrictions; import com.replaymod.core.utils.Restrictions;
import com.replaymod.core.utils.WrappedTimer;
import com.replaymod.replay.camera.CameraEntity; import com.replaymod.replay.camera.CameraEntity;
import com.replaymod.replaystudio.replay.ReplayFile; import com.replaymod.replaystudio.replay.ReplayFile;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
@@ -257,8 +258,8 @@ public class ReplaySender extends ChannelInboundHandlerAdapter {
// Entity#addedToChunk is not set and it is therefore not updated every tick. // Entity#addedToChunk is not set and it is therefore not updated every tick.
// To counteract this, we need to manually update it's position if it hasn't been added // To counteract this, we need to manually update it's position if it hasn't been added
// to any chunk yet. // to any chunk yet.
if (mc.theWorld != null) { if (mc.world != null) {
for (EntityPlayer playerEntity : mc.theWorld.playerEntities) { for (EntityPlayer playerEntity : mc.world.playerEntities) {
if (!playerEntity.addedToChunk && playerEntity instanceof EntityOtherPlayerMP) { if (!playerEntity.addedToChunk && playerEntity instanceof EntityOtherPlayerMP) {
playerEntity.onLivingUpdate(); playerEntity.onLivingUpdate();
} }
@@ -292,7 +293,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter {
// If we do not give minecraft time to tick, there will be dead entity artifacts left in the world // If we do not give minecraft time to tick, there will be dead entity artifacts left in the world
// Therefore we have to remove all loaded, dead entities manually if we are in sync mode. // Therefore we have to remove all loaded, dead entities manually if we are in sync mode.
// We do this after every SpawnX packet and after the destroy entities packet. // We do this after every SpawnX packet and after the destroy entities packet.
if (!asyncMode && mc.theWorld != null) { if (!asyncMode && mc.world != null) {
if (p instanceof SPacketSpawnPlayer if (p instanceof SPacketSpawnPlayer
|| p instanceof SPacketSpawnObject || p instanceof SPacketSpawnObject
|| p instanceof SPacketSpawnMob || p instanceof SPacketSpawnMob
@@ -300,7 +301,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter {
|| p instanceof SPacketSpawnPainting || p instanceof SPacketSpawnPainting
|| p instanceof SPacketSpawnExperienceOrb || p instanceof SPacketSpawnExperienceOrb
|| p instanceof SPacketDestroyEntities) { || p instanceof SPacketDestroyEntities) {
World world = mc.theWorld; World world = mc.world;
for (int i = 0; i < world.loadedEntityList.size(); ++i) { for (int i = 0; i < world.loadedEntityList.size(); ++i) {
Entity entity = world.loadedEntityList.get(i); Entity entity = world.loadedEntityList.get(i);
if (entity.isDead) { if (entity.isDead) {
@@ -331,7 +332,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter {
ByteBuf bb = Unpooled.wrappedBuffer(bytes); ByteBuf bb = Unpooled.wrappedBuffer(bytes);
PacketBuffer pb = new PacketBuffer(bb); PacketBuffer pb = new PacketBuffer(bb);
int i = pb.readVarIntFromBuffer(); int i = pb.readVarInt();
Packet p = EnumConnectionState.PLAY.getPacket(EnumPacketDirection.CLIENTBOUND, i); Packet p = EnumConnectionState.PLAY.getPacket(EnumPacketDirection.CLIENTBOUND, i);
p.readPacketData(pb); p.readPacketData(pb);
@@ -396,7 +397,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter {
if (!file.exists()) { if (!file.exists()) {
IOUtils.copy(replayFile.getResourcePack(hash).get(), new FileOutputStream(file)); IOUtils.copy(replayFile.getResourcePack(hash).get(), new FileOutputStream(file));
} }
mc.getResourcePackRepository().setResourcePackInstance(file); mc.getResourcePackRepository().setServerResourcePack(file);
} }
} }
return null; return null;
@@ -460,7 +461,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter {
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public Void call() { public Void call() {
if (mc.theWorld == null || !mc.isCallingFromMinecraftThread()) { if (mc.world == null || !mc.isCallingFromMinecraftThread()) {
synchronized(mc.scheduledTasks) { synchronized(mc.scheduledTasks) {
mc.scheduledTasks.add(ListenableFutureTask.create(this)); mc.scheduledTasks.add(ListenableFutureTask.create(this));
} }
@@ -518,7 +519,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter {
* @return {@code true} if it is paused, {@code false} otherwise * @return {@code true} if it is paused, {@code false} otherwise
*/ */
public boolean paused() { public boolean paused() {
return mc.timer.timerSpeed == 0; return mc.timer.tickLength == Float.POSITIVE_INFINITY;
} }
/** /**
@@ -538,7 +539,7 @@ public class ReplaySender extends ChannelInboundHandlerAdapter {
*/ */
public void setReplaySpeed(final double d) { public void setReplaySpeed(final double d) {
if(d != 0) this.replaySpeed = d; if(d != 0) this.replaySpeed = d;
mc.timer.timerSpeed = (float) d; mc.timer.tickLength = WrappedTimer.DEFAULT_MS_PER_TICK / (float) d;
} }
///////////////////////////////////////////////////////// /////////////////////////////////////////////////////////

View File

@@ -19,6 +19,7 @@ import net.minecraft.entity.EntityLiving;
import net.minecraft.entity.item.EntityItemFrame; import net.minecraft.entity.item.EntityItemFrame;
import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.inventory.EntityEquipmentSlot; import net.minecraft.inventory.EntityEquipmentSlot;
import net.minecraft.stats.RecipeBook;
import net.minecraft.stats.StatisticsManager; import net.minecraft.stats.StatisticsManager;
import net.minecraft.util.EnumHand; import net.minecraft.util.EnumHand;
import net.minecraft.util.ResourceLocation; import net.minecraft.util.ResourceLocation;
@@ -39,7 +40,7 @@ import java.util.function.Function;
/** /**
* The camera entity used as the main player entity during replay viewing. * The camera entity used as the main player entity during replay viewing.
* During a replay {@link Minecraft#thePlayer} should be an instance of this class. * During a replay {@link Minecraft#player} should be an instance of this class.
* Camera movement is controlled by a separate {@link CameraController}. * Camera movement is controlled by a separate {@link CameraController}.
*/ */
public class CameraEntity extends EntityPlayerSP { public class CameraEntity extends EntityPlayerSP {
@@ -66,8 +67,8 @@ public class CameraEntity extends EntityPlayerSP {
*/ */
private final EventHandler eventHandler = new EventHandler(); private final EventHandler eventHandler = new EventHandler();
public CameraEntity(Minecraft mcIn, World worldIn, NetHandlerPlayClient netHandlerPlayClient, StatisticsManager statisticsManager) { public CameraEntity(Minecraft mcIn, World worldIn, NetHandlerPlayClient netHandlerPlayClient, StatisticsManager statisticsManager, RecipeBook recipeBook) {
super(mcIn, worldIn, netHandlerPlayClient, statisticsManager); super(mcIn, worldIn, netHandlerPlayClient, statisticsManager, recipeBook);
MinecraftForge.EVENT_BUS.register(eventHandler); MinecraftForge.EVENT_BUS.register(eventHandler);
if (ReplayModReplay.instance.getReplayHandler().getSpectatedUUID() == null) { if (ReplayModReplay.instance.getReplayHandler().getSpectatedUUID() == null) {
cameraController = ReplayModReplay.instance.createCameraController(this); cameraController = ReplayModReplay.instance.createCameraController(this);
@@ -156,9 +157,9 @@ public class CameraEntity extends EntityPlayerSP {
// entity is recreated and we have to spectate a new entity // entity is recreated and we have to spectate a new entity
UUID spectating = ReplayModReplay.instance.getReplayHandler().getSpectatedUUID(); UUID spectating = ReplayModReplay.instance.getReplayHandler().getSpectatedUUID();
if (spectating != null && (view.getUniqueID() != spectating if (spectating != null && (view.getUniqueID() != spectating
|| view.worldObj != worldObj) || view.world != world)
|| worldObj.getEntityByID(view.getEntityId()) != view) { || world.getEntityByID(view.getEntityId()) != view) {
view = worldObj.getPlayerEntityByUUID(spectating); view = world.getPlayerEntityByUUID(spectating);
if (view != null) { if (view != null) {
mc.setRenderViewEntity(view); mc.setRenderViewEntity(view);
} else { } else {
@@ -177,17 +178,17 @@ public class CameraEntity extends EntityPlayerSP {
@Override @Override
public void preparePlayerToSpawn() { public void preparePlayerToSpawn() {
// Make sure our world is up-to-date in case of world changes // Make sure our world is up-to-date in case of world changes
if (mc.theWorld != null) { if (mc.world != null) {
worldObj = mc.theWorld; world = mc.world;
} }
super.preparePlayerToSpawn(); super.preparePlayerToSpawn();
} }
@Override @Override
public void setAngles(float yaw, float pitch) { public void setRotation(float yaw, float pitch) {
if (mc.getRenderViewEntity() == this) { if (mc.getRenderViewEntity() == this) {
// Only update camera rotation when the camera is the view // Only update camera rotation when the camera is the view
super.setAngles(yaw, pitch); super.setRotation(yaw, pitch);
} }
} }
@@ -252,6 +253,15 @@ public class CameraEntity extends EntityPlayerSP {
return false; return false;
} }
@Override
public boolean isInvisible() {
Entity view = mc.getRenderViewEntity();
if (view != this) {
return view.isInvisible();
}
return super.isInvisible();
}
@Override @Override
public ResourceLocation getLocationSkin() { public ResourceLocation getLocationSkin() {
Entity view = mc.getRenderViewEntity(); Entity view = mc.getRenderViewEntity();
@@ -439,8 +449,8 @@ public class CameraEntity extends EntityPlayerSP {
mc.entityRenderer.itemRenderer.itemStackOffHand = player.getItemStackFromSlot(EntityEquipmentSlot.OFFHAND); mc.entityRenderer.itemRenderer.itemStackOffHand = player.getItemStackFromSlot(EntityEquipmentSlot.OFFHAND);
mc.thePlayer.renderArmYaw = mc.thePlayer.prevRenderArmYaw = player.rotationYaw; mc.player.renderArmYaw = mc.player.prevRenderArmYaw = player.rotationYaw;
mc.thePlayer.renderArmPitch = mc.thePlayer.prevRenderArmPitch = player.rotationPitch; mc.player.renderArmPitch = mc.player.prevRenderArmPitch = player.rotationPitch;
} }
} }
} }

View File

@@ -132,7 +132,7 @@ public class ClassicCameraController implements CameraController {
Vec3d movement = direction.normalize(); Vec3d movement = direction.normalize();
double factor = motion * (frac / 1000D); double factor = motion * (frac / 1000D);
camera.moveCamera(movement.xCoord * factor, movement.yCoord * factor, movement.zCoord * factor); camera.moveCamera(movement.x * factor, movement.y * factor, movement.z * factor);
} }
private void setMovement(MoveDirection dir) { private void setMovement(MoveDirection dir) {

View File

@@ -16,6 +16,10 @@ import org.lwjgl.input.Keyboard;
import org.lwjgl.util.ReadablePoint; import org.lwjgl.util.ReadablePoint;
public class GuiEditMarkerPopup extends AbstractGuiPopup<GuiEditMarkerPopup> implements Typeable { public class GuiEditMarkerPopup extends AbstractGuiPopup<GuiEditMarkerPopup> implements Typeable {
private static GuiNumberField newGuiNumberField() {
return new GuiNumberField().setSize(150, 20).setValidateOnFocusChange(true);
}
private final ReplayHandler replayHandler; private final ReplayHandler replayHandler;
private final Marker marker; private final Marker marker;
@@ -23,15 +27,15 @@ public class GuiEditMarkerPopup extends AbstractGuiPopup<GuiEditMarkerPopup> imp
public final GuiTextField nameField = new GuiTextField().setSize(150, 20); public final GuiTextField nameField = new GuiTextField().setSize(150, 20);
// TODO: Replace with a min/sec/msec field // TODO: Replace with a min/sec/msec field
public final GuiNumberField timeField = new GuiNumberField().setSize(150, 20).setPrecision(0); public final GuiNumberField timeField = newGuiNumberField().setPrecision(0);
public final GuiNumberField xField = new GuiNumberField().setSize(150, 20).setPrecision(10); public final GuiNumberField xField = newGuiNumberField().setPrecision(10);
public final GuiNumberField yField = new GuiNumberField().setSize(150, 20).setPrecision(10); public final GuiNumberField yField = newGuiNumberField().setPrecision(10);
public final GuiNumberField zField = new GuiNumberField().setSize(150, 20).setPrecision(10); public final GuiNumberField zField = newGuiNumberField().setPrecision(10);
public final GuiNumberField yawField = new GuiNumberField().setSize(150, 20).setPrecision(5); public final GuiNumberField yawField = newGuiNumberField().setPrecision(5);
public final GuiNumberField pitchField = new GuiNumberField().setSize(150, 20).setPrecision(5); public final GuiNumberField pitchField = newGuiNumberField().setPrecision(5);
public final GuiNumberField rollField = new GuiNumberField().setSize(150, 20).setPrecision(5); public final GuiNumberField rollField = newGuiNumberField().setPrecision(5);
public final GuiPanel inputs = GuiPanel.builder() public final GuiPanel inputs = GuiPanel.builder()
.layout(new GridLayout().setColumns(2).setSpacingX(7).setSpacingY(3)) .layout(new GridLayout().setColumns(2).setSpacingX(7).setSpacingY(3))

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