├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── impl-abstraction ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── io │ │ └── github │ │ └── openminigameserver │ │ └── replay │ │ ├── InteropExtension.kt │ │ ├── ReplayManager.kt │ │ ├── TickTime.kt │ │ ├── TimeUnit.kt │ │ ├── abstraction │ │ ├── ReplayActionItemStack.kt │ │ ├── ReplayChunk.kt │ │ ├── ReplayEntity.kt │ │ ├── ReplayGameMode.kt │ │ ├── ReplayHeadTextureSkin.kt │ │ ├── ReplayUser.kt │ │ └── ReplayWorld.kt │ │ ├── commands │ │ ├── ReplayCommand.kt │ │ ├── StartRecordingCommand.kt │ │ └── StopRecordingCommand.kt │ │ ├── platform │ │ ├── IdHelperContainer.kt │ │ ├── ReplayExtension.kt │ │ ├── ReplayExtensionSettings.kt │ │ └── ReplayPlatform.kt │ │ ├── recorder │ │ ├── PositionRecordType.kt │ │ ├── RecorderOptions.kt │ │ └── ReplayRecorder.kt │ │ └── replayer │ │ ├── ActionPlayer.kt │ │ ├── ActionPlayerManager.kt │ │ ├── EntityActionPlayer.kt │ │ ├── IEntityManager.kt │ │ ├── ReplaySession.kt │ │ ├── ReplaySessionTimeStepHelper.kt │ │ ├── ReplayTicker.kt │ │ └── statehelper │ │ ├── ControlItemAction.kt │ │ ├── ReplaySessionPlayerStateHelper.kt │ │ ├── constants │ │ ├── PlayerHeadsItems.kt │ │ ├── PlayerHeadsTextureData.kt │ │ └── StateHelperConstants.kt │ │ └── utils │ │ └── ReplayStatePlayerData.kt │ └── template │ └── kotlin │ └── io │ └── github │ └── openminigameserver │ └── replay │ └── BuildInfo.kt ├── impl-minestom ├── build.gradle.kts └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── openminigameserver │ │ │ └── replay │ │ │ └── mixins │ │ │ ├── InstanceMixin.java │ │ │ ├── PacketUtilsMixin.java │ │ │ ├── PlayerInventoryMixin.java │ │ │ └── PlayerMixin.java │ ├── kotlin │ │ └── io │ │ │ └── github │ │ │ └── openminigameserver │ │ │ └── replay │ │ │ ├── MinestomReplayExtension.kt │ │ │ ├── ReplayListener.kt │ │ │ ├── extensions │ │ │ └── MinestomInteropExtensions.kt │ │ │ ├── helpers │ │ │ ├── EntityHelper.kt │ │ │ ├── EntityManager.kt │ │ │ └── ReplayPlayerEntity.kt │ │ │ ├── platform │ │ │ └── minestom │ │ │ │ ├── ItemStackExtensions.kt │ │ │ │ ├── MinestomActionPlayerManager.kt │ │ │ │ ├── MinestomReplayChunk.kt │ │ │ │ ├── MinestomReplayEntity.kt │ │ │ │ ├── MinestomReplayPlatform.kt │ │ │ │ ├── MinestomReplayUser.kt │ │ │ │ ├── MinestomReplayWorld.kt │ │ │ │ ├── ReplayCommandManager.kt │ │ │ │ └── replayer │ │ │ │ ├── MinestomActionPlayer.kt │ │ │ │ ├── MinestomEntityActionPlayer.kt │ │ │ │ └── PlayerInventoryHelper.kt │ │ │ └── replayer │ │ │ ├── ReplayChunkLoader.kt │ │ │ └── impl │ │ │ ├── RecBlockBreakAnimationPlayer.kt │ │ │ ├── RecBlockEffectPlayer.kt │ │ │ ├── RecBlockStateBatchUpdatePlayer.kt │ │ │ ├── RecBlockStateUpdatePlayer.kt │ │ │ ├── RecEntitiesPositionPlayer.kt │ │ │ ├── RecEntityEquipmentUpdatePlayer.kt │ │ │ ├── RecEntityMetadataPlayer.kt │ │ │ ├── RecEntityMovePlayer.kt │ │ │ ├── RecEntityRemovePlayer.kt │ │ │ ├── RecEntitySpawnPlayer.kt │ │ │ ├── RecParticleEffectPlayer.kt │ │ │ ├── RecPlayerHandAnimationPlayer.kt │ │ │ └── RecSoundEffectPlayer.kt │ └── resources │ │ ├── extension.json │ │ └── mixins.replay.json │ ├── template │ └── kotlin │ │ └── io │ │ └── github │ │ └── openminigameserver │ │ └── replay │ │ └── BuildInfo.kt │ └── test │ └── kotlin │ └── io │ └── github │ └── openminigameserver │ └── replay │ └── test │ ├── EntityCommand.kt │ ├── MyChunkGenerator.kt │ ├── PlayerInit.kt │ ├── Replay.kt │ └── ReplayBootstrapper.kt ├── jitpack.yml ├── model ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── io │ └── github │ └── openminigameserver │ └── replay │ ├── AbstractReplaySession.kt │ ├── io │ ├── ReplayFile.kt │ └── format │ │ ├── ReadByteBufExtensions.kt │ │ └── WriteByteBufExtensions.kt │ └── model │ ├── Replay.kt │ ├── ReplayHeader.kt │ ├── recordable │ ├── EntityRecordableAction.kt │ ├── RecordableAction.kt │ ├── RecordableItemStack.kt │ ├── RecordablePosition.kt │ ├── RecordablePositionAndVector.kt │ ├── RecordableVector.kt │ ├── RecordedChunk.kt │ ├── entity │ │ ├── EntityEquipmentSlot.kt │ │ ├── RecordableEntity.kt │ │ └── data │ │ │ ├── BaseEntityData.kt │ │ │ ├── EquipmentEntityData.kt │ │ │ ├── PlayerEntityData.kt │ │ │ └── PlayerSkinData.kt │ ├── impl │ │ ├── RecBlockBreakAnimation.kt │ │ ├── RecBlockEffect.kt │ │ ├── RecBlockStateBatchUpdate.kt │ │ ├── RecBlockStateUpdate.kt │ │ ├── RecEntitiesPosition.kt │ │ ├── RecEntityEquipmentUpdate.kt │ │ ├── RecEntityMetadata.kt │ │ ├── RecEntityMove.kt │ │ ├── RecEntityRemove.kt │ │ ├── RecEntitySpawn.kt │ │ ├── RecParticleEffect.kt │ │ ├── RecPlayerHandAnimation.kt │ │ └── RecSoundEffect.kt │ └── reverse │ │ ├── ApplyLastReversible.kt │ │ ├── DefaultStateReversible.kt │ │ └── Reversible.kt │ └── storage │ ├── CompletableFutureReplayStorageSystem.kt │ ├── FileReplayStorageSystem.kt │ ├── MemoryReplayStorageSystem.kt │ ├── ReplayStorageSystem.kt │ └── ReplayStorageSystemUtils.kt └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/java,kotlin,gradle,intellij+all 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=java,kotlin,gradle,intellij+all 4 | 5 | ### Intellij+all ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/artifacts 37 | # .idea/compiler.xml 38 | # .idea/jarRepositories.xml 39 | # .idea/modules.xml 40 | # .idea/*.iml 41 | # .idea/modules 42 | # *.iml 43 | # *.ipr 44 | 45 | # CMake 46 | cmake-build-*/ 47 | 48 | # Mongo Explorer plugin 49 | .idea/**/mongoSettings.xml 50 | 51 | # File-based project format 52 | *.iws 53 | 54 | # IntelliJ 55 | out/ 56 | 57 | # mpeltonen/sbt-idea plugin 58 | .idea_modules/ 59 | 60 | # JIRA plugin 61 | atlassian-ide-plugin.xml 62 | 63 | # Cursive Clojure plugin 64 | .idea/replstate.xml 65 | 66 | # Crashlytics plugin (for Android Studio and IntelliJ) 67 | com_crashlytics_export_strings.xml 68 | crashlytics.properties 69 | crashlytics-build.properties 70 | fabric.properties 71 | 72 | # Editor-based Rest Client 73 | .idea/httpRequests 74 | 75 | # Android studio 3.1+ serialized cache file 76 | .idea/caches/build_file_checksums.ser 77 | 78 | ### Intellij+all Patch ### 79 | # Ignores the whole .idea folder and all .iml files 80 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 81 | 82 | .idea/ 83 | 84 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 85 | 86 | *.iml 87 | modules.xml 88 | .idea/misc.xml 89 | *.ipr 90 | 91 | # Sonarlint plugin 92 | .idea/sonarlint 93 | 94 | ### Java ### 95 | # Compiled class file 96 | *.class 97 | 98 | # Log file 99 | *.log 100 | 101 | # BlueJ files 102 | *.ctxt 103 | 104 | # Mobile Tools for Java (J2ME) 105 | .mtj.tmp/ 106 | 107 | # Package Files # 108 | *.jar 109 | *.war 110 | *.nar 111 | *.ear 112 | *.zip 113 | *.tar.gz 114 | *.rar 115 | 116 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 117 | hs_err_pid* 118 | 119 | ### Kotlin ### 120 | # Compiled class file 121 | 122 | # Log file 123 | 124 | # BlueJ files 125 | 126 | # Mobile Tools for Java (J2ME) 127 | 128 | # Package Files # 129 | 130 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 131 | 132 | ### Gradle ### 133 | .gradle 134 | build/ 135 | 136 | # Ignore Gradle GUI config 137 | gradle-app.setting 138 | 139 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 140 | !gradle-wrapper.jar 141 | 142 | # Cache of project 143 | .gradletasknamecache 144 | 145 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 146 | # gradle/wrapper/gradle-wrapper.properties 147 | 148 | ### Gradle Patch ### 149 | **/build/ 150 | 151 | # End of https://www.toptal.com/developers/gitignore/api/java,kotlin,gradle,intellij+all 152 | 153 | run/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Replay 2 | A replay system for Bukkit and Minestom servers. 3 | 4 | ![YourKit](https://www.yourkit.com/images/yklogo.png) 5 | ------ 6 | YourKit supports open source projects with innovative and intelligent tools 7 | for monitoring and profiling Java and .NET applications. 8 | YourKit is the creator of YourKit Java Profiler, 9 | YourKit .NET Profiler, 10 | and YourKit YouMonitor 11 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") version "1.4.30-M1" 3 | id("com.github.johnrengelman.shadow") version "6.1.0" 4 | `maven-publish` 5 | } 6 | 7 | 8 | allprojects.forEach { 9 | 10 | it.group = "io.github.openminigameserver.Replay" 11 | it.version = "1.0-SNAPSHOT" 12 | 13 | it.apply(plugin = "kotlin") 14 | it.apply(plugin = "maven-publish") 15 | it.apply(plugin = "com.github.johnrengelman.shadow") 16 | 17 | it.repositories { 18 | mavenCentral() 19 | maven("https://jitpack.io") 20 | maven("https://libraries.minecraft.net") 21 | maven("https://repo.spongepowered.org/maven") 22 | maven("https://oss.sonatype.org/content/repositories/snapshots/") 23 | maven("https://dl.bintray.com/kotlin/kotlin-eap") 24 | maven("https://kotlin.bintray.com/kotlinx/") 25 | } 26 | 27 | 28 | it.dependencies { 29 | api(kotlin("stdlib")) 30 | 31 | api("org.jetbrains.kotlinx:kotlinx-datetime:0.1.1") 32 | api("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.2") 33 | } 34 | 35 | it.java { 36 | sourceCompatibility = JavaVersion.VERSION_11 37 | targetCompatibility = JavaVersion.VERSION_11 38 | } 39 | val compileKotlin: org.jetbrains.kotlin.gradle.tasks.KotlinCompile by it.tasks 40 | compileKotlin.kotlinOptions { 41 | jvmTarget = "11" 42 | freeCompilerArgs = 43 | freeCompilerArgs + "-Xjvm-default=enable" + "-Xopt-in=kotlin.RequiresOptIn" + "-Xopt-in=kotlin.time.ExperimentalTime" + "-Xopt-in=kotlin.contracts.ExperimentalContracts" 44 | } 45 | val compileTestKotlin: org.jetbrains.kotlin.gradle.tasks.KotlinCompile by it.tasks 46 | compileTestKotlin.kotlinOptions { 47 | jvmTarget = "11" 48 | } 49 | 50 | it.publishing { 51 | publications { 52 | create(it.project.name) { 53 | groupId = it.project.group.toString() 54 | artifactId = it.project.name 55 | version = it.project.version.toString() 56 | from(it.components["java"]) 57 | } 58 | } 59 | } 60 | 61 | if (it.tasks.findByName("install") != null) 62 | it.tasks.replace("install").dependsOn("publishToMavenLocal") 63 | } 64 | 65 | dependencies { 66 | api(project(":impl-minestom")) 67 | } 68 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenMinigameServer/Replay/a6936635d0cf03f06ccbd841f4bc2722cbaa9878/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /impl-abstraction/build.gradle.kts: -------------------------------------------------------------------------------- 1 | repositories { 2 | // for development builds 3 | maven(url = "https://oss.sonatype.org/content/repositories/snapshots/") { 4 | name = "sonatype-oss-snapshots" 5 | } 6 | } 7 | 8 | dependencies { 9 | api(project(":model")) 10 | 11 | implementation("cloud.commandframework:cloud-core:1.4.0") 12 | implementation("cloud.commandframework:cloud-annotations:1.4.0") 13 | 14 | implementation("net.kyori:adventure-api:4.4.0") 15 | } 16 | 17 | tasks { 18 | val templateContext = mapOf("version" to project.version.toString()) 19 | processResources { 20 | expand(*templateContext.toList().toTypedArray()) 21 | } 22 | 23 | create("generateKotlinBuildInfo") { 24 | inputs.properties(templateContext) // for gradle up-to-date check 25 | from("src/template/kotlin/") 26 | into("$buildDir/generated/kotlin/") 27 | expand(*templateContext.toList().toTypedArray()) 28 | } 29 | 30 | kotlin.sourceSets["main"].kotlin.srcDir("$buildDir/generated/kotlin") 31 | compileKotlin.get().dependsOn(get("generateKotlinBuildInfo")) 32 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/InteropExtension.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.runBlocking 5 | import kotlin.concurrent.thread 6 | 7 | internal fun runOnSeparateThread(code: suspend CoroutineScope.() -> Unit) { 8 | thread { 9 | runBlocking(block = code) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/ReplayManager.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay 2 | 3 | import io.github.openminigameserver.replay.model.Replay 4 | import io.github.openminigameserver.replay.model.storage.FileReplayStorageSystem 5 | import io.github.openminigameserver.replay.model.storage.ReplayStorageSystem 6 | import io.github.openminigameserver.replay.platform.ReplayExtension 7 | 8 | class ReplayManager(val extension: ReplayExtension) { 9 | var storageSystem: ReplayStorageSystem = FileReplayStorageSystem(extension.dataDir) 10 | 11 | fun createEmptyReplay() = Replay() 12 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/TickTime.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay 2 | 3 | 4 | data class TickTime(val time: Long, val unit: TimeUnit) 5 | -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/TimeUnit.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay 2 | 3 | enum class TimeUnit { 4 | TICK, DAY, HOUR, MINUTE, SECOND, MILLISECOND; 5 | 6 | fun toMilliseconds(value: Long): Long { 7 | return when (this) { 8 | TICK -> 50 * value 9 | DAY -> value * 86400000 10 | HOUR -> value * 3600000 11 | MINUTE -> value * 60000 12 | SECOND -> value * 1000 13 | MILLISECOND -> value 14 | else -> -1 // Unexpected 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/abstraction/ReplayActionItemStack.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.abstraction 2 | 3 | import io.github.openminigameserver.replay.replayer.statehelper.ControlItemAction 4 | import net.kyori.adventure.text.Component 5 | 6 | data class ReplayActionItemStack( 7 | val title: Component, 8 | val action: ControlItemAction, 9 | val skin: ReplayHeadTextureSkin? = null 10 | ) { 11 | companion object { 12 | val air = ReplayActionItemStack(Component.empty(), action = ControlItemAction.NONE) 13 | } 14 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/abstraction/ReplayChunk.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.abstraction 2 | 3 | interface ReplayChunk { 4 | val x: Int 5 | val z: Int 6 | val serializedData: ByteArray? 7 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/abstraction/ReplayEntity.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.abstraction 2 | 3 | import io.github.openminigameserver.replay.model.recordable.RecordableItemStack 4 | import io.github.openminigameserver.replay.model.recordable.RecordablePosition 5 | import io.github.openminigameserver.replay.model.recordable.RecordablePositionAndVector 6 | import io.github.openminigameserver.replay.model.recordable.RecordableVector 7 | import io.github.openminigameserver.replay.model.recordable.entity.EntityEquipmentSlot 8 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 9 | import io.github.openminigameserver.replay.platform.ReplayPlatform 10 | import java.util.* 11 | 12 | interface ReplayEntity { 13 | val id: Int 14 | val uuid: UUID 15 | val position: RecordablePosition 16 | val velocity: RecordableVector 17 | val world: ReplayWorld? 18 | val instance: ReplayWorld? get() = world 19 | 20 | fun toReplay(replayPlatform: ReplayPlatform): RecordableEntity { 21 | return RecordableEntity( 22 | id, 23 | replayPlatform.getEntityType(this), 24 | RecordablePositionAndVector(position, velocity), 25 | replayPlatform.getEntityData(this) 26 | ) 27 | } 28 | 29 | fun getEquipment(): Map 30 | fun teleport(position: RecordablePosition) 31 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/abstraction/ReplayGameMode.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.abstraction 2 | 3 | enum class ReplayGameMode { 4 | SURVIVAL, 5 | CREATIVE, 6 | ADVENTURE, 7 | SPECTATOR 8 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/abstraction/ReplayHeadTextureSkin.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.abstraction 2 | 3 | data class ReplayHeadTextureSkin(val value: String, val signature: String) 4 | -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/abstraction/ReplayUser.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.abstraction 2 | 3 | import net.kyori.adventure.audience.Audience 4 | import net.kyori.adventure.audience.ForwardingAudience 5 | 6 | /** 7 | * Represents a Player that will use the Replay System 8 | */ 9 | abstract class ReplayUser : ForwardingAudience, ReplayEntity { 10 | abstract var exp: Float 11 | abstract val heldSlot: Byte 12 | abstract var isFlying: Boolean 13 | abstract var isAllowFlying: Boolean 14 | abstract var gameMode: ReplayGameMode 15 | abstract val audience: Audience 16 | abstract val name: String 17 | 18 | override fun audiences(): Iterable { 19 | return listOf(audience) 20 | } 21 | 22 | abstract fun setWorld(instance: ReplayWorld) 23 | abstract fun clearInventory() 24 | abstract fun setHeldItemSlot(count: Byte) 25 | 26 | abstract fun setItemStack(slot: Int, itemStack: ReplayActionItemStack) 27 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/abstraction/ReplayWorld.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.abstraction 2 | 3 | import io.github.openminigameserver.replay.recorder.ReplayRecorder 4 | import io.github.openminigameserver.replay.replayer.ReplaySession 5 | import java.util.* 6 | 7 | /** 8 | * Represents a world on where a replay will be recorded or replayed 9 | */ 10 | abstract class ReplayWorld { 11 | abstract val uuid: UUID 12 | var replaySession: ReplaySession? = null 13 | var recorder: ReplayRecorder? = null 14 | 15 | abstract val entities: Iterable 16 | abstract val chunks: Iterable 17 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/commands/ReplayCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.commands 2 | 3 | import cloud.commandframework.annotations.Argument 4 | import cloud.commandframework.annotations.CommandMethod 5 | import io.github.openminigameserver.replay.ReplayManager 6 | import io.github.openminigameserver.replay.TickTime 7 | import io.github.openminigameserver.replay.TimeUnit 8 | import io.github.openminigameserver.replay.abstraction.ReplayUser 9 | import io.github.openminigameserver.replay.runOnSeparateThread 10 | import net.kyori.adventure.text.Component.text 11 | import net.kyori.adventure.text.format.NamedTextColor 12 | import java.util.* 13 | import kotlin.time.Duration 14 | 15 | object ReplayCommand { 16 | @CommandMethod("replay play ") 17 | fun playReplay(sender: ReplayUser, @Argument("id", suggestions = "replay") id: UUID, manager: ReplayManager) { 18 | var instance = sender.world ?: return 19 | if (instance.replaySession != null) { 20 | sender.sendMessage(text("You are already in a replay session.", NamedTextColor.RED)) 21 | return 22 | } 23 | runOnSeparateThread { 24 | try { 25 | sender.sendMessage(text("Attempting to load replay...", NamedTextColor.GRAY)) 26 | 27 | val replay = manager.storageSystem.loadReplay(id) 28 | 29 | if (replay == null) { 30 | sender.sendMessage(text("Please provide a valid Replay ID!", NamedTextColor.RED)) 31 | return@runOnSeparateThread 32 | } 33 | 34 | val session = manager.extension.platform.createReplaySession( 35 | replay, 36 | instance.entities.filterIsInstance().toMutableList(), 37 | instance, 38 | TickTime(1, TimeUnit.MILLISECOND) 39 | ) 40 | instance = session.world 41 | instance.replaySession = session 42 | session.init() 43 | 44 | } catch (e: Throwable) { 45 | e.printStackTrace() 46 | sender.sendMessage(text("An error occurred while trying to load your replay.", NamedTextColor.RED)) 47 | return@runOnSeparateThread 48 | } 49 | } 50 | } 51 | 52 | @CommandMethod("replay pause|start|resume") 53 | fun pauseReplay(sender: ReplayUser) { 54 | val session = sender.instance?.replaySession ?: return 55 | 56 | session.paused = !session.paused 57 | } 58 | 59 | @CommandMethod("replay exit") 60 | fun exitReplay(sender: ReplayUser) { 61 | val session = sender.instance?.replaySession ?: return 62 | 63 | session.removeViewer(sender) 64 | } 65 | 66 | @CommandMethod("replay restart") 67 | fun restartReplay(sender: ReplayUser) { 68 | val session = sender.instance?.replaySession ?: return 69 | 70 | session.time = Duration.ZERO 71 | session.paused = false 72 | } 73 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/commands/StartRecordingCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.commands 2 | 3 | import cloud.commandframework.annotations.CommandMethod 4 | import io.github.openminigameserver.replay.ReplayManager 5 | import io.github.openminigameserver.replay.abstraction.ReplayUser 6 | import io.github.openminigameserver.replay.recorder.RecorderOptions 7 | import io.github.openminigameserver.replay.recorder.ReplayRecorder 8 | import net.kyori.adventure.text.Component.text 9 | import net.kyori.adventure.text.event.ClickEvent 10 | import net.kyori.adventure.text.format.NamedTextColor 11 | import net.kyori.adventure.text.format.NamedTextColor.GOLD 12 | 13 | object StartRecordingCommand { 14 | 15 | @CommandMethod("startrecordingchunks") 16 | fun startRecordingWithChunks(sender: ReplayUser, replayManager: ReplayManager) { 17 | startRecordingReplay(sender, true, replayManager) 18 | } 19 | 20 | @CommandMethod("startrecording") 21 | fun startRecording(sender: ReplayUser, replayManager: ReplayManager) { 22 | startRecordingReplay(sender, false, replayManager) 23 | } 24 | 25 | private fun startRecordingReplay(sender: ReplayUser, recordChunks: Boolean = false, replayManager: ReplayManager) { 26 | sender.sendMessage(text("Recording started.", NamedTextColor.GREEN)) 27 | sender.sendMessage( 28 | text("Click here to stop recording.", GOLD).clickEvent(ClickEvent.runCommand("/stoprecording")) 29 | ) 30 | val recorder = ReplayRecorder( 31 | replayManager.extension, 32 | sender.instance!!, 33 | RecorderOptions(recordInstanceChunks = recordChunks) 34 | ) 35 | sender.instance!!.recorder = recorder 36 | 37 | recorder.startRecording() 38 | } 39 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/commands/StopRecordingCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.commands 2 | 3 | import cloud.commandframework.annotations.CommandMethod 4 | import io.github.openminigameserver.replay.abstraction.ReplayUser 5 | import io.github.openminigameserver.replay.platform.ReplayExtension 6 | import io.github.openminigameserver.replay.runOnSeparateThread 7 | import net.kyori.adventure.text.Component.newline 8 | import net.kyori.adventure.text.Component.text 9 | import net.kyori.adventure.text.format.NamedTextColor.* 10 | 11 | object StopRecordingCommand { 12 | 13 | @CommandMethod("stoprecording") 14 | fun stopRecording(sender: ReplayUser, extension: ReplayExtension) { 15 | val recorder = sender.world?.recorder ?: let { 16 | sender.sendMessage(text("Please start a recording.", RED)) 17 | return 18 | } 19 | sender.world?.recorder = null 20 | 21 | recorder.stopRecording() 22 | sender.sendMessage(text("Recording stopped.", GREEN)) 23 | 24 | runOnSeparateThread { 25 | extension.replayManager.storageSystem.saveReplay(recorder.replay) 26 | val message = text { 27 | 28 | recorder.replay.actions.groupBy { it.javaClass.simpleName } 29 | .forEach { actions -> 30 | it.append(text("${actions.key}: ${actions.value.count()}", GOLD)).append( 31 | newline() 32 | ) 33 | } 34 | } 35 | sender.sendMessage( 36 | text("Your replay has been created! Click here to play it!", GOLD).hoverEvent(message) 37 | ) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/platform/IdHelperContainer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.platform 2 | 3 | import java.util.concurrent.ConcurrentHashMap 4 | 5 | class IdHelperContainer(val compute: (K).() -> W) { 6 | 7 | private val values = ConcurrentHashMap() 8 | 9 | fun getOrCompute(id: K): W { 10 | var result = values[id] 11 | if (result == null) { 12 | result = compute(id) 13 | values[id] = result 14 | } 15 | return result!! 16 | } 17 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/platform/ReplayExtension.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.platform 2 | 3 | import io.github.openminigameserver.replay.BuildInfo 4 | import io.github.openminigameserver.replay.ReplayManager 5 | import io.github.openminigameserver.replay.abstraction.ReplayEntity 6 | import io.github.openminigameserver.replay.abstraction.ReplayUser 7 | import io.github.openminigameserver.replay.abstraction.ReplayWorld 8 | import io.github.openminigameserver.replay.commands.ReplayCommand 9 | import io.github.openminigameserver.replay.commands.StartRecordingCommand 10 | import io.github.openminigameserver.replay.commands.StopRecordingCommand 11 | import kotlinx.coroutines.runBlocking 12 | 13 | class ReplayExtension(var platform: ReplayPlatform) { 14 | 15 | val dataDir get() = platform.dataDir 16 | 17 | val replayManager = ReplayManager(this) 18 | 19 | fun init() { 20 | platform.log("Replay by OpenMinigameServer version ${BuildInfo.version}.") 21 | prepareCommandManager() 22 | 23 | if (platform.settings.shouldRegisterCommands) { 24 | platform.commandAnnotationParser.parse(ReplayCommand) 25 | platform.commandAnnotationParser.parse(StartRecordingCommand) 26 | platform.commandAnnotationParser.parse(StopRecordingCommand) 27 | } 28 | } 29 | 30 | private fun prepareCommandManager() { 31 | platform.commandManager.parameterInjectorRegistry().apply { 32 | registerInjector(ReplayExtension::class.java) { _, _ -> this@ReplayExtension } 33 | registerInjector(ReplayManager::class.java) { _, _ -> replayManager } 34 | } 35 | 36 | platform.commandManager.parserRegistry.registerSuggestionProvider("replay") { t, _ -> 37 | runBlocking { replayManager.storageSystem.getReplaysForPlayer(t.sender.uuid) }.map { it.toString() } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/platform/ReplayExtensionSettings.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.platform 2 | 3 | data class ReplayExtensionSettings( 4 | val shouldRegisterCommands: Boolean = true 5 | ) 6 | -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/platform/ReplayPlatform.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.platform 2 | 3 | import cloud.commandframework.CommandManager 4 | import cloud.commandframework.annotations.AnnotationParser 5 | import io.github.openminigameserver.replay.TickTime 6 | import io.github.openminigameserver.replay.TimeUnit 7 | import io.github.openminigameserver.replay.abstraction.ReplayEntity 8 | import io.github.openminigameserver.replay.abstraction.ReplayUser 9 | import io.github.openminigameserver.replay.abstraction.ReplayWorld 10 | import io.github.openminigameserver.replay.model.Replay 11 | import io.github.openminigameserver.replay.model.recordable.entity.data.BaseEntityData 12 | import io.github.openminigameserver.replay.replayer.ActionPlayerManager 13 | import io.github.openminigameserver.replay.replayer.IEntityManager 14 | import io.github.openminigameserver.replay.replayer.ReplaySession 15 | import java.io.File 16 | import java.util.* 17 | 18 | abstract class ReplayPlatform { 19 | abstract val worlds: IdHelperContainer 20 | abstract val entities: IdHelperContainer 21 | 22 | abstract val name: String 23 | abstract val version: String 24 | 25 | abstract val commandManager: CommandManager 26 | abstract val commandAnnotationParser: AnnotationParser 27 | 28 | abstract val actionPlayerManager: ActionPlayerManager 29 | 30 | abstract val dataDir: File 31 | 32 | abstract fun log(message: String) 33 | 34 | val settings = ReplayExtensionSettings() 35 | 36 | abstract fun registerSyncRepeatingTask(time: TickTime, action: () -> Unit): Any 37 | 38 | abstract fun cancelTask(tickerTask: Any) 39 | 40 | abstract fun getEntityType(replayEntity: E): String 41 | abstract fun getEntityData(replayEntity: E): BaseEntityData? 42 | 43 | abstract fun addToViewerTeam(p: P) 44 | abstract fun removeFromViewerTeam(player: P) 45 | 46 | abstract fun getWorldById(it: UUID): W 47 | abstract fun unregisterWorld(instance: W) 48 | 49 | abstract fun getEntityManager(replaySession: ReplaySession): IEntityManager 50 | 51 | abstract fun createReplaySession( 52 | replay: Replay, 53 | viewers: MutableList, 54 | instance: ReplayWorld? = null, 55 | tickTime: TickTime = TickTime(1L, TimeUnit.TICK) 56 | ): ReplaySession 57 | 58 | abstract fun getPlayerInventoryCopy(player: P): Any 59 | 60 | abstract fun loadPlayerInventoryCopy(player: P, inventory: Any) 61 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/recorder/PositionRecordType.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.recorder 2 | 3 | enum class PositionRecordType { 4 | /** 5 | * Group recorded positions into one Recordable 6 | */ 7 | GROUP_ALL, 8 | 9 | /** 10 | * Split recorded positions into multiple Recordables 11 | */ 12 | SEPARATE_ALL 13 | } 14 | -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/recorder/RecorderOptions.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.recorder 2 | 3 | data class RecorderOptions( 4 | val positionRecordType: PositionRecordType = PositionRecordType.GROUP_ALL, 5 | val recordAllLocationChanges: Boolean = true, 6 | val recordInstanceChunks: Boolean = true 7 | ) 8 | -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/recorder/ReplayRecorder.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.recorder 2 | 3 | import io.github.openminigameserver.replay.ReplayManager 4 | import io.github.openminigameserver.replay.TickTime 5 | import io.github.openminigameserver.replay.TimeUnit 6 | import io.github.openminigameserver.replay.abstraction.ReplayEntity 7 | import io.github.openminigameserver.replay.abstraction.ReplayUser 8 | import io.github.openminigameserver.replay.abstraction.ReplayWorld 9 | import io.github.openminigameserver.replay.model.recordable.RecordablePosition 10 | import io.github.openminigameserver.replay.model.recordable.RecordablePositionAndVector 11 | import io.github.openminigameserver.replay.model.recordable.RecordedChunk 12 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 13 | import io.github.openminigameserver.replay.model.recordable.impl.* 14 | import io.github.openminigameserver.replay.platform.ReplayExtension 15 | import io.github.openminigameserver.replay.platform.ReplayPlatform 16 | import io.github.openminigameserver.replay.recorder.PositionRecordType.GROUP_ALL 17 | import io.github.openminigameserver.replay.recorder.PositionRecordType.SEPARATE_ALL 18 | import java.util.* 19 | 20 | class ReplayRecorder( 21 | private val replayExtension: ReplayExtension, 22 | private val instance: ReplayWorld, 23 | private val options: RecorderOptions = RecorderOptions(), 24 | private val tickInterval: TickTime = TickTime(1L, TimeUnit.TICK) 25 | ) { 26 | private val replayManager: ReplayManager 27 | get() = replayExtension.replayManager 28 | private var tickerTask: Any 29 | val replay = replayManager.createEmptyReplay() 30 | private var isRecording = false 31 | 32 | init { 33 | tickerTask = buildTickerTask() 34 | } 35 | 36 | fun onHandSwing(entity: RecordableEntity, hand: Hand) { 37 | replay.addAction(RecPlayerHandAnimation(hand, entity)) 38 | } 39 | 40 | fun onEntityRemove(entity: RecordableEntity, position: RecordablePosition) { 41 | replay.addAction(RecEntityRemove(position, entity)) 42 | } 43 | 44 | fun onEntitySpawn(entity: RecordableEntity, positionAndVector: RecordablePositionAndVector) { 45 | replay.entities[entity.id] = entity 46 | replay.addAction( 47 | RecEntitySpawn( 48 | positionAndVector, entity 49 | ) 50 | ) 51 | } 52 | 53 | fun onEntityEquipmentChange(entity: ReplayEntity) { 54 | val replayEntity = replay.getEntityById(entity.id) ?: return 55 | 56 | replay.addAction(RecEntityEquipmentUpdate(replayEntity, entity.getEquipment())) 57 | } 58 | 59 | private fun buildTickerTask(): Any { 60 | val entityPositions = mutableMapOf() 61 | 62 | return replayExtension.platform.registerSyncRepeatingTask(tickInterval) { 63 | doEntityTick(entityPositions) 64 | } 65 | } 66 | 67 | private fun doEntityTick(entityPositions: MutableMap) { 68 | val recordedPositions = mutableMapOf() 69 | 70 | instance.entities.forEach { entity -> 71 | if (entity.instance != instance || !isRecording) return@forEach 72 | val currentPosition = entity.position 73 | 74 | val oldPosition = entityPositions[entity.uuid] 75 | val currentNewPosition = 76 | RecordablePositionAndVector(currentPosition, entity.velocity) 77 | if (oldPosition == null || oldPosition != currentNewPosition || options.recordAllLocationChanges) { 78 | val replayEntity = replay.getEntityById(entity.id) ?: return@forEach 79 | recordedPositions[replayEntity] = currentNewPosition 80 | entityPositions[entity.uuid] = currentNewPosition 81 | } 82 | } 83 | 84 | when (options.positionRecordType) { 85 | GROUP_ALL -> listOf(RecEntitiesPosition(recordedPositions)) 86 | SEPARATE_ALL -> recordedPositions.map { RecEntityMove(it.value, it.key) } 87 | }.forEach { 88 | if (it !is RecEntitiesPosition || it.positions.isNotEmpty()) 89 | replay.addAction(it) 90 | } 91 | } 92 | 93 | fun startRecording() { 94 | recordInstanceIfNeeded() 95 | instance.entities.forEach { entity -> 96 | //Save all entities 97 | replay.apply { 98 | @Suppress("UNCHECKED_CAST") 99 | entities[entity.id] = 100 | entity.toReplay(replayExtension.platform as ReplayPlatform) 101 | } 102 | } 103 | isRecording = true 104 | } 105 | 106 | private fun recordInstanceIfNeeded() { 107 | if (!options.recordInstanceChunks) return 108 | 109 | instance.chunks.forEach { 110 | replay.chunks.add( 111 | RecordedChunk( 112 | it.x, 113 | it.z, 114 | it.serializedData 115 | ?: throw Exception( 116 | "Unable to serialize chunk of type ${it.javaClass.name}.\n" + 117 | "Please make sure that your chunks can be serialized if you want to save them in the replay." 118 | ) 119 | ) 120 | ) 121 | 122 | } 123 | } 124 | 125 | fun stopRecording() { 126 | replay.duration = replay.currentDuration 127 | isRecording = false 128 | replayExtension.platform.cancelTask(tickerTask) 129 | } 130 | 131 | fun notifyBlockChange( 132 | x: Int, 133 | y: Int, 134 | z: Int, 135 | newState: Short 136 | ) { 137 | replay.addAction( 138 | RecBlockStateUpdate( 139 | RecordablePosition(x.toDouble(), y.toDouble(), z.toDouble(), 0f, 0f), 140 | newState 141 | ) 142 | ) 143 | } 144 | 145 | } 146 | 147 | 148 | -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/replayer/ActionPlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer 2 | 3 | import io.github.openminigameserver.replay.abstraction.ReplayUser 4 | import io.github.openminigameserver.replay.abstraction.ReplayWorld 5 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 6 | 7 | interface ActionPlayer { 8 | fun play(action: T, session: ReplaySession, instance: W, viewers: List

) 9 | } 10 | -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/replayer/ActionPlayerManager.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer 2 | 3 | import io.github.openminigameserver.replay.abstraction.ReplayEntity 4 | import io.github.openminigameserver.replay.abstraction.ReplayUser 5 | import io.github.openminigameserver.replay.abstraction.ReplayWorld 6 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 7 | import io.github.openminigameserver.replay.platform.ReplayPlatform 8 | 9 | open class ActionPlayerManager(val platform: ReplayPlatform) { 10 | @PublishedApi 11 | internal val actionPlayers = mutableMapOf, ActionPlayer<*, W, P>>() 12 | 13 | /** 14 | * Register an action player for the specified action type 15 | */ 16 | inline fun > registerActionPlayerGeneric(player: T) { 17 | actionPlayers[R::class.java] = player 18 | } 19 | 20 | /** 21 | * Register an action player for the specified action type 22 | */ 23 | fun > registerActionPlayer( 24 | playerInstance: T, 25 | recordableClazz: Class 26 | ) { 27 | actionPlayers[recordableClazz] = playerInstance 28 | } 29 | 30 | /** 31 | * Get an action player for the given action 32 | */ 33 | fun getActionPlayer(action: R): ActionPlayer { 34 | @Suppress("UNCHECKED_CAST") 35 | return actionPlayers[action.javaClass] as? ActionPlayer 36 | ?: throw Exception("Unable to find action player for ${action.javaClass.simpleName}") 37 | } 38 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/replayer/EntityActionPlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer 2 | 3 | import io.github.openminigameserver.replay.abstraction.ReplayEntity 4 | import io.github.openminigameserver.replay.abstraction.ReplayUser 5 | import io.github.openminigameserver.replay.abstraction.ReplayWorld 6 | import io.github.openminigameserver.replay.model.recordable.EntityRecordableAction 7 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 8 | 9 | abstract class EntityActionPlayer : 10 | ActionPlayer { 11 | @Suppress("UNCHECKED_CAST") 12 | override fun play(action: T, session: ReplaySession, instance: W, viewers: List

) { 13 | val entity = session.entityManager.getNativeEntity(action.entity) as? E 14 | if (entity != null) { 15 | play(action, action.entity, entity, session, instance, viewers) 16 | } 17 | } 18 | 19 | abstract fun play( 20 | action: T, 21 | replayEntity: RecordableEntity, 22 | nativeEntity: E, 23 | session: ReplaySession, 24 | instance: W, 25 | viewers: List

26 | ) 27 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/replayer/IEntityManager.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer 2 | 3 | import io.github.openminigameserver.replay.abstraction.ReplayEntity 4 | import io.github.openminigameserver.replay.abstraction.ReplayUser 5 | import io.github.openminigameserver.replay.model.recordable.RecordablePosition 6 | import io.github.openminigameserver.replay.model.recordable.RecordableVector 7 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 8 | import kotlin.time.Duration 9 | 10 | interface IEntityManager

{ 11 | var session: ReplaySession 12 | val entities: Collection 13 | 14 | //Replay entity id 15 | val replayEntities: MutableMap 16 | fun resetEntity(entity: RecordableEntity, startTime: Duration, targetReplayTime: Duration) 17 | fun spawnEntity( 18 | entity: RecordableEntity, 19 | position: RecordablePosition, 20 | velocity: RecordableVector = RecordableVector(0.0, 0.0, 0.0) 21 | ) 22 | 23 | fun refreshPosition( 24 | minestomEntity: E, 25 | position: RecordablePosition 26 | ) 27 | 28 | fun getNativeEntity(entity: RecordableEntity): E? 29 | fun removeEntity(entity: RecordableEntity) 30 | fun removeNativeEntity(entity: E) 31 | fun removeAllEntities() 32 | fun removeEntityViewer(player: P) 33 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/replayer/ReplaySession.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer 2 | 3 | import io.github.openminigameserver.replay.AbstractReplaySession 4 | import io.github.openminigameserver.replay.TickTime 5 | import io.github.openminigameserver.replay.TimeUnit 6 | import io.github.openminigameserver.replay.abstraction.ReplayEntity 7 | import io.github.openminigameserver.replay.abstraction.ReplayUser 8 | import io.github.openminigameserver.replay.abstraction.ReplayWorld 9 | import io.github.openminigameserver.replay.model.Replay 10 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 11 | import io.github.openminigameserver.replay.platform.ReplayPlatform 12 | import io.github.openminigameserver.replay.replayer.statehelper.ReplaySessionPlayerStateHelper 13 | import kotlinx.datetime.Clock 14 | import kotlinx.datetime.Instant 15 | import net.kyori.adventure.text.Component 16 | import net.kyori.adventure.text.format.NamedTextColor 17 | import java.util.* 18 | import java.util.concurrent.CountDownLatch 19 | import kotlin.time.Duration 20 | import kotlin.time.seconds 21 | 22 | class ReplaySession constructor( 23 | internal val replayPlatform: ReplayPlatform, 24 | val world: ReplayWorld, 25 | override val replay: Replay, 26 | val viewers: MutableList, 27 | private val tickTime: TickTime = TickTime(1L, TimeUnit.TICK) 28 | ) : AbstractReplaySession() { 29 | 30 | val viewerCountDownLatch = CountDownLatch(if (replay.hasChunks) viewers.size else 0) 31 | 32 | var currentStepDuration = 10.seconds 33 | set(value) { 34 | field = value 35 | updateReplayStateToViewers() 36 | } 37 | 38 | override val hasEnded: Boolean 39 | get() = time == replay.duration 40 | 41 | val playerStateHelper = ReplaySessionPlayerStateHelper(this) 42 | private val playerTimeStepHelper = ReplaySessionTimeStepHelper(this) 43 | private val ticker: Runnable = ReplayTicker(this) 44 | 45 | init { 46 | resetActions() 47 | } 48 | 49 | private fun resetActions(targetDuration: Duration = Duration.ZERO) { 50 | actions.clear() 51 | actions.addAll(replay.actions.filter { it.timestamp >= targetDuration }.sortedByDescending { it.timestamp }) 52 | } 53 | 54 | var speed: Double = 1.0 55 | set(value) { 56 | field = value 57 | updateReplayStateToViewers() 58 | } 59 | 60 | var paused = true 61 | set(value) { 62 | field = value 63 | updateReplayStateToViewers() 64 | } 65 | 66 | var hasSpawnedEntities = false 67 | 68 | /** 69 | * Last timestamp in milliseconds the [tick] method was called. 70 | */ 71 | private var lastTickTime: Instant = Clock.System.now() 72 | 73 | /** 74 | * Current replay time. 75 | * Valid regardless of [paused]. 76 | */ 77 | override var time: Duration = Duration.ZERO 78 | 79 | private fun updateReplayStateToViewers() { 80 | playerStateHelper.updateReplayStateToViewers() 81 | } 82 | 83 | override fun init() { 84 | isInitialized = true 85 | viewers.forEach { p -> 86 | setupViewer(p) 87 | } 88 | Thread { 89 | viewerCountDownLatch.await() 90 | while (isInitialized) { 91 | ticker.run() 92 | Thread.sleep(tickTime.unit.toMilliseconds(tickTime.time)) 93 | } 94 | }.start() 95 | } 96 | 97 | private val oldViewerInstanceMap = mutableMapOf() 98 | private fun setupViewer(p: ReplayUser) { 99 | replayPlatform.addToViewerTeam(p) 100 | if (p.instance != world) { 101 | p.instance?.uuid?.let { oldViewerInstanceMap[p.uuid] = it } 102 | p.setWorld(world) 103 | } 104 | } 105 | 106 | override fun unInit() { 107 | isInitialized = false 108 | entityManager.removeAllEntities() 109 | world.replaySession = null 110 | playerStateHelper.unInit() 111 | viewers.forEach { removeViewer(it) } 112 | if (replay.hasChunks) { 113 | replayPlatform.unregisterWorld(world) 114 | } 115 | } 116 | 117 | fun removeViewer(player: ReplayUser) { 118 | try { 119 | entityManager.removeEntityViewer(player) 120 | playerStateHelper.removeViewer(player) 121 | 122 | replayPlatform.removeFromViewerTeam(player) 123 | 124 | player.sendActionBar(Component.empty()) 125 | 126 | val oldInstance = 127 | oldViewerInstanceMap[player.uuid]?.let { replayPlatform.getWorldById(it) } 128 | oldInstance?.let { player.setWorld(oldInstance) } 129 | } catch (e: Throwable) { 130 | e.printStackTrace() 131 | } finally { 132 | viewers.remove(player) 133 | if (viewers.isEmpty()) { 134 | unInit() 135 | } 136 | } 137 | } 138 | 139 | /** 140 | * The next action to be played. 141 | */ 142 | private var nextAction: RecordableAction? = null 143 | 144 | /** 145 | * Update the current time and play actions accordingly. 146 | */ 147 | internal var lastReplayTime = Duration.ZERO /* Used to detect if we're going backwards */ 148 | 149 | override fun tick(forceTick: Boolean, isTimeStep: Boolean) { 150 | if (!hasSpawnedEntities) { 151 | 152 | replay.entities.values.filter { it.spawnOnStart }.forEach { 153 | entityManager.spawnEntity(it, it.spawnPosition!!.position, it.spawnPosition!!.velocity) 154 | } 155 | 156 | playerStateHelper.init() 157 | 158 | hasSpawnedEntities = true 159 | } 160 | 161 | val currentTime = Clock.System.now() 162 | if (!forceTick && paused) { 163 | lastTickTime = currentTime 164 | return 165 | } 166 | 167 | val timePassed = currentTime - lastTickTime 168 | val targetReplayTime = (this.time + (timePassed * speed)) 169 | 170 | if (isTimeStep) { 171 | playerTimeStepHelper.performTimeStep(lastReplayTime, targetReplayTime) 172 | resetActions(targetReplayTime) 173 | nextAction = null 174 | lastTickTime = currentTime 175 | return 176 | } 177 | 178 | fun readNextAction() { 179 | nextAction = actions.takeIf { !it.empty() }?.pop() 180 | } 181 | 182 | while (true) { 183 | if (nextAction == null) { 184 | readNextAction() 185 | if (nextAction == null) { 186 | // If still null, then we reached end of replay 187 | time = replay.duration 188 | paused = true 189 | 190 | return 191 | } 192 | } else { 193 | if (nextAction!!.timestamp < targetReplayTime) { 194 | playAction(nextAction!!) 195 | this.time = nextAction?.timestamp ?: Duration.ZERO 196 | nextAction = null 197 | } else { 198 | 199 | this.time += timePassed * speed 200 | break 201 | } 202 | } 203 | } 204 | lastTickTime = currentTime 205 | } 206 | 207 | val entityManager = replayPlatform.getEntityManager(this) 208 | override fun playAction(action: RecordableAction) { 209 | try { 210 | replayPlatform.actionPlayerManager.getActionPlayer(action).play(action, this, world, viewers) 211 | } catch (e: Throwable) { 212 | e.printStackTrace() 213 | 214 | paused = true 215 | viewers.forEach { 216 | it.sendMessage( 217 | Component.text( 218 | "An error occurred while playing your replay. Please contact an administrator for support.", 219 | NamedTextColor.RED 220 | ) 221 | ) 222 | } 223 | 224 | //Unload everything 225 | unInit() 226 | } 227 | } 228 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/replayer/ReplaySessionTimeStepHelper.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer 2 | 3 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 4 | import io.github.openminigameserver.replay.model.recordable.reverse.Reversible 5 | import kotlin.time.Duration 6 | 7 | class ReplaySessionTimeStepHelper(private val session: ReplaySession) { 8 | private val replay get() = session.replay 9 | private val entityManager get() = session.entityManager 10 | 11 | fun performTimeStep(currentTime: Duration, targetReplayTime: Duration) { 12 | val isForwardStep = targetReplayTime > currentTime 13 | val (start, end) = currentTime to targetReplayTime 14 | 15 | 16 | val reversibleActions: List = 17 | session.findManyActions(start, end) { it is Reversible }.groupBy { it.javaClass } 18 | .flatMap { it.value }.map { it as Reversible } 19 | .flatMap { entry -> 20 | if (!isForwardStep) entry.provideRevertedActions(start, end, session) else listOf( 21 | entry as RecordableAction 22 | ) 23 | } 24 | 25 | val actionsToPlay = mutableListOf() 26 | actionsToPlay.addAll(reversibleActions.toMutableList()) 27 | 28 | if (isForwardStep) actionsToPlay.sortBy { it.timestamp } else actionsToPlay.sortByDescending { it.timestamp } 29 | 30 | actionsToPlay.filter { it is Reversible && it.isAppliedInBatch }.groupBy { it.javaClass } 31 | .forEach { groupedEntry -> 32 | val first = groupedEntry.value.firstOrNull() as? Reversible ?: return@forEach 33 | actionsToPlay.removeAll(groupedEntry.value) 34 | first.batchActions(groupedEntry.value.let { it.sortedBy { it.timestamp } }) 35 | ?.let { it1 -> actionsToPlay.add(it1) } 36 | } 37 | 38 | //Reset entities first, then play actions 39 | entityManager.entities.forEach { 40 | entityManager.resetEntity(it, start, end) 41 | } 42 | 43 | actionsToPlay.forEach { 44 | session.playAction(it) 45 | } 46 | 47 | } 48 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/replayer/ReplayTicker.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer 2 | 3 | class ReplayTicker(private val session: ReplaySession) : Runnable { 4 | override fun run() { 5 | session.tick() 6 | } 7 | 8 | } 9 | -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/replayer/statehelper/ControlItemAction.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.statehelper 2 | 3 | enum class ControlItemAction { 4 | NONE, 5 | 6 | COOL_DOWN, 7 | 8 | STEP_BACKWARDS, 9 | PAUSE, 10 | 11 | RESUME, 12 | STEP_FORWARD, 13 | 14 | SPEED_UP, 15 | 16 | PLAY_AGAIN 17 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/replayer/statehelper/ReplaySessionPlayerStateHelper.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.statehelper 2 | 3 | import io.github.openminigameserver.replay.TickTime 4 | import io.github.openminigameserver.replay.TimeUnit 5 | import io.github.openminigameserver.replay.abstraction.ReplayActionItemStack 6 | import io.github.openminigameserver.replay.abstraction.ReplayGameMode 7 | import io.github.openminigameserver.replay.abstraction.ReplayUser 8 | import io.github.openminigameserver.replay.model.recordable.entity.data.PlayerEntityData 9 | import io.github.openminigameserver.replay.replayer.ReplaySession 10 | import io.github.openminigameserver.replay.replayer.statehelper.constants.* 11 | import io.github.openminigameserver.replay.replayer.statehelper.utils.ReplayStatePlayerData 12 | import net.kyori.adventure.key.Key 13 | import net.kyori.adventure.sound.Sound 14 | import net.kyori.adventure.text.Component.empty 15 | import net.kyori.adventure.text.Component.text 16 | import net.kyori.adventure.text.format.NamedTextColor 17 | import java.util.* 18 | import kotlin.math.roundToInt 19 | import kotlin.time.Duration 20 | import kotlin.time.seconds 21 | 22 | class ReplaySessionPlayerStateHelper(val session: ReplaySession) { 23 | val replayPlatform = session.replayPlatform 24 | 25 | val viewers: List 26 | get() = session.viewers 27 | 28 | val host: ReplayUser? 29 | get() = viewers.firstOrNull() 30 | 31 | private var isInitialized = true 32 | 33 | private var tickerTask: Any? = null 34 | private fun getActionBarMessage() = if (!isInitialized) empty() else text { 35 | val spacing = " ".repeat(6) 36 | val (minutes, seconds) = formatTime(session.time) 37 | val (minutesFinal, secondsFinal) = formatTime(session.replay.duration) 38 | 39 | with(it) { 40 | 41 | 42 | append((if (session.paused) text("Paused", NamedTextColor.RED) else text("Playing", NamedTextColor.GREEN))) 43 | append(text(spacing)) 44 | 45 | append(text("$minutes:$seconds", NamedTextColor.YELLOW)) 46 | append(text(" / ")) 47 | append(text("$minutesFinal:$secondsFinal", NamedTextColor.YELLOW)) 48 | 49 | append(text(spacing)) 50 | append(text("x", NamedTextColor.GOLD)) 51 | append( 52 | text( 53 | (if (session.speed >= 0.5) "%.1f" else "%.2f").format( 54 | session.speed, 55 | Locale.ENGLISH 56 | ), NamedTextColor.GOLD 57 | ) 58 | ) 59 | append(text(" ".repeat(2))) 60 | } 61 | } 62 | 63 | private fun formatTime(time: Duration): Pair { 64 | val currentTime = time.inSeconds 65 | val minutes = formatResultToTime(currentTime / 60) 66 | val seconds = formatResultToTime(currentTime % 60) 67 | return Pair(minutes, seconds) 68 | } 69 | 70 | private fun formatResultToTime(currentTime: Double) = (currentTime).toInt().toString().padStart(2, '0') 71 | 72 | private val tickerTaskRunnable = Runnable { 73 | updateViewersActionBar(session.viewers.toMutableList()) 74 | } 75 | 76 | private fun updateAllItems() { 77 | host?.let { updateItems(it) } 78 | } 79 | 80 | private fun updateViewersActionBar(viewers: MutableList) { 81 | viewers.forEach { 82 | it.sendActionBar(getActionBarMessage()) 83 | } 84 | } 85 | 86 | fun init() { 87 | initializePlayerControlItems() 88 | teleportViewers() 89 | playLoadedSoundToViewers() 90 | tickerTask = 91 | replayPlatform.registerSyncRepeatingTask(TickTime(250, TimeUnit.MILLISECOND)) { tickerTaskRunnable.run() } 92 | } 93 | 94 | private fun playLoadedSoundToViewers() { 95 | viewers.forEach { 96 | try { 97 | it.playSound(Sound.sound(Key.key("entity.player.levelup"), Sound.Source.PLAYER, 1f, 1f)) 98 | } catch (e: Throwable) { 99 | // e.printStackTrace() 100 | } 101 | } 102 | } 103 | 104 | private val oldData = mutableMapOf() 105 | fun removeViewer(player: ReplayUser) { 106 | oldData[player.uuid]?.apply(replayPlatform, player) 107 | } 108 | 109 | private fun initializePlayerControlItems() { 110 | session.viewers.forEach { p: ReplayUser -> 111 | oldData[p.uuid] = ReplayStatePlayerData(replayPlatform, p) 112 | p.clearInventory() 113 | p.gameMode = ReplayGameMode.ADVENTURE 114 | p.isAllowFlying = true 115 | p.isFlying = true 116 | } 117 | 118 | host?.let { 119 | it.setHeldItemSlot(SLOT_PLAY_PAUSE.toByte()) 120 | updateItems(it) 121 | } 122 | 123 | } 124 | 125 | private fun updateItems(it: ReplayUser) { 126 | it.setItemStack( 127 | SLOT_DECREASE_SPEED, 128 | getItemStackOrAirIfReplayEnded(PlayerHeadsItems.getDecreaseSpeedItem()) 129 | ) 130 | it.setItemStack( 131 | SLOT_STEP_BACKWARDS, 132 | getItemStackOrAirIfReplayEnded(PlayerHeadsItems.getStepBackwardsItem(session.currentStepDuration)) 133 | ) 134 | 135 | it.setItemStack(SLOT_PLAY_PAUSE, PlayerHeadsItems.getPlayPauseItem(session.paused, session.hasEnded)) 136 | 137 | it.setItemStack( 138 | SLOT_STEP_FORWARD, 139 | getItemStackOrAirIfReplayEnded(PlayerHeadsItems.getStepForwardItem(session.currentStepDuration)) 140 | ) 141 | it.setItemStack( 142 | SLOT_INCREASE_SPEED, 143 | getItemStackOrAirIfReplayEnded(PlayerHeadsItems.getIncreaseSpeedItem()) 144 | ) 145 | } 146 | 147 | private fun getItemStackOrAirIfReplayEnded(itemStack: ReplayActionItemStack) = 148 | if (session.hasEnded) ReplayActionItemStack.air else itemStack 149 | 150 | val skipSpeeds = arrayOf(1, 5, 10, 30, 60) 151 | private val speeds = arrayOf(0.25, 0.5, 1.0, 2.0, 4.0) 152 | fun handleItemAction(player: ReplayUser, action: ControlItemAction) { 153 | when (action) { 154 | ControlItemAction.COOL_DOWN -> { 155 | val previousSpeed = speeds[(speeds.indexOf(session.speed) - 1).coerceAtLeast(0)] 156 | session.speed = previousSpeed 157 | session.tick(true) 158 | } 159 | ControlItemAction.PAUSE -> { 160 | session.paused = true 161 | } 162 | ControlItemAction.RESUME -> { 163 | session.paused = false 164 | } 165 | ControlItemAction.PLAY_AGAIN -> { 166 | session.lastReplayTime = session.replay.duration 167 | session.time = Duration.ZERO 168 | 169 | session.tick(forceTick = action == ControlItemAction.PLAY_AGAIN, isTimeStep = true) 170 | session.paused = false 171 | } 172 | ControlItemAction.SPEED_UP -> { 173 | val nextSpeed = speeds[(speeds.indexOf(session.speed) + 1).coerceAtMost(speeds.size - 1)] 174 | session.speed = nextSpeed 175 | session.tick(true) 176 | } 177 | ControlItemAction.STEP_BACKWARDS -> { 178 | doStep(false) 179 | } 180 | ControlItemAction.STEP_FORWARD -> { 181 | doStep(true) 182 | 183 | } 184 | else -> TODO(action.name) 185 | } 186 | try { 187 | player.playSound(Sound.sound(Key.key("block.lever.click"), Sound.Source.BLOCK, 1f, 1f)) 188 | } catch (e: Throwable) { 189 | // e.printStackTrace() 190 | } 191 | updateReplayStateToViewers() 192 | } 193 | 194 | private fun doStep(isForward: Boolean) { 195 | val oldPausedState = session.paused 196 | val duration = session.currentStepDuration * if (isForward) 1 else -1 197 | session.paused = true 198 | 199 | session.lastReplayTime = session.time 200 | session.time = 201 | (session.time + duration).coerceIn(Duration.ZERO, session.replay.duration) 202 | session.tick(forceTick = true, isTimeStep = true) 203 | 204 | session.paused = oldPausedState 205 | updateReplayStateToViewers() 206 | } 207 | 208 | 209 | private fun teleportViewers() { 210 | val entities = session.replay.entities.values 211 | val targetEntity = 212 | entities.firstOrNull { (it.entityData as? PlayerEntityData)?.userName == session.viewers.first().name } 213 | ?: entities.firstOrNull() 214 | val targetEntityMinestom = targetEntity?.let { session.entityManager.getNativeEntity(it) } 215 | 216 | if (targetEntityMinestom != null) { 217 | session.viewers.forEach { it.teleport(targetEntityMinestom.position) } 218 | } 219 | } 220 | 221 | fun unInit() { 222 | tickerTask?.let { replayPlatform.cancelTask(it) } 223 | tickerTask = null 224 | isInitialized = false 225 | updateViewersActionBar(session.viewers) 226 | } 227 | 228 | fun updateReplayStateToViewers() { 229 | updateViewersActionBar(session.viewers) 230 | updateAllItems() 231 | } 232 | 233 | fun handleItemSwing(player: ReplayUser, itemStack: ReplayActionItemStack) { 234 | val action = itemStack.action.takeUnless { it == ControlItemAction.NONE } 235 | if (action == ControlItemAction.STEP_BACKWARDS || action == ControlItemAction.STEP_FORWARD) { 236 | val duration = session.currentStepDuration.inSeconds.roundToInt() 237 | val currentSkipIndex = skipSpeeds.indexOf(duration).coerceAtLeast(0) 238 | var nextIndex = currentSkipIndex + 1 239 | if (nextIndex >= skipSpeeds.size) { 240 | nextIndex = 0 241 | } 242 | 243 | session.currentStepDuration = skipSpeeds[nextIndex].seconds 244 | 245 | } 246 | } 247 | 248 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/replayer/statehelper/constants/PlayerHeadsItems.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.statehelper.constants 2 | 3 | import io.github.openminigameserver.replay.abstraction.ReplayActionItemStack 4 | import io.github.openminigameserver.replay.abstraction.ReplayHeadTextureSkin 5 | import io.github.openminigameserver.replay.replayer.statehelper.ControlItemAction 6 | import net.kyori.adventure.text.Component.text 7 | import net.kyori.adventure.text.format.NamedTextColor 8 | import kotlin.time.Duration 9 | 10 | const val actionData = "controlItem" 11 | 12 | object PlayerHeadsItems { 13 | fun getPlayPauseItem(paused: Boolean, finished: Boolean): ReplayActionItemStack { 14 | if (finished) { 15 | return ReplayActionItemStack( 16 | text("Play Recording Again", NamedTextColor.GREEN), 17 | ControlItemAction.PLAY_AGAIN 18 | ) 19 | } 20 | return ReplayActionItemStack( 21 | text("Click to ${if (paused) "Resume" else "Pause"}", NamedTextColor.GREEN), 22 | if (paused) ControlItemAction.RESUME else ControlItemAction.PAUSE 23 | ) 24 | } 25 | 26 | fun getDecreaseSpeedItem() = 27 | buildItemStack(PlayerHeadsTextureData.decreaseSpeed, "Decrease Speed", ControlItemAction.COOL_DOWN) 28 | 29 | fun getIncreaseSpeedItem() = 30 | buildItemStack(PlayerHeadsTextureData.increaseSpeed, "Increase Speed", ControlItemAction.SPEED_UP) 31 | 32 | private fun buildItemStack(skin: ReplayHeadTextureSkin, name: String, action: ControlItemAction) = 33 | ReplayActionItemStack(text(name, NamedTextColor.GREEN), action, skin) 34 | 35 | fun getStepBackwardsItem(skipSpeed: Duration): ReplayActionItemStack = buildItemStack( 36 | PlayerHeadsTextureData.backwards, 37 | "${skipSpeed.inSeconds.toInt()}s Backwards", 38 | ControlItemAction.STEP_BACKWARDS 39 | ) 40 | 41 | fun getStepForwardItem(skipSpeed: Duration): ReplayActionItemStack = buildItemStack( 42 | PlayerHeadsTextureData.forwards, 43 | "${skipSpeed.inSeconds.toInt()}s Forward", 44 | ControlItemAction.STEP_FORWARD 45 | ) 46 | 47 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/replayer/statehelper/constants/PlayerHeadsTextureData.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.statehelper.constants 2 | 3 | import io.github.openminigameserver.replay.abstraction.ReplayHeadTextureSkin 4 | 5 | object PlayerHeadsTextureData { 6 | val decreaseSpeed = ReplayHeadTextureSkin( 7 | "eyJ0aW1lc3RhbXAiOjE1ODMyMjI0MDE2NDEsInByb2ZpbGVJZCI6Ijc1MTQ0NDgxOTFlNjQ1NDY4Yzk3MzlhNmUzOTU3YmViIiwicHJvZmlsZU5hbWUiOiJUaGFua3NNb2phbmciLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2RjZDdjMTRiOTJjYjM3OTA5MjA4YTBkMjA0NzgwNDkzZjljOWNjNWY1NmQxMDE5YjczNjM0MTc5MDlmMWQ5NTYifX19", 8 | "" 9 | ) 10 | 11 | val backwards = ReplayHeadTextureSkin( 12 | "eyJ0aW1lc3RhbXAiOjE1NTc5MjM4NDYyOTMsInByb2ZpbGVJZCI6IjU3MGIwNWJhMjZmMzRhOGViZmRiODBlY2JjZDdlNjIwIiwicHJvZmlsZU5hbWUiOiJMb3JkU29ubnkiLCJzaWduYXR1cmVSZXF1aXJlZCI6dHJ1ZSwidGV4dHVyZXMiOnsiU0tJTiI6eyJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlL2E2ZTFjZDAwNjc4NTViNjdlMGZkNWI3ZWI3NDU3MjgxYzQxZDEzYmQ1YmM5MTU4YzRhODJmNTE4MTk4YTFkMjIifX19", 13 | "" 14 | ) 15 | 16 | val forwards = ReplayHeadTextureSkin( 17 | "eyJ0aW1lc3RhbXAiOjE1NTc5MjM2NzA1MTMsInByb2ZpbGVJZCI6ImIwZDczMmZlMDBmNzQwN2U5ZTdmNzQ2MzAxY2Q5OGNhIiwicHJvZmlsZU5hbWUiOiJPUHBscyIsInNpZ25hdHVyZVJlcXVpcmVkIjp0cnVlLCJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZGIyZjMwNTAyYThmZTRjODBlODgzZDIzYjQ3Mzg5YjAzYTc4MThkOWJiYWQyYmE0ZGMxMGQ2NTNkM2ViNTJiMiJ9fX0=", 18 | "" 19 | ) 20 | 21 | val increaseSpeed = ReplayHeadTextureSkin( 22 | "eyJ0aW1lc3RhbXAiOjE1ODMyMjIxMzI2NDUsInByb2ZpbGVJZCI6ImRkZWQ1NmUxZWY4YjQwZmU4YWQxNjI5MjBmN2FlY2RhIiwicHJvZmlsZU5hbWUiOiJEaXNjb3JkQXBwIiwic2lnbmF0dXJlUmVxdWlyZWQiOnRydWUsInRleHR1cmVzIjp7IlNLSU4iOnsidXJsIjoiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS9jZjM4MjFhYWIwYTVhYmZlN2Y0OTM3YWMyOGVjOGUzMWEzMzYwY2I1MTVjMTEwNDZmZjc1MGFlMmEwYTM5MWFmIn19fQ==", 23 | "" 24 | ) 25 | } -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/replayer/statehelper/constants/StateHelperConstants.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.statehelper.constants 2 | 3 | const val SLOT_DECREASE_SPEED = 2 4 | const val SLOT_STEP_BACKWARDS = 3 5 | const val SLOT_PLAY_PAUSE = 4 6 | const val SLOT_STEP_FORWARD = 5 7 | const val SLOT_INCREASE_SPEED = 6 -------------------------------------------------------------------------------- /impl-abstraction/src/main/kotlin/io/github/openminigameserver/replay/replayer/statehelper/utils/ReplayStatePlayerData.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.statehelper.utils 2 | 3 | import io.github.openminigameserver.replay.abstraction.ReplayGameMode 4 | import io.github.openminigameserver.replay.abstraction.ReplayUser 5 | import io.github.openminigameserver.replay.platform.ReplayPlatform 6 | 7 | 8 | class ReplayStatePlayerData( 9 | private val isAllowFlying: Boolean, 10 | private val isFlying: Boolean, 11 | private val gameMode: ReplayGameMode, 12 | private val heldSlot: Byte, 13 | private val exp: Float, 14 | private val inventory: Any 15 | ) { 16 | 17 | constructor(replayPlatform: ReplayPlatform<*, ReplayUser, *>, player: ReplayUser) : this( 18 | player.isAllowFlying, 19 | player.isFlying, 20 | player.gameMode, 21 | player.heldSlot, 22 | player.exp, 23 | replayPlatform.getPlayerInventoryCopy(player) 24 | ) 25 | 26 | fun apply(replayPlatform: ReplayPlatform<*, ReplayUser, *>, player: ReplayUser) { 27 | player.isAllowFlying = isAllowFlying 28 | player.isFlying = isFlying 29 | player.gameMode = gameMode 30 | player.setHeldItemSlot(heldSlot) 31 | player.exp = exp 32 | replayPlatform.loadPlayerInventoryCopy(player, inventory) 33 | } 34 | 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /impl-abstraction/src/template/kotlin/io/github/openminigameserver/replay/BuildInfo.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay; 2 | 3 | object BuildInfo { 4 | const val version: String = "${version}" 5 | } -------------------------------------------------------------------------------- /impl-minestom/build.gradle.kts: -------------------------------------------------------------------------------- 1 | val minestomVersion = "f7ec45802f" 2 | val cloudVersion = "58e8fd76f3" 3 | 4 | dependencies { 5 | api(project(":impl-abstraction")) 6 | implementation("com.github.OpenMinigameServer:cloud-minestom:$cloudVersion") 7 | implementation("cloud.commandframework:cloud-annotations:1.4.0") 8 | implementation("com.github.mworzala:adventure-platform-minestom:d208f53200") 9 | compileOnly(minestom(minestomVersion)) 10 | testImplementation(minestom(minestomVersion)) 11 | } 12 | 13 | fun minestom(commit: String): String { 14 | return "com.github.Minestom:Minestom:$commit" 15 | } 16 | 17 | tasks { 18 | val templateContext = mapOf("version" to project.version.toString()) 19 | processResources { 20 | expand(*templateContext.toList().toTypedArray()) 21 | } 22 | 23 | create("generateKotlinBuildInfo") { 24 | inputs.properties(templateContext) // for gradle up-to-date check 25 | from("src/template/kotlin/") 26 | into("$buildDir/generated/kotlin/") 27 | expand(*templateContext.toList().toTypedArray()) 28 | } 29 | 30 | kotlin.sourceSets["main"].kotlin.srcDir("$buildDir/generated/kotlin") 31 | compileKotlin.get().dependsOn(get("generateKotlinBuildInfo")) 32 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/java/io/github/openminigameserver/replay/mixins/InstanceMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.mixins; 2 | 3 | import io.github.openminigameserver.replay.extensions.MinestomInteropExtensionsKt; 4 | import net.minestom.server.data.Data; 5 | import net.minestom.server.instance.Chunk; 6 | import net.minestom.server.instance.Instance; 7 | import net.minestom.server.instance.InstanceContainer; 8 | import net.minestom.server.instance.block.CustomBlock; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.injection.At; 11 | import org.spongepowered.asm.mixin.injection.Inject; 12 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 13 | 14 | @Mixin(InstanceContainer.class) 15 | public abstract class InstanceMixin { 16 | 17 | @Inject(method = "UNSAFE_setBlock", at = @At(value = "INVOKE", target = "Lnet/minestom/server/instance/Chunk;" + 18 | "UNSAFE_setBlock(IIISSLnet/minestom/server/data/Data;Z)V")) 19 | public void onSetBlock(Chunk chunk, int x, int y, int z, short blockStateId, CustomBlock customBlock, Data data, 20 | CallbackInfo ci) { 21 | //noinspection ConstantConditions 22 | if (!(((Object) this) instanceof Instance)) return; 23 | var instance = (Instance) (Object) this; 24 | var recorder = MinestomInteropExtensionsKt.getRecorder(instance); 25 | if (recorder == null) return; 26 | 27 | recorder.notifyBlockChange(x, y, z, blockStateId); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /impl-minestom/src/main/java/io/github/openminigameserver/replay/mixins/PacketUtilsMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.mixins; 2 | 3 | import io.github.openminigameserver.replay.ReplayListener; 4 | import net.minestom.server.entity.Player; 5 | import net.minestom.server.network.packet.server.ServerPacket; 6 | import net.minestom.server.utils.PacketUtils; 7 | import net.minestom.server.utils.callback.validator.PlayerValidator; 8 | import org.spongepowered.asm.mixin.Mixin; 9 | import org.spongepowered.asm.mixin.injection.At; 10 | import org.spongepowered.asm.mixin.injection.Inject; 11 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 12 | 13 | import java.util.Collection; 14 | 15 | @Mixin(PacketUtils.class) 16 | public class PacketUtilsMixin { 17 | @Inject(method = "sendGroupedPacket(Ljava/util/Collection;" + 18 | "Lnet/minestom/server/network/packet/server/ServerPacket;" + 19 | "Lnet/minestom/server/utils/callback/validator/PlayerValidator;)V", at = @At("HEAD")) 20 | private static void onSendGroupedPacket(Collection players, ServerPacket packet, 21 | PlayerValidator playerValidator, CallbackInfo ci) { 22 | ReplayListener.handleSentPacket(packet, players); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /impl-minestom/src/main/java/io/github/openminigameserver/replay/mixins/PlayerInventoryMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.mixins; 2 | 3 | import io.github.openminigameserver.replay.MinestomReplayExtension; 4 | import io.github.openminigameserver.replay.extensions.MinestomInteropExtensionsKt; 5 | import io.github.openminigameserver.replay.platform.minestom.MinestomReplayPlatform; 6 | import net.minestom.server.entity.Player; 7 | import net.minestom.server.inventory.PlayerInventory; 8 | import org.spongepowered.asm.mixin.Final; 9 | import org.spongepowered.asm.mixin.Mixin; 10 | import org.spongepowered.asm.mixin.Shadow; 11 | import org.spongepowered.asm.mixin.injection.At; 12 | import org.spongepowered.asm.mixin.injection.Inject; 13 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 14 | 15 | @Mixin(PlayerInventory.class) 16 | public class PlayerInventoryMixin { 17 | @Shadow 18 | @Final 19 | protected Player player; 20 | 21 | @Inject(method = "update", at = @At("TAIL")) 22 | public void onInventoryUpdate(CallbackInfo ci) { 23 | if (player.getInstance() != null) { 24 | var recorder = MinestomInteropExtensionsKt.getRecorder(player.getInstance()); 25 | if (recorder != null) { 26 | recorder.onEntityEquipmentChange(((MinestomReplayPlatform) MinestomReplayExtension.extension.getPlatform()).getPlayer((Player) (Object) this)); 27 | } 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /impl-minestom/src/main/java/io/github/openminigameserver/replay/mixins/PlayerMixin.java: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.mixins; 2 | 3 | import io.github.openminigameserver.replay.MinestomReplayExtension; 4 | import io.github.openminigameserver.replay.extensions.MinestomInteropExtensionsKt; 5 | import io.github.openminigameserver.replay.platform.minestom.MinestomReplayPlatform; 6 | import net.minestom.server.entity.EntityType; 7 | import net.minestom.server.entity.LivingEntity; 8 | import net.minestom.server.entity.Player; 9 | import net.minestom.server.utils.Position; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.spongepowered.asm.mixin.Mixin; 12 | import org.spongepowered.asm.mixin.injection.At; 13 | import org.spongepowered.asm.mixin.injection.Inject; 14 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 15 | 16 | @Mixin(Player.class) 17 | public abstract class PlayerMixin extends LivingEntity { 18 | public PlayerMixin(@NotNull EntityType entityType, @NotNull Position spawnPosition) { 19 | super(entityType, spawnPosition); 20 | } 21 | 22 | @Inject(method = "refreshHeldSlot", at = @At("RETURN")) 23 | public void onChangeHeld(byte slot, CallbackInfo ci) { 24 | if (getInstance() != null) { 25 | var recorder = MinestomInteropExtensionsKt.getRecorder(getInstance()); 26 | if (recorder != null) { 27 | recorder.onEntityEquipmentChange(((MinestomReplayPlatform) MinestomReplayExtension.extension.getPlatform()).getPlayer((Player) (Object) this)); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/MinestomReplayExtension.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay 2 | 3 | import io.github.openminigameserver.replay.helpers.EntityHelper 4 | import io.github.openminigameserver.replay.platform.ReplayExtension 5 | import io.github.openminigameserver.replay.platform.minestom.MinestomReplayPlatform 6 | import net.minestom.server.MinecraftServer 7 | import net.minestom.server.extensions.Extension 8 | import net.minestom.server.extras.selfmodification.MinestomRootClassLoader 9 | import org.slf4j.Logger 10 | import java.io.File 11 | 12 | class MinestomReplayExtension : Extension() { 13 | val minestomLogger: Logger get() = this.logger 14 | 15 | companion object { 16 | @JvmStatic 17 | lateinit var dataFolder: File 18 | 19 | var hasLoadedPlatform = false 20 | 21 | lateinit var platform: MinestomReplayPlatform 22 | 23 | lateinit var extension: ReplayExtension 24 | } 25 | 26 | init { 27 | val classLoader = this.javaClass.classLoader 28 | if (classLoader is MinestomRootClassLoader) { 29 | classLoader.protectedClasses.add("io.github.openminigameserver.replay.AbstractReplaySession") 30 | classLoader.protectedPackages.add("io.github.openminigameserver.replay.model") 31 | } 32 | } 33 | 34 | override fun initialize() { 35 | dataFolder = File( 36 | MinecraftServer.getExtensionManager().extensionFolder, 37 | "Replay" 38 | ).also { it.mkdirs() } 39 | platform = MinestomReplayPlatform(this) 40 | hasLoadedPlatform = true 41 | extension = ReplayExtension(platform) 42 | 43 | extension.init() 44 | 45 | ReplayListener.platform = platform 46 | ReplayListener.registerListeners() 47 | logger.info("Initialized event listeners.") 48 | 49 | EntityHelper.init() 50 | logger.info("Initialized entity helpers.") 51 | } 52 | 53 | override fun terminate() { 54 | 55 | } 56 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/ReplayListener.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay 2 | 3 | import io.github.openminigameserver.replay.extensions.* 4 | import io.github.openminigameserver.replay.helpers.ReplayPlayerEntity 5 | import io.github.openminigameserver.replay.model.Replay 6 | import io.github.openminigameserver.replay.model.recordable.impl.* 7 | import io.github.openminigameserver.replay.platform.minestom.MinestomReplayPlatform 8 | import io.github.openminigameserver.replay.platform.minestom.controlItemAction 9 | import io.github.openminigameserver.replay.recorder.ReplayRecorder 10 | import io.github.openminigameserver.replay.replayer.statehelper.ControlItemAction 11 | import net.minestom.server.MinecraftServer 12 | import net.minestom.server.entity.Entity 13 | import net.minestom.server.entity.Player 14 | import net.minestom.server.entity.fakeplayer.FakePlayer 15 | import net.minestom.server.event.instance.AddEntityToInstanceEvent 16 | import net.minestom.server.event.inventory.InventoryPreClickEvent 17 | import net.minestom.server.event.player.PlayerDisconnectEvent 18 | import net.minestom.server.event.player.PlayerHandAnimationEvent 19 | import net.minestom.server.event.player.PlayerSwapItemEvent 20 | import net.minestom.server.event.player.PlayerUseItemEvent 21 | import net.minestom.server.network.packet.server.ServerPacket 22 | import net.minestom.server.network.packet.server.play.* 23 | 24 | object ReplayListener { 25 | 26 | lateinit var platform: MinestomReplayPlatform 27 | private val playerDisconnectHandler: (event: PlayerDisconnectEvent) -> Unit = event@{ 28 | if (it.player !is FakePlayer && it.player !is ReplayPlayerEntity) { 29 | val replaySession = it.player.instance!!.replaySession ?: return@event 30 | replaySession.removeViewer(platform.getPlayer(it.player)) 31 | } 32 | } 33 | private val viewerJoinedReplaySession: (event: AddEntityToInstanceEvent) -> Unit = event@{ 34 | val player = it.entity as? Player ?: return@event 35 | val instance = it.instance 36 | val session = instance.replaySession ?: return@event 37 | 38 | val isViewer = session.viewers.any { it.uuid == player.uuid } 39 | if (!isViewer) return@event 40 | 41 | session.viewerCountDownLatch.countDown() 42 | } 43 | 44 | private val playerSwapItemEvent: (event: PlayerSwapItemEvent) -> Unit = { 45 | if (it.player.instance!!.replaySession != null) { 46 | it.isCancelled = true 47 | } 48 | } 49 | 50 | private val useItemHandler: (event: PlayerUseItemEvent) -> Unit = eventCallback@{ 51 | val replaySession = it.player.instance!!.replaySession ?: return@eventCallback 52 | 53 | val action = it.itemStack.controlItemAction 54 | if (action != ControlItemAction.NONE) { 55 | runOnSeparateThread { 56 | replaySession.playerStateHelper.handleItemAction(platform.getPlayer(it.player), action) 57 | } 58 | } 59 | } 60 | 61 | private val inventoryPreClickHandler: (event: InventoryPreClickEvent) -> Unit = eventCallback@{ 62 | it.player.instance!!.replaySession ?: return@eventCallback 63 | 64 | if (it.inventory == null) 65 | it.isCancelled = true 66 | } 67 | 68 | private val handAnimationHandler: (event: PlayerHandAnimationEvent) -> Unit = 69 | eventCallback@{ event: PlayerHandAnimationEvent -> 70 | val session = event.player.instance?.replaySession ?: return@eventCallback 71 | 72 | session.playerStateHelper.handleItemSwing( 73 | platform.getPlayer(event.player), 74 | platform.getItemStack(event.player.getItemInHand(event.hand)) 75 | ) 76 | } 77 | 78 | fun registerListeners() { 79 | val eventHandler = MinecraftServer.getGlobalEventHandler() 80 | eventHandler.addEventCallback(PlayerDisconnectEvent::class.java, playerDisconnectHandler) 81 | eventHandler.addEventCallback(AddEntityToInstanceEvent::class.java, viewerJoinedReplaySession) 82 | eventHandler.addEventCallback(PlayerSwapItemEvent::class.java, playerSwapItemEvent) 83 | eventHandler.addEventCallback(PlayerUseItemEvent::class.java, useItemHandler) 84 | eventHandler.addEventCallback(InventoryPreClickEvent::class.java, inventoryPreClickHandler) 85 | 86 | MinecraftServer.getGlobalEventHandler().addEventCallback( 87 | PlayerHandAnimationEvent::class.java, 88 | handAnimationHandler 89 | ) 90 | } 91 | 92 | @JvmStatic 93 | fun handleSentPacket( 94 | packet: ServerPacket, 95 | players: Collection 96 | ) { 97 | if (!MinestomReplayExtension.hasLoadedPlatform) return 98 | when (packet) { 99 | is SoundEffectPacket -> { 100 | handleRecording(players) { replay -> 101 | replay.addAction(packet.run { 102 | RecSoundEffect(soundId, SoundCategory.valueOf(soundCategory.name), x, y, z, volume, pitch) 103 | }) 104 | } 105 | } 106 | is EntityMetaDataPacket -> { 107 | val entity = Entity.getEntity(packet.entityId) as? Player 108 | handleRecording(entity?.let { players.plus(entity) } ?: players) { replay -> 109 | val metadataArray = packet.getMetadataArray() 110 | 111 | replay.getEntityById(packet.entityId)?.let { 112 | replay.addAction(RecEntityMetadata(metadataArray, it)) 113 | } 114 | } 115 | } 116 | is EntityEquipmentPacket -> { 117 | val entity = Entity.getEntity(packet.entityId) ?: return 118 | val instance = entity.instance ?: return 119 | 120 | val recorder: ReplayRecorder = instance.recorder ?: return 121 | 122 | recorder.onEntityEquipmentChange(platform.getEntity(entity)) 123 | } 124 | is EffectPacket -> { 125 | handleEffectPacket(players, packet) 126 | } 127 | is ParticlePacket -> { 128 | handleRecording(players) { replay -> 129 | replay.addAction( 130 | RecParticleEffect( 131 | packet.particleId, 132 | packet.longDistance, 133 | packet.x, 134 | packet.y, 135 | packet.z, 136 | packet.offsetX, 137 | packet.offsetY, 138 | packet.offsetZ, 139 | packet.particleData, 140 | packet.particleCount, 141 | packet.dataConsumer?.getMetadataArray() 142 | ) 143 | ) 144 | } 145 | } 146 | } 147 | } 148 | 149 | private fun handleEffectPacket( 150 | players: Collection, 151 | packet: EffectPacket 152 | ) { 153 | handleRecording(players) { replay -> 154 | replay.addAction( 155 | RecBlockEffect( 156 | packet.effectId, 157 | packet.position.toPosition().toReplay(), 158 | packet.data, 159 | packet.disableRelativeVolume 160 | ) 161 | ) 162 | } 163 | } 164 | 165 | private inline fun handleRecording(players: Collection, code: (Replay) -> Unit) { 166 | players.filter { it.instance != null }.groupBy { it.instance?.uniqueId }.forEach { 167 | val player = it.value.first() 168 | val replay = player.instance?.recorder?.replay ?: return@forEach 169 | 170 | code.invoke(replay) 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/extensions/MinestomInteropExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.extensions 2 | 3 | import com.google.common.cache.Cache 4 | import com.google.common.cache.CacheBuilder 5 | import io.github.openminigameserver.replay.MinestomReplayExtension 6 | import io.github.openminigameserver.replay.abstraction.ReplayWorld 7 | import io.github.openminigameserver.replay.model.Replay 8 | import io.github.openminigameserver.replay.model.recordable.RecordableItemStack 9 | import io.github.openminigameserver.replay.model.recordable.RecordablePosition 10 | import io.github.openminigameserver.replay.model.recordable.RecordablePositionAndVector 11 | import io.github.openminigameserver.replay.model.recordable.RecordableVector 12 | import io.github.openminigameserver.replay.model.recordable.entity.EntityEquipmentSlot 13 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 14 | import io.github.openminigameserver.replay.model.recordable.entity.data.BaseEntityData 15 | import io.github.openminigameserver.replay.model.recordable.entity.data.PlayerEntityData 16 | import io.github.openminigameserver.replay.model.recordable.entity.data.PlayerSkinData 17 | import io.github.openminigameserver.replay.recorder.ReplayRecorder 18 | import io.github.openminigameserver.replay.replayer.ReplaySession 19 | import kotlinx.coroutines.CoroutineScope 20 | import kotlinx.coroutines.runBlocking 21 | import net.minestom.server.entity.Entity 22 | import net.minestom.server.entity.Player 23 | import net.minestom.server.entity.PlayerSkin 24 | import net.minestom.server.instance.Instance 25 | import net.minestom.server.inventory.EquipmentHandler 26 | import net.minestom.server.item.ItemStack 27 | import net.minestom.server.network.packet.server.play.EntityEquipmentPacket 28 | import net.minestom.server.network.packet.server.play.EntityMetaDataPacket 29 | import net.minestom.server.utils.NBTUtils 30 | import net.minestom.server.utils.Position 31 | import net.minestom.server.utils.Vector 32 | import net.minestom.server.utils.binary.BinaryReader 33 | import net.minestom.server.utils.binary.BinaryWriter 34 | import java.util.* 35 | import java.util.concurrent.TimeUnit 36 | import java.util.function.Consumer 37 | 38 | fun Position.toReplay(): RecordablePosition = RecordablePosition(x, y, z, yaw, pitch) 39 | fun RecordablePosition.toMinestom(): Position = Position(x, y, z, yaw, pitch) 40 | 41 | fun Vector.toReplay(): RecordableVector = RecordableVector(x, y, z) 42 | fun RecordableVector.toMinestom(): Vector = Vector(x, y, z) 43 | 44 | fun ItemStack.toReplay(): RecordableItemStack = 45 | RecordableItemStack(BinaryWriter().also { NBTUtils.writeItemStack(it, this) }.toByteArray()) 46 | 47 | fun RecordableItemStack.toMinestom(): ItemStack = 48 | BinaryReader(nbtValue).let { NBTUtils.readItemStack(it) ?: ItemStack.getAirItem() } 49 | 50 | const val REPLAY_RECORDER_DATA = "replay:recorder" 51 | const val REPLAY_REPLAYER_DATA = "replay:replayer" 52 | 53 | var Player.recorder: ReplayRecorder? 54 | get() = instance!!.recorder 55 | set(value) { 56 | instance!!.recorder = value 57 | } 58 | 59 | var Instance.recorder: ReplayRecorder? 60 | get() = replayWorld.recorder 61 | set(value) { 62 | replayWorld.recorder = value 63 | } 64 | 65 | val Instance.replayWorld: ReplayWorld 66 | get() = MinestomReplayExtension.platform.getWorldById(uniqueId) 67 | 68 | var Instance.replaySession: ReplaySession? 69 | get() = replayWorld.replaySession 70 | set(value) { 71 | replayWorld.replaySession = value 72 | } 73 | 74 | internal fun runOnSeparateThread(code: suspend CoroutineScope.() -> Unit) { 75 | Thread { 76 | runBlocking(block = code) 77 | }.start() 78 | } 79 | 80 | internal val profileCache: Cache = 81 | CacheBuilder.newBuilder().expireAfterAccess(1, TimeUnit.MINUTES).build() 82 | 83 | internal fun Entity.toReplay(spawnOnStart: Boolean = true): RecordableEntity { 84 | var data: BaseEntityData? = null 85 | if (this is Player) { 86 | val skin = skin 87 | 88 | data = PlayerEntityData(username, skin?.toReplay(), metadataPacket.getMetadataArray(), getEquipmentForEntity()) 89 | } 90 | return RecordableEntity( 91 | entityId, 92 | entityType.namespaceID, 93 | RecordablePositionAndVector(position.toReplay(), velocity.toReplay()), 94 | data 95 | ).apply { 96 | this.spawnOnStart = 97 | spawnOnStart 98 | } 99 | } 100 | 101 | internal fun PlayerSkin.toReplay(): PlayerSkinData { 102 | val decoder = Base64.getDecoder() 103 | return PlayerSkinData(decoder.decode(textures), decoder.decode(signature)) 104 | } 105 | 106 | fun PlayerSkinData.toMinestom(): PlayerSkin { 107 | val encoder = Base64.getEncoder() 108 | return PlayerSkin(encoder.encodeToString(textures), encoder.encodeToString(signature)) 109 | } 110 | 111 | 112 | fun Replay.getEntity(entity: Entity): RecordableEntity? { 113 | return getEntityById(entity.entityId) 114 | } 115 | 116 | internal fun Consumer.getMetadataArray(): ByteArray { 117 | return BinaryWriter().use { accept(it); it.toByteArray() } 118 | } 119 | 120 | internal fun EntityMetaDataPacket.getMetadataArray(): ByteArray { 121 | val binaryWriter = BinaryWriter() 122 | write(binaryWriter) 123 | return binaryWriter.toByteArray() 124 | } 125 | 126 | internal fun EquipmentHandler.getEquipmentForEntity(): Map = 127 | EntityEquipmentPacket.Slot.values().map { 128 | EntityEquipmentSlot.valueOf(it.name) to getEquipment(it).toReplay() 129 | }.toMap() 130 | 131 | internal fun EquipmentHandler.setEquipmentForEntity(equipment: Map) = 132 | equipment.forEach { 133 | val item = it.value.toMinestom() 134 | when (it.key) { 135 | EntityEquipmentSlot.MAIN_HAND -> itemInMainHand = item 136 | EntityEquipmentSlot.OFF_HAND -> itemInOffHand = item 137 | EntityEquipmentSlot.BOOTS -> boots = item 138 | EntityEquipmentSlot.LEGGINGS -> leggings = item 139 | EntityEquipmentSlot.CHESTPLATE -> chestplate = item 140 | EntityEquipmentSlot.HELMET -> helmet = item 141 | } 142 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/helpers/EntityHelper.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.helpers 2 | 3 | import io.github.openminigameserver.replay.extensions.setEquipmentForEntity 4 | import io.github.openminigameserver.replay.extensions.toMinestom 5 | import io.github.openminigameserver.replay.model.recordable.entity.data.BaseEntityData 6 | import io.github.openminigameserver.replay.model.recordable.entity.data.EquipmentEntityData 7 | import io.github.openminigameserver.replay.model.recordable.entity.data.PlayerEntityData 8 | import net.minestom.server.entity.* 9 | import net.minestom.server.entity.type.animal.* 10 | import net.minestom.server.entity.type.decoration.EntityArmorStand 11 | import net.minestom.server.entity.type.decoration.EntityItemFrame 12 | import net.minestom.server.entity.type.monster.* 13 | import net.minestom.server.entity.type.other.EntityAreaEffectCloud 14 | import net.minestom.server.entity.type.other.EntityEndCrystal 15 | import net.minestom.server.entity.type.other.EntityIronGolem 16 | import net.minestom.server.entity.type.other.EntitySnowman 17 | import net.minestom.server.entity.type.projectile.EntityEyeOfEnder 18 | import net.minestom.server.entity.type.projectile.EntityPotion 19 | import net.minestom.server.entity.type.vehicle.EntityBoat 20 | import net.minestom.server.inventory.EquipmentHandler 21 | import net.minestom.server.utils.Position 22 | import org.apache.commons.lang3.reflect.ConstructorUtils 23 | import java.util.* 24 | 25 | internal object EntityHelper { 26 | 27 | private val entityTypeMap = mutableMapOf>() 28 | 29 | @JvmStatic 30 | fun init() { 31 | entityTypeMap[EntityType.POTION] = EntityPotion::class.java 32 | entityTypeMap[EntityType.CAVE_SPIDER] = EntityCaveSpider::class.java 33 | entityTypeMap[EntityType.SILVERFISH] = EntitySilverfish::class.java 34 | entityTypeMap[EntityType.COW] = EntityCow::class.java 35 | entityTypeMap[EntityType.BLAZE] = EntityBlaze::class.java 36 | entityTypeMap[EntityType.ZOMBIFIED_PIGLIN] = EntityZombifiedPiglin::class.java 37 | entityTypeMap[EntityType.PANDA] = EntityPanda::class.java 38 | entityTypeMap[EntityType.ARMOR_STAND] = EntityArmorStand::class.java 39 | entityTypeMap[EntityType.GIANT] = EntityGiant::class.java 40 | entityTypeMap[EntityType.PHANTOM] = EntityPhantom::class.java 41 | entityTypeMap[EntityType.GHAST] = EntityGhast::class.java 42 | entityTypeMap[EntityType.BEE] = EntityBee::class.java 43 | entityTypeMap[EntityType.SPIDER] = EntitySpider::class.java 44 | entityTypeMap[EntityType.EXPERIENCE_ORB] = ExperienceOrb::class.java 45 | entityTypeMap[EntityType.ITEM] = ItemEntity::class.java 46 | entityTypeMap[EntityType.ITEM_FRAME] = EntityItemFrame::class.java 47 | entityTypeMap[EntityType.END_CRYSTAL] = EntityEndCrystal::class.java 48 | entityTypeMap[EntityType.SNOW_GOLEM] = EntitySnowman::class.java 49 | entityTypeMap[EntityType.RABBIT] = EntityRabbit::class.java 50 | entityTypeMap[EntityType.WITCH] = EntityWitch::class.java 51 | entityTypeMap[EntityType.ENDERMITE] = EntityEndermite::class.java 52 | entityTypeMap[EntityType.GUARDIAN] = EntityGuardian::class.java 53 | entityTypeMap[EntityType.EYE_OF_ENDER] = EntityEyeOfEnder::class.java 54 | entityTypeMap[EntityType.POLAR_BEAR] = EntityPolarBear::class.java 55 | entityTypeMap[EntityType.OCELOT] = EntityOcelot::class.java 56 | entityTypeMap[EntityType.CAT] = EntityCat::class.java 57 | entityTypeMap[EntityType.CHICKEN] = EntityChicken::class.java 58 | entityTypeMap[EntityType.IRON_GOLEM] = EntityIronGolem::class.java 59 | entityTypeMap[EntityType.BOAT] = EntityBoat::class.java 60 | entityTypeMap[EntityType.AREA_EFFECT_CLOUD] = EntityAreaEffectCloud::class.java 61 | entityTypeMap[EntityType.ZOMBIE] = EntityZombie::class.java 62 | entityTypeMap[EntityType.DOLPHIN] = EntityDolphin::class.java 63 | entityTypeMap[EntityType.FOX] = EntityFox::class.java 64 | entityTypeMap[EntityType.PIG] = EntityPig::class.java 65 | entityTypeMap[EntityType.LLAMA] = EntityLlama::class.java 66 | entityTypeMap[EntityType.CREEPER] = EntityCreeper::class.java 67 | entityTypeMap[EntityType.ARMOR_STAND] = EntityArmorStand::class.java 68 | entityTypeMap[EntityType.SLIME] = EntitySlime::class.java 69 | entityTypeMap[EntityType.MOOSHROOM] = EntityMooshroom::class.java 70 | } 71 | 72 | fun createEntity( 73 | type: EntityType, 74 | spawnPosition: Position, 75 | entityData: BaseEntityData?, 76 | isAutoViewable: Boolean 77 | ): Entity = 78 | (if (type == EntityType.PLAYER && entityData is PlayerEntityData) { 79 | ReplayPlayerEntity(UUID.randomUUID(), entityData.userName, entityData.metadata).also { 80 | it.skin = entityData.skin?.toMinestom() 81 | } 82 | } else { 83 | val entityTypeClazz = entityTypeMap[type] 84 | 85 | if (entityTypeClazz != null) { 86 | ConstructorUtils.invokeConstructor(entityTypeClazz, spawnPosition) 87 | } else { 88 | object : EntityCreature(type, spawnPosition) { 89 | override fun update(time: Long) { 90 | } 91 | 92 | override fun spawn() { 93 | } 94 | } 95 | } 96 | 97 | }).also { 98 | it.isAutoViewable = isAutoViewable 99 | it.setNoGravity(true) 100 | if (it is LivingEntity) { 101 | it.health = it.maxHealth 102 | } 103 | 104 | if (it is EquipmentHandler && entityData is EquipmentEntityData) { 105 | it.setEquipmentForEntity(entityData.equipment) 106 | } 107 | } 108 | 109 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/helpers/EntityManager.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.helpers 2 | 3 | import io.github.openminigameserver.replay.extensions.toMinestom 4 | import io.github.openminigameserver.replay.extensions.toReplay 5 | import io.github.openminigameserver.replay.model.recordable.RecordablePosition 6 | import io.github.openminigameserver.replay.model.recordable.RecordableVector 7 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 8 | import io.github.openminigameserver.replay.model.recordable.impl.RecEntitiesPosition 9 | import io.github.openminigameserver.replay.model.recordable.impl.RecEntityMove 10 | import io.github.openminigameserver.replay.platform.minestom.MinestomReplayEntity 11 | import io.github.openminigameserver.replay.platform.minestom.MinestomReplayPlatform 12 | import io.github.openminigameserver.replay.platform.minestom.MinestomReplayUser 13 | import io.github.openminigameserver.replay.platform.minestom.MinestomReplayWorld 14 | import io.github.openminigameserver.replay.replayer.IEntityManager 15 | import io.github.openminigameserver.replay.replayer.ReplaySession 16 | import net.minestom.server.entity.Player 17 | import net.minestom.server.registry.Registries 18 | import net.minestom.server.utils.Vector 19 | import kotlin.time.Duration 20 | 21 | class EntityManager(val platform: MinestomReplayPlatform, override var session: ReplaySession) : 22 | IEntityManager { 23 | override val entities: Collection 24 | get() = replayEntities.keys.mapNotNull { session.replay.getEntityById(it) } 25 | 26 | //Replay entity id 27 | override val replayEntities = mutableMapOf() 28 | 29 | override fun resetEntity(entity: RecordableEntity, startTime: Duration, targetReplayTime: Duration) { 30 | getNativeEntity(entity)?.let { e -> 31 | val nativeEntity = e.entity 32 | nativeEntity.remove() 33 | nativeEntity.askSynchronization() 34 | 35 | //Check if Entity (has been spawned at start) or (has been spawned somewhere before and has not been removed before) 36 | val shouldSpawn = true 37 | 38 | //Find actual position 39 | var finalPos = entity.spawnPosition?.position 40 | session.findActionsForEntity(startTime, entity, targetReplayTime) 41 | ?.let { finalPos = it.data.position } 42 | session.findLastAction( 43 | startTime, 44 | targetReplayTime 45 | ) { it.positions.containsKey(entity) } 46 | ?.let { finalPos = it.positions[entity]!!.position } 47 | 48 | nativeEntity.velocity = Vector(0.0, 0.0, 0.0) 49 | finalPos?.let { previousLoc -> 50 | if (shouldSpawn) { 51 | this.spawnEntity(entity, previousLoc) 52 | } 53 | } 54 | } 55 | } 56 | 57 | override fun spawnEntity( 58 | entity: RecordableEntity, 59 | position: RecordablePosition, 60 | velocity: RecordableVector 61 | ) { 62 | replayEntities[entity.id]?.takeIf { !it.entity.isRemoved }?.entity?.remove() 63 | val spawnPosition = position.toMinestom() 64 | val minestomEntity = 65 | MinestomReplayEntity( 66 | platform, 67 | EntityHelper.createEntity( 68 | Registries.getEntityType(entity.type)!!, 69 | spawnPosition, 70 | entity.entityData, 71 | session.replay.hasChunks 72 | ) 73 | ) 74 | replayEntities[entity.id] = minestomEntity 75 | 76 | if (minestomEntity.instance != session.world) 77 | minestomEntity.entity.setInstance((session.world as MinestomReplayWorld).instance) 78 | 79 | refreshPosition(minestomEntity, spawnPosition.toReplay()) 80 | minestomEntity.velocity = velocity 81 | 82 | session.viewers.forEach { 83 | minestomEntity.entity.addViewer((it as MinestomReplayUser).player) 84 | } 85 | } 86 | 87 | override fun refreshPosition(entity: MinestomReplayEntity, position: RecordablePosition) { 88 | val minestomEntity = entity.entity 89 | minestomEntity.velocity = Vector(0.0, 0.0, 0.0) 90 | minestomEntity.refreshPosition(position.toMinestom()) 91 | minestomEntity.refreshView(position.yaw, position.pitch) 92 | minestomEntity.askSynchronization() 93 | } 94 | 95 | override fun getNativeEntity(entity: RecordableEntity): MinestomReplayEntity? { 96 | return replayEntities[entity.id] 97 | } 98 | 99 | override fun removeEntity(entity: RecordableEntity) { 100 | getNativeEntity(entity)?.entity?.remove() 101 | replayEntities.remove(entity.id) 102 | } 103 | 104 | override fun removeNativeEntity(entity: MinestomReplayEntity) { 105 | entity.entity.remove() 106 | replayEntities.remove(entity.id) 107 | } 108 | 109 | override fun removeAllEntities() { 110 | replayEntities.forEach { 111 | val entity = it.value.entity 112 | if (entity !is Player || entity is ReplayPlayerEntity) 113 | entity.remove() 114 | } 115 | replayEntities.clear() 116 | } 117 | 118 | override fun removeEntityViewer(player: MinestomReplayUser) { 119 | replayEntities.forEach { 120 | it.value.entity.removeViewer(player.player) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/helpers/ReplayPlayerEntity.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.helpers 2 | 3 | import io.netty.channel.local.LocalAddress 4 | import net.minestom.server.entity.Player 5 | import net.minestom.server.network.packet.server.ServerPacket 6 | import net.minestom.server.network.packet.server.play.EntityMetaDataPacket 7 | import net.minestom.server.network.player.PlayerConnection 8 | import net.minestom.server.utils.binary.BinaryWriter 9 | import java.net.SocketAddress 10 | import java.util.* 11 | import java.util.concurrent.atomic.AtomicInteger 12 | 13 | 14 | class ReplayPlayerEntity(uuid: UUID, username: String, private val firstMetadata: ByteArray) : 15 | Player(uuid, "$username§r".take(16), ReplayPlayerConnection()) { 16 | init { 17 | settings.refresh(Locale.ENGLISH.toLanguageTag(), 0, ChatMode.ENABLED, true, 127, MainHand.RIGHT) 18 | } 19 | 20 | private var isFirstMetadata = true 21 | 22 | override fun getMetadataPacket(): EntityMetaDataPacket { 23 | if (isFirstMetadata) { 24 | isFirstMetadata = false 25 | return object : EntityMetaDataPacket() { 26 | override fun write(writer: BinaryWriter) { 27 | writer.writeVarInt(this@ReplayPlayerEntity.entityId) 28 | writer.write(firstMetadata) 29 | writer.writeByte(0xFF.toByte()) 30 | } 31 | } 32 | } 33 | return super.getMetadataPacket() 34 | } 35 | } 36 | 37 | 38 | class ReplayPlayerConnection : PlayerConnection() { 39 | private val id = counter.getAndIncrement() 40 | 41 | companion object { 42 | private val counter = AtomicInteger() 43 | } 44 | 45 | override fun sendPacket(serverPacket: ServerPacket) { 46 | } 47 | 48 | /** 49 | * Gets the remote address of the client. 50 | * 51 | * @return the remote address 52 | */ 53 | override fun getRemoteAddress(): SocketAddress { 54 | return LocalAddress("replay-player$id") 55 | } 56 | 57 | /** 58 | * Forcing the player to disconnect. 59 | */ 60 | override fun disconnect() { 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/platform/minestom/ItemStackExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.platform.minestom 2 | 3 | import io.github.openminigameserver.replay.replayer.statehelper.ControlItemAction 4 | import net.minestom.server.data.DataImpl 5 | import net.minestom.server.item.ItemStack 6 | 7 | const val actionData = "controlItem" 8 | 9 | var ItemStack.controlItemAction: ControlItemAction 10 | get() = data?.get(actionData) ?: ControlItemAction.NONE 11 | set(value) { 12 | if (data == null) { 13 | data = DataImpl() 14 | } 15 | data?.set(actionData, value) 16 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/platform/minestom/MinestomActionPlayerManager.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.platform.minestom 2 | 3 | import io.github.openminigameserver.replay.platform.ReplayPlatform 4 | import io.github.openminigameserver.replay.replayer.ActionPlayerManager 5 | import io.github.openminigameserver.replay.replayer.impl.* 6 | 7 | class MinestomActionPlayerManager(platform: ReplayPlatform) : 8 | ActionPlayerManager(platform) { 9 | 10 | init { 11 | registerActionPlayerGeneric(RecBlockBreakAnimationPlayer) 12 | registerActionPlayerGeneric(RecBlockEffectPlayer) 13 | registerActionPlayerGeneric(RecBlockStateUpdatePlayer) 14 | registerActionPlayerGeneric(RecEntitiesPositionPlayer) 15 | registerActionPlayerGeneric(RecEntityEquipmentUpdatePlayer) 16 | registerActionPlayerGeneric(RecEntityMetadataPlayer) 17 | registerActionPlayerGeneric(RecEntityMovePlayer) 18 | registerActionPlayerGeneric(RecEntityRemovePlayer) 19 | registerActionPlayerGeneric(RecEntitySpawnPlayer) 20 | registerActionPlayerGeneric(RecParticleEffectPlayer) 21 | registerActionPlayerGeneric(RecPlayerHandAnimationPlayer) 22 | registerActionPlayerGeneric(RecSoundEffectPlayer) 23 | registerActionPlayerGeneric(RecBlockStateBatchUpdatePlayer) 24 | } 25 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/platform/minestom/MinestomReplayChunk.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.platform.minestom 2 | 3 | import io.github.openminigameserver.replay.abstraction.ReplayChunk 4 | import net.minestom.server.instance.Chunk 5 | 6 | class MinestomReplayChunk(private val chunk: Chunk) : ReplayChunk { 7 | override val x: Int 8 | get() = chunk.chunkX 9 | override val z: Int 10 | get() = chunk.chunkZ 11 | override val serializedData: ByteArray? 12 | get() = chunk.serializedData 13 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/platform/minestom/MinestomReplayEntity.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.platform.minestom 2 | 3 | import io.github.openminigameserver.replay.abstraction.ReplayEntity 4 | import io.github.openminigameserver.replay.abstraction.ReplayWorld 5 | import io.github.openminigameserver.replay.extensions.getEquipmentForEntity 6 | import io.github.openminigameserver.replay.extensions.toMinestom 7 | import io.github.openminigameserver.replay.extensions.toReplay 8 | import io.github.openminigameserver.replay.model.recordable.RecordableItemStack 9 | import io.github.openminigameserver.replay.model.recordable.RecordablePosition 10 | import io.github.openminigameserver.replay.model.recordable.RecordableVector 11 | import io.github.openminigameserver.replay.model.recordable.entity.EntityEquipmentSlot 12 | import net.minestom.server.entity.Entity 13 | import net.minestom.server.inventory.EquipmentHandler 14 | import java.util.* 15 | 16 | class MinestomReplayEntity(private val replayPlatform: MinestomReplayPlatform, val entity: Entity) : ReplayEntity { 17 | override val id: Int 18 | get() = entity.entityId 19 | override val uuid: UUID 20 | get() = entity.uuid 21 | override val position: RecordablePosition 22 | get() = entity.position.toReplay() 23 | override var velocity: RecordableVector 24 | get() = entity.velocity.toReplay() 25 | set(value) { 26 | entity.velocity = value.toMinestom() 27 | } 28 | override val world: ReplayWorld? 29 | get() = entity.instance?.uniqueId?.let { replayPlatform.getWorldById(it) } 30 | 31 | override fun getEquipment(): Map { 32 | if (entity is EquipmentHandler) { 33 | return entity.getEquipmentForEntity() 34 | } 35 | return emptyMap() 36 | } 37 | 38 | override fun teleport(position: RecordablePosition) { 39 | entity.teleport(position.toMinestom()) 40 | } 41 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/platform/minestom/MinestomReplayPlatform.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.platform.minestom 2 | 3 | import cloud.commandframework.CommandManager 4 | import cloud.commandframework.annotations.AnnotationParser 5 | import io.github.openminigameserver.replay.MinestomReplayExtension 6 | import io.github.openminigameserver.replay.TickTime 7 | import io.github.openminigameserver.replay.abstraction.* 8 | import io.github.openminigameserver.replay.helpers.EntityManager 9 | import io.github.openminigameserver.replay.model.Replay 10 | import io.github.openminigameserver.replay.model.recordable.entity.data.BaseEntityData 11 | import io.github.openminigameserver.replay.platform.IdHelperContainer 12 | import io.github.openminigameserver.replay.platform.ReplayPlatform 13 | import io.github.openminigameserver.replay.platform.minestom.replayer.getPlayerInventoryCopy 14 | import io.github.openminigameserver.replay.platform.minestom.replayer.loadAllItems 15 | import io.github.openminigameserver.replay.replayer.ActionPlayerManager 16 | import io.github.openminigameserver.replay.replayer.IEntityManager 17 | import io.github.openminigameserver.replay.replayer.ReplayChunkLoader 18 | import io.github.openminigameserver.replay.replayer.ReplaySession 19 | import net.kyori.adventure.platform.minestom.MinestomComponentSerializer 20 | import net.kyori.adventure.text.Component 21 | import net.minestom.server.MinecraftServer 22 | import net.minestom.server.chat.ChatColor 23 | import net.minestom.server.chat.ColoredText 24 | import net.minestom.server.data.DataImpl 25 | import net.minestom.server.entity.Entity 26 | import net.minestom.server.entity.Player 27 | import net.minestom.server.item.ItemStack 28 | import net.minestom.server.item.metadata.PlayerHeadMeta 29 | import net.minestom.server.network.packet.server.play.TeamsPacket 30 | import net.minestom.server.scoreboard.Team 31 | import net.minestom.server.timer.Task 32 | import net.minestom.server.utils.time.TimeUnit 33 | import org.jglrxavpok.hephaistos.nbt.NBTCompound 34 | import org.jglrxavpok.hephaistos.nbt.NBTList 35 | import java.io.File 36 | import java.util.* 37 | 38 | class MinestomReplayPlatform(private val replayExtension: MinestomReplayExtension) : 39 | ReplayPlatform() { 40 | override val name: String 41 | get() = "Minestom" 42 | override val version: String 43 | get() = MinecraftServer.VERSION_NAME 44 | 45 | override val commandManager: CommandManager = ReplayCommandManager(this) 46 | override val commandAnnotationParser: AnnotationParser = 47 | AnnotationParser(commandManager, ReplayUser::class.java) { commandManager.createDefaultCommandMeta() } 48 | override val dataDir: File 49 | get() = MinestomReplayExtension.dataFolder 50 | 51 | override fun log(message: String) { 52 | replayExtension.minestomLogger.info(message) 53 | } 54 | 55 | override fun cancelTask(tickerTask: Any) { 56 | val task = tickerTask as? Task ?: return 57 | task.cancel() 58 | } 59 | 60 | override fun registerSyncRepeatingTask(time: TickTime, action: () -> Unit): Any { 61 | return MinecraftServer.getSchedulerManager().buildTask(action) 62 | .repeat(time.unit.toMilliseconds(time.time), TimeUnit.MILLISECOND).schedule() 63 | } 64 | 65 | override fun getEntityType(replayEntity: MinestomReplayEntity): String { 66 | return replayEntity.entity.entityType.namespaceID 67 | } 68 | 69 | override fun getEntityData(replayEntity: MinestomReplayEntity): BaseEntityData? { 70 | return null 71 | } 72 | 73 | override val actionPlayerManager: ActionPlayerManager = 74 | MinestomActionPlayerManager(this) 75 | 76 | private val viewerTeam: Team = MinecraftServer.getTeamManager().createBuilder("ReplayViewers") 77 | .prefix(ColoredText.of(ChatColor.GRAY, "[Viewer] ")) 78 | .collisionRule(TeamsPacket.CollisionRule.NEVER) 79 | .teamColor(ChatColor.GRAY) 80 | .build() 81 | 82 | override fun addToViewerTeam(p: MinestomReplayUser) { 83 | viewerTeam.addMember(p.player.username) 84 | } 85 | 86 | override fun removeFromViewerTeam(player: MinestomReplayUser) { 87 | viewerTeam.removeMember(player.player.username) 88 | } 89 | 90 | override fun unregisterWorld(instance: MinestomReplayWorld) { 91 | MinecraftServer.getInstanceManager().unregisterInstance(instance.instance) 92 | } 93 | 94 | override fun getWorldById(it: UUID): MinestomReplayWorld { 95 | return worlds.getOrCompute(it) 96 | } 97 | 98 | override fun getEntityManager(replaySession: ReplaySession): IEntityManager { 99 | return EntityManager(MinestomReplayExtension.platform, replaySession) 100 | } 101 | 102 | private fun createEmptyReplayInstance(replay: Replay) = 103 | ( 104 | MinecraftServer.getInstanceManager().createInstanceContainer().apply { 105 | enableAutoChunkLoad(true) 106 | data = DataImpl() 107 | chunkLoader = ReplayChunkLoader(replay) 108 | } 109 | ).let { worlds.getOrCompute(it.uniqueId) } 110 | 111 | override fun createReplaySession( 112 | replay: Replay, 113 | viewers: MutableList, 114 | instance: ReplayWorld?, 115 | tickTime: TickTime 116 | ): ReplaySession { 117 | val hasChunks = replay.hasChunks 118 | val finalInstance = 119 | if (hasChunks) createEmptyReplayInstance(replay) else instance!! 120 | 121 | return ReplaySession( 122 | MinestomReplayExtension.platform as ReplayPlatform, 123 | finalInstance, 124 | replay, 125 | viewers, 126 | tickTime 127 | ) 128 | } 129 | 130 | override fun getPlayerInventoryCopy(player: MinestomReplayUser): Any { 131 | return getPlayerInventoryCopy(player.player) 132 | } 133 | 134 | override fun loadPlayerInventoryCopy(player: MinestomReplayUser, inventory: Any) { 135 | @Suppress("UNCHECKED_CAST") 136 | (inventory as? NBTList)?.let { loadAllItems(it, player.player.inventory) } 137 | } 138 | 139 | fun getPlayer(player: Player): ReplayUser { 140 | return entities.getOrCompute(player.entityId) as ReplayUser 141 | } 142 | 143 | fun getItemStack(itemInHand: ItemStack): ReplayActionItemStack { 144 | val action = itemInHand.controlItemAction 145 | val skin = (itemInHand.itemMeta as? PlayerHeadMeta)?.playerSkin?.let { 146 | ReplayHeadTextureSkin( 147 | it.textures, 148 | it.signature 149 | ) 150 | } 151 | 152 | return ReplayActionItemStack(itemInHand.displayName?.let { 153 | MinestomComponentSerializer.get().deserialize( 154 | it 155 | ) 156 | } ?: Component.empty(), action, skin) 157 | } 158 | 159 | fun getEntity(entity: Entity): ReplayEntity { 160 | return entities.getOrCompute(entity.entityId) 161 | } 162 | 163 | override val worlds: IdHelperContainer = 164 | IdHelperContainer { MinestomReplayWorld(MinecraftServer.getInstanceManager().getInstance(this)!!) } 165 | 166 | override val entities: IdHelperContainer = IdHelperContainer { 167 | val entity: Entity = Player.getEntity(this) ?: Entity.getEntity(this)!! 168 | if (entity is Player) MinestomReplayUser(this@MinestomReplayPlatform, entity) else 169 | MinestomReplayEntity(this@MinestomReplayPlatform, entity) 170 | } 171 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/platform/minestom/MinestomReplayUser.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.platform.minestom 2 | 3 | import io.github.openminigameserver.replay.abstraction.* 4 | import io.github.openminigameserver.replay.replayer.statehelper.ControlItemAction 5 | import net.kyori.adventure.audience.Audience 6 | import net.kyori.adventure.platform.minestom.MinestomAudiences 7 | import net.kyori.adventure.platform.minestom.MinestomComponentSerializer 8 | import net.kyori.adventure.text.Component.text 9 | import net.minestom.server.entity.GameMode 10 | import net.minestom.server.entity.Player 11 | import net.minestom.server.entity.PlayerSkin 12 | import net.minestom.server.item.ItemStack 13 | import net.minestom.server.item.Material 14 | import net.minestom.server.item.metadata.PlayerHeadMeta 15 | import java.util.* 16 | 17 | internal val audiences = MinestomAudiences.create() 18 | 19 | class MinestomReplayUser(val replayPlatform: MinestomReplayPlatform, val player: Player) : ReplayUser(), 20 | ReplayEntity by MinestomReplayEntity(replayPlatform, player) { 21 | override var exp: Float 22 | get() = player.exp 23 | set(value) { 24 | player.exp = value 25 | } 26 | override val heldSlot: Byte 27 | get() = player.heldSlot 28 | override var isFlying: Boolean 29 | get() = player.isFlying 30 | set(value) { 31 | player.isFlying = value 32 | } 33 | override var isAllowFlying: Boolean 34 | get() = player.isFlying 35 | set(value) { 36 | player.isFlying = value 37 | } 38 | override var gameMode: ReplayGameMode 39 | get() = ReplayGameMode.valueOf(player.gameMode.name) 40 | set(value) { 41 | player.gameMode = GameMode.valueOf(value.name) 42 | } 43 | override val audience: Audience = audiences.player(player) 44 | override val name: String 45 | get() = player.username 46 | 47 | override fun setWorld(instance: ReplayWorld) { 48 | (instance as? MinestomReplayWorld)?.instance?.let { player.setInstance(it) } 49 | } 50 | 51 | override fun clearInventory() { 52 | player.inventory.clear() 53 | } 54 | 55 | override fun setHeldItemSlot(slot: Byte) { 56 | player.setHeldItemSlot(slot) 57 | } 58 | 59 | override fun setItemStack(slot: Int, itemStack: ReplayActionItemStack) { 60 | player.inventory.setItemStack(slot, itemStack.toMinestom()) 61 | } 62 | } 63 | 64 | private fun ReplayActionItemStack.toMinestom(): ItemStack { 65 | val material = when (action) { 66 | ControlItemAction.NONE -> Material.AIR 67 | 68 | ControlItemAction.PAUSE -> Material.PINK_DYE 69 | ControlItemAction.RESUME -> Material.GRAY_DYE 70 | ControlItemAction.PLAY_AGAIN -> Material.LIME_DYE 71 | 72 | else -> Material.PLAYER_HEAD 73 | } 74 | 75 | return ItemStack(material, 1).apply { 76 | val itemSkin = skin 77 | if (itemSkin != null) { 78 | itemMeta = PlayerHeadMeta().apply { 79 | setSkullOwner(UUID.randomUUID()) 80 | setPlayerSkin(PlayerSkin(itemSkin.value, itemSkin.signature)) 81 | } 82 | } 83 | 84 | displayName = MinestomComponentSerializer.get().serialize(text("§r").append(title)) 85 | controlItemAction = action 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/platform/minestom/MinestomReplayWorld.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.platform.minestom 2 | 3 | import io.github.openminigameserver.replay.MinestomReplayExtension 4 | import io.github.openminigameserver.replay.abstraction.ReplayChunk 5 | import io.github.openminigameserver.replay.abstraction.ReplayEntity 6 | import io.github.openminigameserver.replay.abstraction.ReplayWorld 7 | import net.minestom.server.instance.Instance 8 | import java.util.* 9 | 10 | class MinestomReplayWorld(val instance: Instance) : ReplayWorld() { 11 | override val uuid: UUID 12 | get() = instance.uniqueId 13 | override val entities: Iterable 14 | get() = instance.entities.map { MinestomReplayExtension.platform.entities.getOrCompute(it.entityId) } 15 | override val chunks: Iterable 16 | get() = instance.chunks.map { MinestomReplayChunk(it) } 17 | 18 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/platform/minestom/ReplayCommandManager.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.platform.minestom 2 | 3 | import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator 4 | import io.github.openminigameserver.cloudminestom.MinestomCommandManager 5 | import io.github.openminigameserver.replay.abstraction.ReplayUser 6 | import java.util.function.Function 7 | 8 | class ReplayCommandManager(replayPlatform: MinestomReplayPlatform) : MinestomCommandManager( 9 | AsynchronousCommandExecutionCoordinator.newBuilder().withAsynchronousParsing().build(), 10 | Function { 11 | replayPlatform.entities.getOrCompute(it.asPlayer().entityId) as ReplayUser 12 | }, Function { 13 | (it as MinestomReplayUser).player 14 | } 15 | ) -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/platform/minestom/replayer/MinestomActionPlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.platform.minestom.replayer 2 | 3 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 4 | import io.github.openminigameserver.replay.platform.minestom.MinestomReplayUser 5 | import io.github.openminigameserver.replay.platform.minestom.MinestomReplayWorld 6 | import io.github.openminigameserver.replay.replayer.ActionPlayer 7 | import io.github.openminigameserver.replay.replayer.ReplaySession 8 | import net.minestom.server.entity.Player 9 | import net.minestom.server.instance.Instance 10 | 11 | interface MinestomActionPlayer : ActionPlayer { 12 | fun play(action: T, session: ReplaySession, instance: Instance, viewers: List) 13 | 14 | override fun play( 15 | action: T, 16 | session: ReplaySession, 17 | instance: MinestomReplayWorld, 18 | viewers: List 19 | ) { 20 | play(action, session, instance.instance, viewers.map { it.player }) 21 | } 22 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/platform/minestom/replayer/MinestomEntityActionPlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.platform.minestom.replayer 2 | 3 | import io.github.openminigameserver.replay.model.recordable.EntityRecordableAction 4 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 5 | import io.github.openminigameserver.replay.platform.minestom.MinestomReplayEntity 6 | import io.github.openminigameserver.replay.platform.minestom.MinestomReplayUser 7 | import io.github.openminigameserver.replay.platform.minestom.MinestomReplayWorld 8 | import io.github.openminigameserver.replay.replayer.EntityActionPlayer 9 | import io.github.openminigameserver.replay.replayer.ReplaySession 10 | import net.minestom.server.entity.Entity 11 | import net.minestom.server.entity.Player 12 | import net.minestom.server.instance.Instance 13 | 14 | abstract class MinestomEntityActionPlayer : 15 | EntityActionPlayer() { 16 | override fun play( 17 | action: T, 18 | replayEntity: RecordableEntity, 19 | nativeEntity: MinestomReplayEntity, 20 | session: ReplaySession, 21 | instance: MinestomReplayWorld, 22 | viewers: List 23 | ) { 24 | play(action, replayEntity, nativeEntity.entity, session, instance.instance, viewers.map { it.player }) 25 | } 26 | 27 | abstract fun play( 28 | action: T, 29 | replayEntity: RecordableEntity, 30 | nativeEntity: Entity, 31 | session: ReplaySession, 32 | instance: Instance, 33 | viewers: List 34 | ) 35 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/platform/minestom/replayer/PlayerInventoryHelper.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.platform.minestom.replayer 2 | 3 | import net.minestom.server.entity.Player 4 | import net.minestom.server.inventory.PlayerInventory 5 | import net.minestom.server.item.ItemStack 6 | import net.minestom.server.registry.Registries 7 | import net.minestom.server.utils.NBTUtils 8 | import org.jglrxavpok.hephaistos.nbt.NBTCompound 9 | import org.jglrxavpok.hephaistos.nbt.NBTList 10 | import org.jglrxavpok.hephaistos.nbt.NBTTypes 11 | 12 | internal fun getPlayerInventoryCopy(player: Player) = NBTList(NBTTypes.TAG_Compound).also { 13 | saveAllItems( 14 | it, 15 | player.inventory 16 | ) 17 | } 18 | 19 | internal fun loadAllItems(items: NBTList, destination: PlayerInventory) { 20 | destination.clear() 21 | for (tag in items) { 22 | val item = Registries.getMaterial(tag.getString("id")) 23 | val stack = ItemStack(item, tag.getByte("Count")!!) 24 | if (tag.containsKey("tag")) { 25 | NBTUtils.loadDataIntoItem(stack, tag.getCompound("tag")!!) 26 | } 27 | destination.setItemStack(tag.getByte("Slot")!!.toInt(), stack) 28 | } 29 | destination.update() 30 | } 31 | 32 | internal fun saveAllItems(list: NBTList, inventory: PlayerInventory) { 33 | for (i in 0 until inventory.size) { 34 | val stack = inventory.getItemStack(i) 35 | val nbt = NBTCompound() 36 | val tag = NBTCompound() 37 | NBTUtils.saveDataIntoNBT(stack, tag) 38 | nbt["tag"] = tag 39 | nbt.setByte("Slot", i.toByte()) 40 | nbt.setByte("Count", stack.amount) 41 | nbt.setString("id", stack.material.getName()) 42 | list.add(nbt) 43 | } 44 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/replayer/ReplayChunkLoader.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer 2 | 3 | import com.google.common.collect.HashBasedTable 4 | import io.github.openminigameserver.replay.model.Replay 5 | import io.github.openminigameserver.replay.model.recordable.RecordedChunk 6 | import net.minestom.server.instance.* 7 | import net.minestom.server.utils.binary.BinaryReader 8 | import net.minestom.server.utils.chunk.ChunkCallback 9 | import net.minestom.server.world.biomes.Biome 10 | import java.util.* 11 | 12 | class ReplayChunkLoader(val replay: Replay) : IChunkLoader { 13 | private val chunksMap: HashBasedTable = HashBasedTable.create() 14 | 15 | init { 16 | replay.chunks.forEach { 17 | chunksMap.put(it.chunkX, it.chunkZ, it) 18 | } 19 | } 20 | 21 | override fun loadChunk(instance: Instance, chunkX: Int, chunkZ: Int, callback: ChunkCallback?): Boolean { 22 | val biomes = arrayOfNulls(Chunk.BIOME_COUNT).also { Arrays.fill(it, Biome.PLAINS) } 23 | val chunk = if (instance is InstanceContainer) instance.chunkSupplier.createChunk( 24 | biomes, 25 | chunkX, 26 | chunkZ 27 | ) else DynamicChunk(biomes, chunkX, chunkZ) 28 | 29 | val savedChunk = chunksMap.get(chunkX, chunkZ) 30 | if (savedChunk != null) { 31 | val reader = BinaryReader(savedChunk.data) 32 | chunk.readChunk(reader, callback) 33 | } else { 34 | callback?.accept(chunk) 35 | } 36 | return true 37 | 38 | } 39 | 40 | override fun saveChunk(chunk: Chunk, callback: Runnable?) { 41 | callback?.run() 42 | } 43 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/replayer/impl/RecBlockBreakAnimationPlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.impl 2 | 3 | import io.github.openminigameserver.replay.extensions.toMinestom 4 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 5 | import io.github.openminigameserver.replay.model.recordable.impl.RecBlockBreakAnimation 6 | import io.github.openminigameserver.replay.platform.minestom.replayer.MinestomEntityActionPlayer 7 | import io.github.openminigameserver.replay.replayer.ReplaySession 8 | import net.minestom.server.entity.Entity 9 | import net.minestom.server.entity.Player 10 | import net.minestom.server.instance.Instance 11 | import net.minestom.server.network.packet.server.play.BlockBreakAnimationPacket 12 | import net.minestom.server.utils.PacketUtils 13 | 14 | object RecBlockBreakAnimationPlayer : MinestomEntityActionPlayer() { 15 | override fun play( 16 | action: RecBlockBreakAnimation, 17 | replayEntity: RecordableEntity, 18 | nativeEntity: Entity, 19 | session: ReplaySession, 20 | instance: Instance, 21 | viewers: List 22 | ) { 23 | PacketUtils.sendGroupedPacket( 24 | viewers, 25 | BlockBreakAnimationPacket( 26 | nativeEntity.entityId, 27 | action.position.toMinestom().toBlockPosition(), 28 | action.destroyStage 29 | ) 30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/replayer/impl/RecBlockEffectPlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.impl 2 | 3 | import io.github.openminigameserver.replay.extensions.toMinestom 4 | import io.github.openminigameserver.replay.model.recordable.impl.RecBlockEffect 5 | import io.github.openminigameserver.replay.platform.minestom.replayer.MinestomActionPlayer 6 | import io.github.openminigameserver.replay.replayer.ReplaySession 7 | import net.minestom.server.entity.Player 8 | import net.minestom.server.instance.Instance 9 | import net.minestom.server.network.packet.server.play.EffectPacket 10 | import net.minestom.server.utils.PacketUtils 11 | 12 | object RecBlockEffectPlayer : MinestomActionPlayer { 13 | override fun play(action: RecBlockEffect, session: ReplaySession, instance: Instance, viewers: List) { 14 | PacketUtils.sendGroupedPacket(viewers, EffectPacket().apply { 15 | effectId = action.effectId 16 | this.data = action.data 17 | this.position = action.position.toMinestom().toBlockPosition() 18 | this.disableRelativeVolume = action.disableRelativeVolume 19 | }) 20 | } 21 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/replayer/impl/RecBlockStateBatchUpdatePlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.impl 2 | 3 | import io.github.openminigameserver.replay.extensions.toMinestom 4 | import io.github.openminigameserver.replay.model.recordable.impl.RecBlockStateBatchUpdate 5 | import io.github.openminigameserver.replay.platform.minestom.replayer.MinestomActionPlayer 6 | import io.github.openminigameserver.replay.replayer.ReplaySession 7 | import net.minestom.server.entity.Player 8 | import net.minestom.server.instance.Instance 9 | 10 | object RecBlockStateBatchUpdatePlayer : MinestomActionPlayer { 11 | override fun play( 12 | action: RecBlockStateBatchUpdate, 13 | session: ReplaySession, 14 | instance: Instance, 15 | viewers: List 16 | ) { 17 | val batch = instance.createBlockBatch() 18 | action.actions.forEach { 19 | batch.setBlockStateId(it.position.toMinestom().toBlockPosition(), it.newState) 20 | } 21 | batch.flush {} 22 | } 23 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/replayer/impl/RecBlockStateUpdatePlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.impl 2 | 3 | import io.github.openminigameserver.replay.extensions.toMinestom 4 | import io.github.openminigameserver.replay.model.recordable.impl.RecBlockStateUpdate 5 | import io.github.openminigameserver.replay.platform.minestom.replayer.MinestomActionPlayer 6 | import io.github.openminigameserver.replay.replayer.ReplaySession 7 | import net.minestom.server.entity.Player 8 | import net.minestom.server.instance.Instance 9 | 10 | object RecBlockStateUpdatePlayer : MinestomActionPlayer { 11 | override fun play(action: RecBlockStateUpdate, session: ReplaySession, instance: Instance, viewers: List) { 12 | instance.setBlockStateId(action.position.toMinestom().toBlockPosition(), action.newState) 13 | } 14 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/replayer/impl/RecEntitiesPositionPlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.impl 2 | 3 | import io.github.openminigameserver.replay.extensions.toMinestom 4 | import io.github.openminigameserver.replay.model.recordable.impl.RecEntitiesPosition 5 | import io.github.openminigameserver.replay.platform.minestom.MinestomReplayEntity 6 | import io.github.openminigameserver.replay.platform.minestom.replayer.MinestomActionPlayer 7 | import io.github.openminigameserver.replay.replayer.ReplaySession 8 | import net.minestom.server.entity.Player 9 | import net.minestom.server.instance.Instance 10 | 11 | object RecEntitiesPositionPlayer : MinestomActionPlayer { 12 | override fun play(action: RecEntitiesPosition, session: ReplaySession, instance: Instance, viewers: List) { 13 | action.positions.forEach { 14 | val entity = 15 | (session.entityManager.getNativeEntity(it.key) as? MinestomReplayEntity)?.entity ?: return@forEach 16 | 17 | val data = it.value 18 | val position = data.position.toMinestom() 19 | val velocity = data.velocity.toMinestom() 20 | entity.refreshPosition(position) 21 | entity.refreshView(position.yaw, position.pitch) 22 | entity.askSynchronization() 23 | entity.velocity = velocity 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/replayer/impl/RecEntityEquipmentUpdatePlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.impl 2 | 3 | import io.github.openminigameserver.replay.extensions.setEquipmentForEntity 4 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 5 | import io.github.openminigameserver.replay.model.recordable.impl.RecEntityEquipmentUpdate 6 | import io.github.openminigameserver.replay.platform.minestom.replayer.MinestomEntityActionPlayer 7 | import io.github.openminigameserver.replay.replayer.ReplaySession 8 | import net.minestom.server.entity.Entity 9 | import net.minestom.server.entity.Player 10 | import net.minestom.server.instance.Instance 11 | import net.minestom.server.inventory.EquipmentHandler 12 | 13 | object RecEntityEquipmentUpdatePlayer : MinestomEntityActionPlayer() { 14 | override fun play( 15 | action: RecEntityEquipmentUpdate, 16 | replayEntity: RecordableEntity, 17 | nativeEntity: Entity, 18 | session: ReplaySession, 19 | instance: Instance, 20 | viewers: List 21 | ) { 22 | if (nativeEntity is EquipmentHandler) { 23 | nativeEntity.setEquipmentForEntity(action.equipment) 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/replayer/impl/RecEntityMetadataPlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 4 | import io.github.openminigameserver.replay.model.recordable.impl.RecEntityMetadata 5 | import io.github.openminigameserver.replay.platform.minestom.replayer.MinestomEntityActionPlayer 6 | import io.github.openminigameserver.replay.replayer.ReplaySession 7 | import net.minestom.server.entity.Entity 8 | import net.minestom.server.entity.Player 9 | import net.minestom.server.instance.Instance 10 | import net.minestom.server.network.packet.server.play.EntityMetaDataPacket 11 | import net.minestom.server.utils.PacketUtils 12 | import net.minestom.server.utils.binary.BinaryWriter 13 | 14 | object RecEntityMetadataPlayer : MinestomEntityActionPlayer() { 15 | override fun play( 16 | action: RecEntityMetadata, 17 | replayEntity: RecordableEntity, 18 | nativeEntity: Entity, 19 | session: ReplaySession, 20 | instance: Instance, 21 | viewers: List 22 | ) { 23 | PacketUtils.sendGroupedPacket(viewers, object : EntityMetaDataPacket() { 24 | override fun write(writer: BinaryWriter) { 25 | writer.writeVarInt(nativeEntity.entityId) 26 | writer.write(action.metadata) 27 | writer.writeByte(0xFF.toByte()) 28 | } 29 | }) 30 | } 31 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/replayer/impl/RecEntityMovePlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.impl 2 | 3 | import io.github.openminigameserver.replay.extensions.toMinestom 4 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 5 | import io.github.openminigameserver.replay.model.recordable.impl.RecEntityMove 6 | import io.github.openminigameserver.replay.platform.minestom.replayer.MinestomEntityActionPlayer 7 | import io.github.openminigameserver.replay.replayer.ReplaySession 8 | import net.minestom.server.entity.Entity 9 | import net.minestom.server.entity.Player 10 | import net.minestom.server.instance.Instance 11 | 12 | object RecEntityMovePlayer : MinestomEntityActionPlayer() { 13 | override fun play( 14 | action: RecEntityMove, 15 | replayEntity: RecordableEntity, 16 | nativeEntity: Entity, 17 | session: ReplaySession, 18 | instance: Instance, 19 | viewers: List 20 | ) { 21 | val data = action.data 22 | val position = data.position.toMinestom() 23 | val velocity = data.velocity.toMinestom() 24 | nativeEntity.refreshPosition(position) 25 | nativeEntity.refreshView(position.yaw, position.pitch) 26 | nativeEntity.askSynchronization() 27 | nativeEntity.velocity = velocity 28 | } 29 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/replayer/impl/RecEntityRemovePlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 4 | import io.github.openminigameserver.replay.model.recordable.impl.RecEntityRemove 5 | import io.github.openminigameserver.replay.platform.minestom.replayer.MinestomEntityActionPlayer 6 | import io.github.openminigameserver.replay.replayer.ReplaySession 7 | import net.minestom.server.entity.Entity 8 | import net.minestom.server.entity.Player 9 | import net.minestom.server.instance.Instance 10 | 11 | object RecEntityRemovePlayer : MinestomEntityActionPlayer() { 12 | override fun play( 13 | action: RecEntityRemove, 14 | replayEntity: RecordableEntity, 15 | nativeEntity: Entity, 16 | session: ReplaySession, 17 | instance: Instance, 18 | viewers: List 19 | ) { 20 | session.entityManager.removeEntity(replayEntity) 21 | } 22 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/replayer/impl/RecEntitySpawnPlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.impl.RecEntitySpawn 4 | import io.github.openminigameserver.replay.platform.minestom.replayer.MinestomActionPlayer 5 | import io.github.openminigameserver.replay.replayer.ReplaySession 6 | import net.minestom.server.entity.Player 7 | import net.minestom.server.instance.Instance 8 | 9 | object RecEntitySpawnPlayer : MinestomActionPlayer { 10 | 11 | override fun play(action: RecEntitySpawn, session: ReplaySession, instance: Instance, viewers: List) { 12 | session.entityManager.spawnEntity( 13 | action.entity, 14 | action.positionAndVelocity.position, 15 | action.positionAndVelocity.velocity 16 | ) 17 | } 18 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/replayer/impl/RecParticleEffectPlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.impl.RecParticleEffect 4 | import io.github.openminigameserver.replay.platform.minestom.replayer.MinestomActionPlayer 5 | import io.github.openminigameserver.replay.replayer.ReplaySession 6 | import net.minestom.server.entity.Player 7 | import net.minestom.server.instance.Instance 8 | import net.minestom.server.network.packet.server.play.ParticlePacket 9 | import net.minestom.server.utils.PacketUtils 10 | import java.util.function.Consumer 11 | 12 | object RecParticleEffectPlayer : MinestomActionPlayer { 13 | override fun play(action: RecParticleEffect, session: ReplaySession, instance: Instance, viewers: List) { 14 | PacketUtils.sendGroupedPacket(viewers, ParticlePacket().apply { 15 | this.particleId = action.particleId 16 | this.particleCount = action.particleCount 17 | this.particleData = action.particleData 18 | this.x = action.x 19 | this.y = action.y 20 | this.z = action.z 21 | this.offsetX = action.offsetX 22 | this.offsetY = action.offsetY 23 | this.offsetZ = action.offsetZ 24 | this.longDistance = action.longDistance 25 | this.dataConsumer = action.extraData?.let { bytes -> Consumer { it.writeBytes(bytes) } } 26 | }) 27 | } 28 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/replayer/impl/RecPlayerHandAnimationPlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 4 | import io.github.openminigameserver.replay.model.recordable.impl.Hand 5 | import io.github.openminigameserver.replay.model.recordable.impl.RecPlayerHandAnimation 6 | import io.github.openminigameserver.replay.platform.minestom.replayer.MinestomEntityActionPlayer 7 | import io.github.openminigameserver.replay.replayer.ReplaySession 8 | import net.minestom.server.entity.Entity 9 | import net.minestom.server.entity.Player 10 | import net.minestom.server.instance.Instance 11 | 12 | object RecPlayerHandAnimationPlayer : MinestomEntityActionPlayer() { 13 | override fun play( 14 | action: RecPlayerHandAnimation, 15 | replayEntity: RecordableEntity, 16 | nativeEntity: Entity, 17 | session: ReplaySession, 18 | instance: Instance, 19 | viewers: List 20 | ) { 21 | if (nativeEntity !is Player) return 22 | when (action.hand) { 23 | Hand.MAIN -> nativeEntity.swingMainHand() 24 | Hand.OFF -> nativeEntity.swingOffHand() 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/kotlin/io/github/openminigameserver/replay/replayer/impl/RecSoundEffectPlayer.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.replayer.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.impl.RecSoundEffect 4 | import io.github.openminigameserver.replay.platform.minestom.replayer.MinestomActionPlayer 5 | import io.github.openminigameserver.replay.replayer.ReplaySession 6 | import net.minestom.server.entity.Player 7 | import net.minestom.server.instance.Instance 8 | import net.minestom.server.network.packet.server.play.SoundEffectPacket 9 | import net.minestom.server.sound.SoundCategory 10 | import net.minestom.server.utils.PacketUtils 11 | 12 | object RecSoundEffectPlayer : MinestomActionPlayer { 13 | override fun play( 14 | @Suppress("DuplicatedCode") action: RecSoundEffect, 15 | session: ReplaySession, 16 | instance: Instance, 17 | viewers: List 18 | ) { 19 | PacketUtils.sendGroupedPacket(viewers, SoundEffectPacket().apply { 20 | this.soundId = action.soundId 21 | this.soundCategory = action.soundCategory?.let { SoundCategory.valueOf(it.name) } 22 | this.pitch = action.pitch 23 | this.volume = action.volume 24 | this.x = action.x 25 | this.y = action.y 26 | this.z = action.z 27 | }) 28 | } 29 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/resources/extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Replay", 3 | "entrypoint": "io.github.openminigameserver.replay.MinestomReplayExtension", 4 | "version": "${version}", 5 | "authors": [ 6 | "NickAcPT" 7 | ], 8 | "mixinConfig": "mixins.replay.json" 9 | } -------------------------------------------------------------------------------- /impl-minestom/src/main/resources/mixins.replay.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "minVersion": "0.8", 4 | "package": "io.github.openminigameserver.replay.mixins", 5 | "compatibilityLevel": "JAVA_11", 6 | "mixins": [ 7 | "InstanceMixin", 8 | "PacketUtilsMixin", 9 | "PlayerInventoryMixin", 10 | "PlayerMixin" 11 | ] 12 | } -------------------------------------------------------------------------------- /impl-minestom/src/template/kotlin/io/github/openminigameserver/replay/BuildInfo.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay; 2 | 3 | object BuildInfo { 4 | const val version: String = "${version}" 5 | } -------------------------------------------------------------------------------- /impl-minestom/src/test/kotlin/io/github/openminigameserver/replay/test/EntityCommand.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.test 2 | 3 | /* 4 | object EntityCommand : Command("entity") { 5 | 6 | val entities = mutableListOf() 7 | 8 | init { 9 | addSyntax({ sender: CommandSender, args: Arguments -> 10 | if (sender !is Player) return@addSyntax 11 | val type = args.getEntityType("type") 12 | 13 | val lastEntity = EntityHelper.createEntity(type, sender.position, null, false) 14 | lastEntity.isAutoViewable = true 15 | lastEntity.setInstance(sender.instance!!) 16 | entities.add(lastEntity) 17 | 18 | if (lastEntity is EntityCreature) { 19 | val targetGoal = FollowTargetGoal(lastEntity, UpdateOption(1, TimeUnit.TICK)) 20 | lastEntity.target = sender 21 | lastEntity.goalSelectors.add(targetGoal) 22 | } 23 | 24 | }, ArgumentWord("of").from("living"), ArgumentEntityType("type")) 25 | addSyntax({ sender: CommandSender, args: Arguments -> 26 | entities.forEach { 27 | if (it is EntityCreature) { 28 | it.currentGoalSelector?.end() 29 | it.goalSelectors.clear() 30 | } 31 | it.remove() 32 | } 33 | entities.clear() 34 | }, ArgumentWord("of").from("delete")) 35 | } 36 | }*/ 37 | -------------------------------------------------------------------------------- /impl-minestom/src/test/kotlin/io/github/openminigameserver/replay/test/MyChunkGenerator.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.test 2 | 3 | import net.minestom.server.instance.Chunk 4 | import net.minestom.server.instance.ChunkGenerator 5 | import net.minestom.server.instance.ChunkPopulator 6 | import net.minestom.server.instance.batch.ChunkBatch 7 | import net.minestom.server.instance.block.Block 8 | import net.minestom.server.world.biomes.Biome 9 | import java.util.* 10 | 11 | class MyChunkGenerator : ChunkGenerator { 12 | override fun generateChunkData(batch: ChunkBatch, chunkX: Int, chunkZ: Int) { 13 | // Set chunk blocks 14 | for (x in 0 until Chunk.CHUNK_SIZE_X) for (z in 0 until Chunk.CHUNK_SIZE_Z) { 15 | for (y in 0..39) { 16 | batch.setBlock(x, y, z, Block.STONE) 17 | } 18 | } 19 | } 20 | 21 | override fun fillBiomes(biomes: Array, chunkX: Int, chunkZ: Int) { 22 | Arrays.fill(biomes, Biome.PLAINS) 23 | } 24 | 25 | override fun getPopulators(): List? { 26 | return null 27 | } 28 | } -------------------------------------------------------------------------------- /impl-minestom/src/test/kotlin/io/github/openminigameserver/replay/test/PlayerInit.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.test 2 | 3 | import net.minestom.server.MinecraftServer 4 | import net.minestom.server.data.DataImpl 5 | import net.minestom.server.entity.GameMode 6 | import net.minestom.server.event.EventCallback 7 | import net.minestom.server.event.player.PlayerLoginEvent 8 | import net.minestom.server.instance.InstanceManager 9 | import net.minestom.server.utils.Position 10 | 11 | 12 | class PlayerInit : EventCallback { 13 | 14 | private val instanceManager: InstanceManager = MinecraftServer.getInstanceManager() 15 | 16 | // Create the instance 17 | private val instanceContainer by lazy { 18 | instanceManager.createInstanceContainer().apply { 19 | chunkGenerator = MyChunkGenerator() 20 | data = DataImpl() 21 | enableAutoChunkLoad(true) 22 | } 23 | } 24 | 25 | override fun run(event: PlayerLoginEvent) { 26 | val player = event.player 27 | player.data = DataImpl() 28 | player.gameMode = GameMode.CREATIVE 29 | player.isAllowFlying = true 30 | player.isFlying = true 31 | 32 | event.setSpawningInstance(instanceContainer) 33 | player.respawnPoint = Position(0.0, 42.0, 0.0) 34 | } 35 | } 36 | 37 | 38 | -------------------------------------------------------------------------------- /impl-minestom/src/test/kotlin/io/github/openminigameserver/replay/test/Replay.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.test 2 | 3 | import io.github.openminigameserver.replay.extensions.profileCache 4 | import net.minestom.server.MinecraftServer 5 | import net.minestom.server.entity.PlayerSkin 6 | import net.minestom.server.event.player.PlayerLoginEvent 7 | import net.minestom.server.event.player.PlayerSkinInitEvent 8 | import net.minestom.server.extras.MojangAuth 9 | import net.minestom.server.extras.PlacementRules 10 | import net.minestom.server.extras.optifine.OptifineSupport 11 | import net.minestom.server.extras.selfmodification.MinestomRootClassLoader 12 | 13 | fun main(args: Array) { 14 | MinestomRootClassLoader.getInstance().protectedPackages.addAll( 15 | arrayOf( 16 | "org.reactivestreams", 17 | "io.leangen.geantyref", 18 | "kotlinx" 19 | ) 20 | ) 21 | 22 | val server = MinecraftServer.init() 23 | MojangAuth.init() 24 | OptifineSupport.enable() 25 | PlacementRules.init() 26 | MinecraftServer.setGroupedPacket(false) 27 | 28 | 29 | MinecraftServer.getGlobalEventHandler().addEventCallback(PlayerSkinInitEvent::class.java) { 30 | it.apply { 31 | kotlin.runCatching { 32 | skin = profileCache.get(this.player.uuid) { 33 | kotlin.runCatching { PlayerSkin.fromUuid(this.player.uuid.toString()) }.getOrNull() 34 | } 35 | } 36 | } 37 | } 38 | MinecraftServer.getGlobalEventHandler().addEventCallback(PlayerLoginEvent::class.java, PlayerInit()) 39 | 40 | 41 | // MinecraftServer.getCommandManager().register(EntityCommand) 42 | server.start( 43 | "0.0.0.0", 25566 44 | ) { _, responseData -> 45 | responseData.apply { 46 | responseData.setDescription("Replay") 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /impl-minestom/src/test/kotlin/io/github/openminigameserver/replay/test/ReplayBootstrapper.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.test 2 | 3 | import net.minestom.server.Bootstrap 4 | 5 | fun main(args: Array) { 6 | System.setProperty("minestom.extension.indevfolder.classes", "../impl-minestom/build/classes/java") 7 | System.setProperty("minestom.extension.indevfolder.resources", "../impl-minestom/build/resources/main/") 8 | 9 | Bootstrap.bootstrap("io.github.openminigameserver.replay.test.ReplayKt", args) 10 | } -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 -------------------------------------------------------------------------------- /model/build.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation("com.github.luben:zstd-jni:1.4.8-1") 3 | implementation("com.esotericsoftware:kryo:5.0.3") 4 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/AbstractReplaySession.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay 2 | 3 | import io.github.openminigameserver.replay.model.Replay 4 | import io.github.openminigameserver.replay.model.recordable.EntityRecordableAction 5 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 6 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 7 | import java.util.* 8 | import kotlin.time.Duration 9 | 10 | abstract class AbstractReplaySession { 11 | abstract val replay: Replay 12 | var isInitialized = false 13 | abstract val hasEnded: Boolean 14 | protected val actions = Stack() 15 | 16 | /** 17 | * Current replay time. 18 | */ 19 | abstract var time: Duration 20 | 21 | fun findManyActions( 22 | startDuration: Duration = time, 23 | targetDuration: Duration = Duration.ZERO, 24 | condition: (RecordableAction) -> Boolean = { true } 25 | ): List { 26 | var start = startDuration 27 | var end = targetDuration 28 | val isReverse = start > end 29 | 30 | if (isReverse) { 31 | start = end.also { end = start } 32 | } 33 | 34 | val actions = replay.actions.filter { it.timestamp in start..end } 35 | return actions.filter { condition(it) } 36 | .let { action -> if (isReverse) action.sortedByDescending { it.timestamp } else action.sortedBy { it.timestamp } } 37 | } 38 | 39 | 40 | inline fun findLastAction( 41 | startDuration: Duration = time, 42 | targetDuration: Duration = Duration.ZERO, 43 | condition: (T) -> Boolean = { true } 44 | ): T? { 45 | var start = startDuration 46 | var end = targetDuration 47 | val isReverse = start > end 48 | 49 | if (isReverse) { 50 | start = end.also { end = start } 51 | } 52 | 53 | return replay.actions.filter { it.timestamp in start..end } 54 | .let { action -> if (isReverse) action.sortedByDescending { it.timestamp } else action.sortedBy { it.timestamp } } 55 | .lastOrNull { it is T && condition(it) } as? T 56 | } 57 | 58 | 59 | inline fun findManyActionsGeneric( 60 | startDuration: Duration = time, 61 | targetDuration: Duration = Duration.ZERO, 62 | crossinline condition: (T) -> Boolean = { true } 63 | ): List { 64 | return findManyActions(startDuration, targetDuration) { 65 | it is T && condition(it) 66 | }.map { it as T } 67 | } 68 | 69 | inline fun findActionsForEntity( 70 | startDuration: Duration = time, 71 | entity: RecordableEntity, 72 | targetDuration: Duration = Duration.ZERO, 73 | condition: (T) -> Boolean = { true } 74 | ): T? { 75 | return findLastAction(startDuration, targetDuration) { it.entity == entity && condition(it) } 76 | } 77 | 78 | abstract fun init() 79 | 80 | abstract fun unInit() 81 | 82 | abstract fun tick(forceTick: Boolean = false, isTimeStep: Boolean = false) 83 | 84 | abstract fun playAction(action: RecordableAction) 85 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/io/ReplayFile.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.io 2 | 3 | import com.esotericsoftware.kryo.io.Input 4 | import com.esotericsoftware.kryo.io.Output 5 | import com.github.luben.zstd.ZstdInputStream 6 | import com.github.luben.zstd.ZstdOutputStream 7 | import io.github.openminigameserver.replay.io.format.readHeader 8 | import io.github.openminigameserver.replay.io.format.readReplayData 9 | import io.github.openminigameserver.replay.io.format.writeHeader 10 | import io.github.openminigameserver.replay.io.format.writeReplayData 11 | import io.github.openminigameserver.replay.model.Replay 12 | import java.io.File 13 | 14 | class ReplayFile(private val file: File, var replay: Replay? = null, private val isCompressed: Boolean = true) { 15 | 16 | fun loadReplay() { 17 | replay = Replay().apply { 18 | Input(getInputStream().readAllBytes()).use { 19 | it.readHeader(this) 20 | it.readReplayData(this) 21 | } 22 | } 23 | } 24 | 25 | fun saveReplay() { 26 | Output(getOutputStream()).use { 27 | it.writeHeader(replay!!) 28 | it.writeReplayData(replay!!) 29 | } 30 | } 31 | 32 | private fun getInputStream() = if (isCompressed) ZstdInputStream(file.inputStream()) else file.inputStream() 33 | 34 | private fun getOutputStream() = if (isCompressed) ZstdOutputStream(file.outputStream()) else file.outputStream() 35 | 36 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/io/format/ReadByteBufExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.io.format 2 | 3 | import com.esotericsoftware.kryo.io.Input 4 | import io.github.openminigameserver.replay.model.Replay 5 | import io.github.openminigameserver.replay.model.ReplayHeader 6 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 7 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 8 | import kotlinx.datetime.Instant 9 | import java.util.* 10 | import kotlin.time.Duration 11 | import kotlin.time.milliseconds 12 | 13 | fun Input.readUUID(): UUID { 14 | return UUID(readLong(), readLong()) 15 | } 16 | 17 | fun Input.readInstant(): Instant { 18 | return Instant.fromEpochSeconds(readLong()) 19 | } 20 | 21 | fun Input.readObject(): Any { 22 | return kryo.readClassAndObject(this) 23 | } 24 | 25 | fun Input.readMap(): Map { 26 | val result = mutableMapOf() 27 | repeat(readInt()) { 28 | result[readObject()] = readObject() 29 | } 30 | return result 31 | } 32 | 33 | fun Input.readDuration(): Duration { 34 | return readLong().milliseconds 35 | } 36 | 37 | inline fun Input.readToCollection(obj: MutableCollection) { 38 | repeat(readInt()) { 39 | kryo.readObjectOrNull(this, C::class.java)?.let { it1 -> obj.add(it1) } 40 | } 41 | } 42 | 43 | fun Input.readHeader(destination: ReplayHeader) { 44 | destination.apply { 45 | version = readInt() 46 | id = readUUID() 47 | recordStartTime = readInstant() 48 | metadata = readMap() as MutableMap 49 | duration = readDuration() 50 | if (version > 3) { 51 | readToCollection(chunks) 52 | } 53 | } 54 | } 55 | 56 | 57 | fun Input.readReplayData(replay: Replay) { 58 | replay.apply { 59 | repeat(readInt()) { 60 | entities[readInt()] = readObject() as RecordableEntity 61 | } 62 | 63 | repeat(readInt()) { 64 | (readObject() as? RecordableAction?).also { actions.add(it) } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/io/format/WriteByteBufExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.io.format 2 | 3 | import com.esotericsoftware.kryo.Kryo 4 | import com.esotericsoftware.kryo.io.Output 5 | import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy 6 | import io.github.openminigameserver.replay.model.Replay 7 | import io.github.openminigameserver.replay.model.ReplayHeader 8 | import kotlinx.datetime.Instant 9 | import org.objenesis.strategy.StdInstantiatorStrategy 10 | import java.util.* 11 | import kotlin.time.Duration 12 | 13 | 14 | var kryo = Kryo().apply { 15 | isRegistrationRequired = false 16 | instantiatorStrategy = DefaultInstantiatorStrategy(StdInstantiatorStrategy()) 17 | } 18 | 19 | fun Output.writeUUID(id: UUID) { 20 | writeLong(id.mostSignificantBits) 21 | writeLong(id.leastSignificantBits) 22 | } 23 | 24 | fun Output.writeInstant(instant: Instant) { 25 | writeLong(instant.epochSeconds) 26 | } 27 | 28 | fun Output.writeDuration(duration: Duration) { 29 | writeLong(duration.toLongMilliseconds()) 30 | } 31 | 32 | fun Output.writeObject(obj: Any) { 33 | kryo.writeClassAndObject(this, obj) 34 | } 35 | 36 | inline fun Output.writeCollection(obj: Collection) { 37 | writeInt(obj.size) 38 | obj.forEach { kryo.writeObjectOrNull(this, it, C::class.java) } 39 | } 40 | 41 | fun Output.writeMap(map: Map) { 42 | writeInt(map.size) 43 | map.forEach { (k, v) -> 44 | writeObject(k) 45 | writeObject(v) 46 | } 47 | } 48 | 49 | fun Output.writeHeader(header: ReplayHeader) { 50 | header.apply { 51 | writeInt(version) 52 | writeUUID(id) 53 | writeInstant(recordStartTime) 54 | writeMap(metadata) 55 | writeDuration(duration) 56 | if (version > 3) { 57 | writeCollection(chunks) 58 | } 59 | } 60 | } 61 | 62 | fun Output.writeReplayData(replay: Replay) { 63 | replay.apply { 64 | writeInt(entities.size) 65 | entities.forEach { (id, entity) -> 66 | writeInt(id) 67 | writeObject(entity) 68 | } 69 | 70 | writeInt(replay.actions.size) 71 | replay.actions.forEach { 72 | writeObject(it) 73 | } 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/Replay.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model 2 | 3 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 4 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 5 | import kotlinx.datetime.Clock.System.now 6 | import kotlinx.datetime.Instant 7 | import java.util.* 8 | import java.util.concurrent.ConcurrentLinkedDeque 9 | import kotlin.time.Duration 10 | 11 | data class Replay( 12 | override var version: Int = 4, 13 | override var id: UUID = UUID.randomUUID(), 14 | override var recordStartTime: Instant = now(), 15 | val entities: MutableMap = mutableMapOf(), 16 | val actions: ConcurrentLinkedDeque = ConcurrentLinkedDeque() 17 | ) : ReplayHeader(version, id, recordStartTime) { 18 | 19 | val currentDuration: Duration 20 | get() { 21 | return now().minus(recordStartTime) 22 | } 23 | 24 | fun addAction(action: RecordableAction) { 25 | action.timestamp = currentDuration 26 | actions.add(action) 27 | } 28 | 29 | fun getEntityById(id: Int): RecordableEntity? { 30 | return entities[id] 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/ReplayHeader.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model 2 | 3 | import io.github.openminigameserver.replay.model.recordable.RecordedChunk 4 | import kotlinx.datetime.Clock 5 | import kotlinx.datetime.Instant 6 | import java.util.* 7 | import kotlin.time.Duration 8 | 9 | open class ReplayHeader( 10 | open var version: Int = 4, 11 | open var id: UUID = UUID.randomUUID(), 12 | open var recordStartTime: Instant = Clock.System.now() 13 | ) { 14 | var metadata: MutableMap = mutableMapOf() 15 | var duration: Duration = Duration.ZERO 16 | var chunks = mutableListOf() 17 | 18 | val hasChunks get() = chunks.isNotEmpty() 19 | 20 | operator fun get(name: String): T? { 21 | return metadata[name] as? T 22 | } 23 | 24 | operator fun set(name: String, value: T) { 25 | metadata[name] = value 26 | } 27 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/EntityRecordableAction.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable 2 | 3 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 4 | 5 | abstract class EntityRecordableAction(open val entity: RecordableEntity) : RecordableAction() { 6 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/RecordableAction.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable 2 | 3 | import kotlin.time.Duration 4 | 5 | abstract class RecordableAction { 6 | var timestamp: Duration = Duration.ZERO 7 | } 8 | -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/RecordableItemStack.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable 2 | 3 | data class RecordableItemStack(val nbtValue: ByteArray) { 4 | override fun equals(other: Any?): Boolean { 5 | if (this === other) return true 6 | if (javaClass != other?.javaClass) return false 7 | 8 | other as RecordableItemStack 9 | 10 | if (!nbtValue.contentEquals(other.nbtValue)) return false 11 | 12 | return true 13 | } 14 | 15 | override fun hashCode(): Int { 16 | return nbtValue.contentHashCode() 17 | } 18 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/RecordablePosition.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable 2 | 3 | data class RecordablePosition( 4 | val x: Double = 0.0, 5 | val y: Double = 0.0, 6 | val z: Double = 0.0, 7 | val yaw: Float = 0f, 8 | val pitch: Float = 0f 9 | ) 10 | -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/RecordablePositionAndVector.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable 2 | 3 | data class RecordablePositionAndVector(val position: RecordablePosition, val velocity: RecordableVector) -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/RecordableVector.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable 2 | 3 | data class RecordableVector(val x: Double = 0.0, val y: Double = 0.0, val z: Double = 0.0) 4 | -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/RecordedChunk.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable 2 | 3 | data class RecordedChunk(val chunkX: Int, val chunkZ: Int, val data: ByteArray) { 4 | override fun equals(other: Any?): Boolean { 5 | if (this === other) return true 6 | if (javaClass != other?.javaClass) return false 7 | 8 | other as RecordedChunk 9 | 10 | if (chunkX != other.chunkX) return false 11 | if (chunkZ != other.chunkZ) return false 12 | if (!data.contentEquals(other.data)) return false 13 | 14 | return true 15 | } 16 | 17 | override fun hashCode(): Int { 18 | var result = chunkX 19 | result = 31 * result + chunkZ 20 | result = 31 * result + data.contentHashCode() 21 | return result 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/entity/EntityEquipmentSlot.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.entity 2 | 3 | enum class EntityEquipmentSlot { 4 | MAIN_HAND, 5 | OFF_HAND, 6 | BOOTS, 7 | LEGGINGS, 8 | CHESTPLATE, 9 | HELMET 10 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/entity/RecordableEntity.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.entity 2 | 3 | import io.github.openminigameserver.replay.model.recordable.RecordablePositionAndVector 4 | import io.github.openminigameserver.replay.model.recordable.entity.data.BaseEntityData 5 | 6 | data class RecordableEntity( 7 | val id: Int, 8 | val type: String, 9 | val spawnPosition: RecordablePositionAndVector?, 10 | val entityData: BaseEntityData? = null 11 | ) { 12 | var spawnOnStart: Boolean = spawnPosition != null 13 | 14 | override fun equals(other: Any?): Boolean { 15 | if (this === other) return true 16 | if (javaClass != other?.javaClass) return false 17 | 18 | other as RecordableEntity 19 | 20 | if (id != other.id) return false 21 | 22 | return true 23 | } 24 | 25 | override fun hashCode(): Int { 26 | return id 27 | } 28 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/entity/data/BaseEntityData.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.entity.data 2 | 3 | abstract class BaseEntityData -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/entity/data/EquipmentEntityData.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.entity.data 2 | 3 | import io.github.openminigameserver.replay.model.recordable.RecordableItemStack 4 | import io.github.openminigameserver.replay.model.recordable.entity.EntityEquipmentSlot 5 | 6 | interface EquipmentEntityData { 7 | val equipment: Map 8 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/entity/data/PlayerEntityData.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.entity.data 2 | 3 | import io.github.openminigameserver.replay.model.recordable.RecordableItemStack 4 | import io.github.openminigameserver.replay.model.recordable.entity.EntityEquipmentSlot 5 | 6 | data class PlayerEntityData(val userName: String, val skin: PlayerSkinData?, val metadata: ByteArray, 7 | override val equipment: Map 8 | ) : 9 | BaseEntityData(), EquipmentEntityData { 10 | override fun equals(other: Any?): Boolean { 11 | if (this === other) return true 12 | if (javaClass != other?.javaClass) return false 13 | 14 | other as PlayerEntityData 15 | 16 | if (userName != other.userName) return false 17 | if (skin != other.skin) return false 18 | if (!metadata.contentEquals(other.metadata)) return false 19 | 20 | return true 21 | } 22 | 23 | override fun hashCode(): Int { 24 | var result = userName.hashCode() 25 | result = 31 * result + (skin?.hashCode() ?: 0) 26 | result = 31 * result + metadata.contentHashCode() 27 | return result 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/entity/data/PlayerSkinData.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.entity.data 2 | 3 | data class PlayerSkinData(val textures: ByteArray, val signature: ByteArray) { 4 | override fun equals(other: Any?): Boolean { 5 | if (this === other) return true 6 | if (javaClass != other?.javaClass) return false 7 | 8 | other as PlayerSkinData 9 | 10 | if (!textures.contentEquals(other.textures)) return false 11 | if (!signature.contentEquals(other.signature)) return false 12 | 13 | return true 14 | } 15 | 16 | override fun hashCode(): Int { 17 | var result = textures.contentHashCode() 18 | result = 31 * result + signature.contentHashCode() 19 | return result 20 | } 21 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/impl/RecBlockBreakAnimation.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.EntityRecordableAction 4 | import io.github.openminigameserver.replay.model.recordable.RecordablePosition 5 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 6 | 7 | class RecBlockBreakAnimation(entity: RecordableEntity, val position: RecordablePosition, val destroyStage: Byte) : 8 | EntityRecordableAction(entity) -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/impl/RecBlockEffect.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 4 | import io.github.openminigameserver.replay.model.recordable.RecordablePosition 5 | 6 | class RecBlockEffect( 7 | val effectId: Int, 8 | val position: RecordablePosition, 9 | val data: Int, 10 | val disableRelativeVolume: Boolean 11 | ) : RecordableAction() -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/impl/RecBlockStateBatchUpdate.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 4 | 5 | 6 | data class RecBlockStateBatchUpdate(val actions: List) : RecordableAction() -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/impl/RecBlockStateUpdate.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 4 | import io.github.openminigameserver.replay.model.recordable.RecordablePosition 5 | import io.github.openminigameserver.replay.model.recordable.reverse.DefaultStateReversible 6 | 7 | data class RecBlockStateUpdate(val position: RecordablePosition, val newState: Short) : 8 | RecordableAction(), DefaultStateReversible { 9 | override val isAppliedInBatch: Boolean 10 | get() = true 11 | 12 | override fun batchActions(value: List): RecordableAction { 13 | return RecBlockStateBatchUpdate(value.mapNotNull { it as? RecBlockStateUpdate }) 14 | } 15 | 16 | override fun provideDefaultState(): RecordableAction { 17 | return RecBlockStateUpdate(position, 0) 18 | } 19 | 20 | override fun equals(other: Any?): Boolean { 21 | if (this === other) return true 22 | if (javaClass != other?.javaClass) return false 23 | 24 | other as RecBlockStateUpdate 25 | 26 | if (position != other.position) return false 27 | 28 | return true 29 | } 30 | 31 | override fun hashCode(): Int { 32 | return position.hashCode() 33 | } 34 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/impl/RecEntitiesPosition.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 4 | import io.github.openminigameserver.replay.model.recordable.RecordablePositionAndVector 5 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 6 | 7 | class RecEntitiesPosition( 8 | val positions: MutableMap 9 | ) : RecordableAction() -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/impl/RecEntityEquipmentUpdate.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.EntityRecordableAction 4 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 5 | import io.github.openminigameserver.replay.model.recordable.RecordableItemStack 6 | import io.github.openminigameserver.replay.model.recordable.entity.EntityEquipmentSlot 7 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 8 | import io.github.openminigameserver.replay.model.recordable.entity.data.EquipmentEntityData 9 | import io.github.openminigameserver.replay.model.recordable.reverse.ApplyLastReversible 10 | import io.github.openminigameserver.replay.model.recordable.reverse.DefaultStateReversible 11 | 12 | 13 | data class RecEntityEquipmentUpdate(override val entity: RecordableEntity, val equipment: Map) : 14 | EntityRecordableAction(entity), DefaultStateReversible, ApplyLastReversible { 15 | 16 | override fun provideDefaultState(): RecordableAction { 17 | return RecEntityEquipmentUpdate( 18 | entity, 19 | (entity.entityData as EquipmentEntityData).equipment 20 | ) 21 | } 22 | 23 | override fun equals(other: Any?): Boolean { 24 | if (this === other) return true 25 | if (javaClass != other?.javaClass) return false 26 | 27 | other as RecEntityEquipmentUpdate 28 | 29 | if (entity != other.entity) return false 30 | 31 | return true 32 | } 33 | 34 | override fun hashCode(): Int { 35 | return entity.hashCode() 36 | } 37 | 38 | 39 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/impl/RecEntityMetadata.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.EntityRecordableAction 4 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 5 | 6 | class RecEntityMetadata(val metadata: ByteArray, entity: RecordableEntity) : EntityRecordableAction(entity) -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/impl/RecEntityMove.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.EntityRecordableAction 4 | import io.github.openminigameserver.replay.model.recordable.RecordablePositionAndVector 5 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 6 | 7 | class RecEntityMove(val data: RecordablePositionAndVector, entity: RecordableEntity) : EntityRecordableAction(entity) 8 | 9 | -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/impl/RecEntityRemove.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.impl 2 | 3 | import io.github.openminigameserver.replay.AbstractReplaySession 4 | import io.github.openminigameserver.replay.model.recordable.* 5 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 6 | import io.github.openminigameserver.replay.model.recordable.reverse.Reversible 7 | import kotlin.time.Duration 8 | 9 | class RecEntityRemove( 10 | val position: RecordablePosition, 11 | entity: RecordableEntity 12 | ) : EntityRecordableAction(entity), Reversible { 13 | override fun provideRevertedActions(start: Duration, end: Duration, session: AbstractReplaySession): List = listOf(RecEntitySpawn( 14 | RecordablePositionAndVector(position, RecordableVector()), entity)) 15 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/impl/RecEntitySpawn.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.impl 2 | 3 | import io.github.openminigameserver.replay.AbstractReplaySession 4 | import io.github.openminigameserver.replay.model.recordable.EntityRecordableAction 5 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 6 | import io.github.openminigameserver.replay.model.recordable.RecordablePositionAndVector 7 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 8 | import io.github.openminigameserver.replay.model.recordable.reverse.Reversible 9 | import kotlin.time.Duration 10 | 11 | class RecEntitySpawn constructor( 12 | val positionAndVelocity: RecordablePositionAndVector, entity: RecordableEntity 13 | ) : EntityRecordableAction(entity), Reversible { 14 | 15 | override fun provideRevertedActions(start: Duration, end: Duration, session: AbstractReplaySession): List = listOf(RecEntityRemove(positionAndVelocity.position, entity)) 16 | } 17 | 18 | -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/impl/RecParticleEffect.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 4 | 5 | class RecParticleEffect( 6 | val particleId: Int = 0, 7 | val longDistance: Boolean = false, 8 | val x: Double = 0.0, 9 | var y: kotlin.Double = 0.0, 10 | var z: kotlin.Double = 0.0, 11 | val offsetX: Float = 0f, 12 | var offsetY: kotlin.Float = 0f, 13 | var offsetZ: kotlin.Float = 0f, 14 | val particleData: Float = 0f, 15 | val particleCount: Int = 0, 16 | val extraData: ByteArray? = null 17 | ) : RecordableAction() -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/impl/RecPlayerHandAnimation.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.EntityRecordableAction 4 | import io.github.openminigameserver.replay.model.recordable.entity.RecordableEntity 5 | 6 | enum class Hand { 7 | MAIN, OFF 8 | } 9 | 10 | class RecPlayerHandAnimation(val hand: Hand, entity: RecordableEntity) : EntityRecordableAction(entity) -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/impl/RecSoundEffect.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.impl 2 | 3 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 4 | 5 | enum class SoundCategory { 6 | MASTER, MUSIC, RECORDS, WEATHER, BLOCKS, HOSTILE, NEUTRAL, PLAYERS, AMBIENT, VOICE 7 | } 8 | 9 | class RecSoundEffect( 10 | var soundId: Int = 0, 11 | var soundCategory: SoundCategory? = null, 12 | var x: Int = 0, 13 | var y: Int = 0, 14 | var z: Int = 0, 15 | var volume: Float = 0f, 16 | var pitch: Float = 0f, 17 | ) : RecordableAction() -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/reverse/ApplyLastReversible.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.reverse 2 | 3 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 4 | 5 | 6 | /** 7 | * Helper class implementing a batch that will only return the last value of this group 8 | */ 9 | interface ApplyLastReversible : Reversible { 10 | 11 | override val isAppliedInBatch: Boolean 12 | get() = true 13 | 14 | override fun batchActions(value: List): RecordableAction? { 15 | return value.last() 16 | } 17 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/reverse/DefaultStateReversible.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.reverse 2 | 3 | import io.github.openminigameserver.replay.AbstractReplaySession 4 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 5 | import kotlin.time.Duration 6 | import kotlin.time.milliseconds 7 | 8 | /** 9 | * Helper class that will lookup a previous instance of itself or the default state 10 | */ 11 | interface DefaultStateReversible : Reversible { 12 | 13 | @JvmDefault 14 | override fun provideRevertedActions( 15 | start: Duration, 16 | end: Duration, 17 | session: AbstractReplaySession 18 | ): List { 19 | val forwardStep = end > start 20 | return session.findManyActions( 21 | Duration.ZERO, 22 | end.let { if (!forwardStep) it - 1.milliseconds else it } 23 | ) { 24 | it is DefaultStateReversible && isMatch(it) 25 | }.takeIf { it.isNotEmpty() } ?: listOf(provideDefaultState()) 26 | } 27 | 28 | /** 29 | * Provide the default state of this action 30 | */ 31 | fun provideDefaultState(): RecordableAction 32 | 33 | /** 34 | * Check whether this action is a match to the given action 35 | */ 36 | fun isMatch(other: RecordableAction): Boolean { 37 | return this == other 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/recordable/reverse/Reversible.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.recordable.reverse 2 | 3 | import io.github.openminigameserver.replay.AbstractReplaySession 4 | import io.github.openminigameserver.replay.model.recordable.RecordableAction 5 | import kotlin.time.Duration 6 | 7 | /** 8 | * Represents an action that is reversible 9 | */ 10 | interface Reversible { 11 | 12 | /** 13 | * States if actions like this are applied in batch 14 | */ 15 | val isAppliedInBatch: Boolean 16 | get() = false 17 | 18 | /** 19 | * Batch multiple actions of this type into a single action 20 | */ 21 | fun batchActions(value: List): RecordableAction? = null 22 | 23 | /** 24 | * Provide a list of actions that, when run, revert this action 25 | */ 26 | fun provideRevertedActions(start: Duration, end: Duration, session: AbstractReplaySession): List 27 | 28 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/storage/CompletableFutureReplayStorageSystem.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.storage 2 | 3 | import io.github.openminigameserver.replay.model.Replay 4 | import kotlinx.coroutines.future.await 5 | import java.util.* 6 | import java.util.concurrent.CompletableFuture 7 | 8 | abstract class CompletableFutureReplayStorageSystem : ReplayStorageSystem { 9 | 10 | abstract fun getReplaysForPlayerCompletable(player: UUID): CompletableFuture> 11 | abstract fun loadReplayCompletable(uuid: UUID): CompletableFuture 12 | abstract fun saveReplayCompletable(replay: Replay): CompletableFuture 13 | 14 | override suspend fun getReplaysForPlayer(player: UUID): List { 15 | return getReplaysForPlayerCompletable(player).await() 16 | } 17 | 18 | override suspend fun loadReplay(uuid: UUID): Replay? { 19 | return loadReplayCompletable(uuid).await() 20 | } 21 | 22 | override suspend fun saveReplay(replay: Replay) { 23 | saveReplayCompletable(replay).await() 24 | } 25 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/storage/FileReplayStorageSystem.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.storage 2 | 3 | import io.github.openminigameserver.replay.io.ReplayFile 4 | import io.github.openminigameserver.replay.model.Replay 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | import java.io.File 8 | import java.util.* 9 | 10 | class FileReplayStorageSystem(dataFolder: File) : ReplayStorageSystem { 11 | 12 | private val replaysFolder = File(dataFolder, "replays").also { it.mkdirs() } 13 | 14 | override suspend fun getReplaysForPlayer(player: UUID): List { 15 | return replaysFolder.listFiles() 16 | ?.mapNotNull { kotlin.runCatching { UUID.fromString(it.nameWithoutExtension) }.getOrNull() } ?: emptyList() 17 | } 18 | 19 | override suspend fun loadReplay(uuid: UUID): Replay? { 20 | return File(replaysFolder, "$uuid.$replayExtension").takeIf { it.exists() }?.let { 21 | withContext(Dispatchers.IO) { 22 | ReplayFile(it).let { it.loadReplay(); it.replay } 23 | } 24 | } 25 | } 26 | 27 | override suspend fun saveReplay(replay: Replay) { 28 | val targetFile = File(replaysFolder, "${replay.id}.$replayExtension") 29 | 30 | withContext(Dispatchers.IO) { 31 | ReplayFile(targetFile, replay).also { it.saveReplay() } 32 | } 33 | 34 | 35 | } 36 | 37 | companion object { 38 | const val replayExtension = "osmrp" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/storage/MemoryReplayStorageSystem.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.storage 2 | 3 | import io.github.openminigameserver.replay.model.Replay 4 | import java.util.* 5 | 6 | object MemoryReplayStorageSystem : ReplayStorageSystem { 7 | private val replays = mutableMapOf() 8 | 9 | override suspend fun getReplaysForPlayer(player: UUID): List { 10 | return replays.keys.toList() 11 | } 12 | 13 | override suspend fun loadReplay(uuid: UUID): Replay? { 14 | return replays[uuid] 15 | } 16 | 17 | override suspend fun saveReplay(replay: Replay) { 18 | replays[replay.id] = replay 19 | } 20 | } -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/storage/ReplayStorageSystem.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.storage 2 | 3 | import io.github.openminigameserver.replay.model.Replay 4 | import java.util.* 5 | 6 | interface ReplayStorageSystem { 7 | 8 | suspend fun getReplaysForPlayer(player: UUID): List 9 | 10 | suspend fun loadReplay(uuid: UUID): Replay? 11 | 12 | suspend fun saveReplay(replay: Replay) 13 | 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /model/src/main/kotlin/io/github/openminigameserver/replay/model/storage/ReplayStorageSystemUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.openminigameserver.replay.model.storage 2 | 3 | import io.github.openminigameserver.replay.model.Replay 4 | import kotlinx.coroutines.runBlocking 5 | import java.util.* 6 | import java.util.concurrent.CompletableFuture 7 | 8 | object ReplayStorageSystemUtils { 9 | @JvmStatic 10 | fun ReplayStorageSystem.getReplaysForPlayerAsCompletable(player: UUID): CompletableFuture> { 11 | return CompletableFuture.supplyAsync { 12 | return@supplyAsync runBlocking { getReplaysForPlayer(player) } 13 | } 14 | } 15 | 16 | @JvmStatic 17 | fun ReplayStorageSystem.loadReplayAsCompletable(uuid: UUID): CompletableFuture { 18 | return CompletableFuture.supplyAsync { 19 | return@supplyAsync runBlocking { loadReplay(uuid) } 20 | } 21 | } 22 | 23 | @JvmStatic 24 | fun ReplayStorageSystem.saveReplayAsCompletable(replay: Replay): CompletableFuture { 25 | return CompletableFuture.runAsync { 26 | return@runAsync runBlocking { saveReplay(replay) } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Replay" 2 | include("model") 3 | include("impl-minestom") 4 | include("impl-abstraction") 5 | --------------------------------------------------------------------------------