├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ └── io │ └── colyseus │ ├── Client.kt │ ├── Connection.kt │ ├── Protocol.kt │ ├── Room.kt │ ├── annotations │ └── SchemaField.kt │ ├── async │ └── ColyseusAsync.kt │ ├── example │ ├── java │ │ ├── Main.java │ │ └── classes │ │ │ ├── Cell.java │ │ │ ├── MyState.java │ │ │ ├── Player.java │ │ │ └── PrimitivesTest.java │ └── kotlin │ │ ├── Main1.kt │ │ ├── Main2.kt │ │ └── MyState.kt │ ├── serializer │ ├── SchemaSerializer.kt │ └── schema │ │ ├── Context.kt │ │ ├── Decoder.kt │ │ ├── Encoder.kt │ │ ├── ReferenceTracker.kt │ │ ├── Schema.kt │ │ └── types │ │ ├── ArraySchema.kt │ │ ├── MapSchema.kt │ │ └── Reflection.kt │ └── util │ ├── Http.kt │ ├── Util.kt │ └── callbacks │ ├── Function0Void.java │ ├── Function1Void.java │ └── Function2Void.java └── test └── kotlin └── io └── colyseus └── test ├── mapper ├── SerializeDeserializeTest.kt └── types │ └── MyState.kt └── schema ├── ArraySchemaTest.kt ├── SchemaDeserializerTest.kt ├── array_schema_types ├── ArraySchemaTypes.kt └── IAmAChild.kt ├── backwards_forwards ├── PlayerV1.kt ├── PlayerV2.kt ├── StateV1.kt └── StateV2.kt ├── child_schema_types ├── ChildSchemaTypes.kt └── IAmAChild.kt ├── filtered_types ├── Player.kt └── State.kt ├── inherited_types ├── Bot.kt ├── Entity.kt ├── InheritedTypes.kt └── Player.kt ├── instance_sharing_types ├── Player.kt ├── Position.kt └── State.kt ├── map_schema_int8 └── MapSchemaInt8.kt ├── map_schema_move_nullify_type └── State.kt ├── map_schema_types ├── IAmAChild.kt └── MapSchemaTypes.kt └── primitive_types └── PrimitiveTypes.kt /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .idea 3 | .DS_Store 4 | .idea/shelf 5 | /confluence/target 6 | /dependencies/repo 7 | /android.tests.dependencies 8 | /dependencies/android.tests.dependencies 9 | /dist 10 | /local 11 | /gh-pages 12 | /ideaSDK 13 | /clionSDK 14 | /android-studio/sdk 15 | out/ 16 | /tmp 17 | workspace.xml 18 | *.versionsBackup 19 | /idea/testData/debugger/tinyApp/classes* 20 | /jps-plugin/testData/kannotator 21 | /js/js.translator/testData/out/ 22 | /js/js.translator/testData/out-min/ 23 | /js/js.translator/testData/out-pir/ 24 | .gradle/ 25 | !**/src/**/build 26 | !**/test/**/build 27 | *.iml 28 | !**/testData/**/*.iml 29 | .idea/libraries/Gradle*.xml 30 | .idea/libraries/Maven*.xml 31 | .idea/artifacts/PILL_*.xml 32 | .idea/artifacts/KotlinPlugin.xml 33 | .idea/modules 34 | .idea/runConfigurations/JPS_*.xml 35 | .idea/runConfigurations/PILL_*.xml 36 | .idea/libraries 37 | .idea/modules.xml 38 | .idea/gradle.xml 39 | .idea/compiler.xml 40 | .idea/inspectionProfiles/profiles_settings.xml 41 | .idea/.name 42 | .idea/artifacts/dist_auto_* 43 | .idea/artifacts/dist.xml 44 | .idea/artifacts/ideaPlugin.xml 45 | .idea/artifacts/kotlinc.xml 46 | .idea/artifacts/kotlin_compiler_jar.xml 47 | .idea/artifacts/kotlin_plugin_jar.xml 48 | .idea/artifacts/kotlin_jps_plugin_jar.xml 49 | .idea/artifacts/kotlin_daemon_client_jar.xml 50 | .idea/artifacts/kotlin_imports_dumper_compiler_plugin_jar.xml 51 | .idea/artifacts/kotlin_main_kts_jar.xml 52 | .idea/artifacts/kotlin_compiler_client_embeddable_jar.xml 53 | .idea/artifacts/kotlin_reflect_jar.xml 54 | .idea/artifacts/kotlin_stdlib_js_ir_* 55 | .idea/artifacts/kotlin_test_js_ir_* 56 | .idea/artifacts/kotlin_stdlib_wasm_* 57 | .idea/jarRepositories.xml 58 | kotlin-ultimate/ 59 | node_modules/ 60 | .rpt2_cache/ 61 | libraries/tools/kotlin-test-js-runner/lib/ 62 | local.properties 63 | *.lst -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License: 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Colyseus-Kotlin 2 | 3 | Implementation of Colyseus client using Kotlin 4 | 5 | ## Download 6 | 7 | ```groovy 8 | repositories { 9 | maven { url 'https://jitpack.io' } 10 | } 11 | 12 | dependencies { 13 | implementation 'com.github.doorbash:colyseus-kotlin:0.14.5' 14 | } 15 | ``` 16 | 17 | ## How to 18 | 19 | ### Define schema class 20 | 21 | ```Kotlin 22 | class MyState : Schema() { 23 | @SchemaField("0/ref", PrimitivesTest::class) 24 | var primitives = PrimitivesTest() 25 | 26 | @SchemaField("1/array/ref", Player::class) 27 | var players = ArraySchema(Player::class.java) 28 | 29 | @SchemaField("2/map/ref", Cell::class) 30 | var cells = MapSchema(Cell::class.java) 31 | } 32 | 33 | class Cell : Schema() { 34 | @SchemaField("0/float32") 35 | var x = Float.default 36 | 37 | @SchemaField("1/float32") 38 | var y = Float.default 39 | } 40 | 41 | class PrimitivesTest : Schema() { 42 | @SchemaField("0/uint8") 43 | var _uint8 = Short.default 44 | 45 | @SchemaField("1/uint16") 46 | var _uint16 = Int.default 47 | 48 | @SchemaField("2/uint32") 49 | var _uint32 = Long.default 50 | 51 | @SchemaField("3/uint64") 52 | var _uint64 = Long.default 53 | 54 | @SchemaField("4/int8") 55 | var _int8 = Byte.default 56 | 57 | @SchemaField("5/int16") 58 | var _int16 = Short.default 59 | 60 | @SchemaField("6/int32") 61 | var _int32 = Int.default 62 | 63 | @SchemaField("7/int64") 64 | var _int64 = Long.default 65 | 66 | @SchemaField("8/float32") 67 | var _float32_n = Float.default 68 | 69 | @SchemaField("9/float32") 70 | var _float32_p = Float.default 71 | 72 | @SchemaField("10/float64") 73 | var _float64_n = Double.default 74 | 75 | @SchemaField("11/float64") 76 | var _float64_p = Double.default 77 | 78 | @SchemaField("12/boolean") 79 | var _boolean = Boolean.default 80 | 81 | @SchemaField("13/string") 82 | var _string = String.default 83 | } 84 | 85 | class Player : Schema() { 86 | @SchemaField("0/int32") 87 | var x = Int.default 88 | } 89 | ``` 90 | 91 | ### Join or create a room: 92 | 93 | ```Kotlin 94 | val client = Client("ws://localhost:2567") 95 | with(client.joinOrCreate(MyState::class.java, "game")) { 96 | println("connected to $name") 97 | 98 | // setup listeners 99 | } 100 | ``` 101 | 102 | ### Setup listeners 103 | 104 | ```Kotlin 105 | /* Room listeners */ 106 | onLeave = { code -> println("onLeave $code") } 107 | 108 | onError = { code, message -> 109 | println("onError $code $message") 110 | } 111 | 112 | onStateChange = { state, isFirstState -> 113 | println("OnStateChange") 114 | println(isFirstState) 115 | } 116 | 117 | /* Schema listeners */ 118 | state.primitives.onChange = { changes -> 119 | for (change in changes) { 120 | with(change!!) { 121 | println("$field: $previousValue -> $value") 122 | } 123 | } 124 | } 125 | 126 | state.players.onAdd = { player, key -> 127 | println("player added: " + key + " " + player.x) 128 | } 129 | 130 | state.players.onRemove = { player, key -> 131 | println("player removed: " + key + " " + player.x) 132 | } 133 | 134 | /* Message Listeners */ 135 | //Set listener by message type 136 | onMessage("hi") { cells: MapSchema -> 137 | println("cells size is ${cells.size}") 138 | println(cells) 139 | } 140 | 141 | // Set listener by type 142 | onMessage { primitives: PrimitivesTest -> 143 | println("some primitives...") 144 | println(primitives._string) 145 | } 146 | 147 | // Set listener by type id 148 | onMessage(2) { cell: Cell -> 149 | println("handler for type 2") 150 | println(cell.x) 151 | } 152 | ``` 153 | 154 | ### Send message to server: 155 | 156 | ```Kotlin 157 | // Send message with message type 158 | send("fire", Math.random() * 100) 159 | 160 | // Send a schema with type id 161 | send(2, Cell().apply { x = 100f; y = 200f }) 162 | 163 | // Send only the message type or type id 164 | send("hello") 165 | send(3) 166 | ``` 167 | 168 | ## Usage examples 169 | 170 | - [colyseus-kotlin-example](https://github.com/doorbash/colyseus-kotlin-example) 171 | - [colyseus-kotlin-java-example](https://github.com/doorbash/colyseus-kotlin-java-example) 172 | - [colyseus-android-chat](https://github.com/doorbash/colyseus-android-chat) - A simple chat Android application 173 | - [agar-io](https://github.com/doorbash/agar-io) - A simple agar.io clone made with Libgdx 174 | 175 | ## License 176 | 177 | MIT 178 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32" 8 | } 9 | } 10 | 11 | plugins { 12 | id 'java' 13 | } 14 | 15 | apply plugin: 'kotlin' 16 | 17 | repositories { 18 | mavenCentral() 19 | } 20 | 21 | dependencies { 22 | testImplementation "org.junit.jupiter:junit-jupiter:5.7.1" 23 | 24 | implementation "org.java-websocket:Java-WebSocket:1.5.1" 25 | implementation "org.msgpack:jackson-dataformat-msgpack:0.8.22" 26 | apiElements "org.jetbrains.kotlin:kotlin-stdlib:1.4.32" 27 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3" 28 | } 29 | 30 | task runExampleJava(dependsOn: classes, type: JavaExec) { 31 | main = "io.colyseus.example.java.Main" 32 | classpath = sourceSets.main.runtimeClasspath 33 | standardInput = System.in 34 | workingDir = new File(".") 35 | } 36 | 37 | task runExampleKotlin1(dependsOn: classes, type: JavaExec) { 38 | main = "io.colyseus.example.kotlin.Main1Kt" 39 | classpath = sourceSets.main.runtimeClasspath 40 | standardInput = System.in 41 | workingDir = new File(".") 42 | } 43 | 44 | task runExampleKotlin2(dependsOn: classes, type: JavaExec) { 45 | main = "io.colyseus.example.kotlin.Main2Kt" 46 | classpath = sourceSets.main.runtimeClasspath 47 | standardInput = System.in 48 | workingDir = new File(".") 49 | } 50 | 51 | compileJava { 52 | sourceCompatibility = 1.8 53 | } 54 | compileKotlin { 55 | kotlinOptions { 56 | jvmTarget = "1.8" 57 | } 58 | } 59 | compileTestKotlin { 60 | kotlinOptions { 61 | jvmTarget = "1.8" 62 | } 63 | } 64 | tasks.test { 65 | useJUnitPlatform() 66 | testLogging { 67 | events "passed", "skipped", "failed" 68 | } 69 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorbash/colyseus-kotlin/4121335961fa79017235f898405cfc1522606129/gradle.properties -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/doorbash/colyseus-kotlin/4121335961fa79017235f898405cfc1522606129/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Apr 04 01:21:05 IRDT 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'colyseus-kotlin' -------------------------------------------------------------------------------- /src/main/java/io/colyseus/Client.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import com.fasterxml.jackson.databind.ObjectMapper 5 | import io.colyseus.async.ColyseusAsync 6 | import io.colyseus.serializer.schema.Schema 7 | import io.colyseus.util.Http.request 8 | import io.colyseus.util.callbacks.Function1Void 9 | import kotlinx.coroutines.launch 10 | import kotlinx.coroutines.withContext 11 | import java.io.UnsupportedEncodingException 12 | import java.net.URLEncoder 13 | import java.util.* 14 | import kotlin.coroutines.Continuation 15 | import kotlin.coroutines.resume 16 | import kotlin.coroutines.resumeWithException 17 | import kotlin.coroutines.suspendCoroutine 18 | 19 | class Client(private val endpoint: String) { 20 | 21 | private val objectMapper = ObjectMapper() 22 | 23 | data class AvailableRoom( 24 | var roomId: String? = null, 25 | var clients: Int = 0, 26 | var maxClients: Int = 0, 27 | var metadata: Any? = null, 28 | ) 29 | 30 | public suspend fun joinOrCreate( 31 | schema: Class, 32 | roomName: String, 33 | options: LinkedHashMap? = null, 34 | httpHeaders: MutableMap? = null, 35 | wsHeaders: Map? = null, 36 | ): Room { 37 | return withContext(ColyseusAsync.coroutineContext) { 38 | createMatchMakeRequest( 39 | schema, 40 | "joinOrCreate", 41 | roomName, 42 | options, 43 | httpHeaders, 44 | wsHeaders, 45 | ) 46 | } 47 | } 48 | 49 | @JvmOverloads 50 | public fun joinOrCreate( 51 | schema: Class, 52 | roomName: String, 53 | options: LinkedHashMap? = null, 54 | httpHeaders: MutableMap? = null, 55 | wsHeaders: Map? = null, 56 | callback: Function1Void>, 57 | onError: Function1Void, 58 | ) { 59 | ColyseusAsync.launch { 60 | try { 61 | callback(createMatchMakeRequest( 62 | schema, 63 | "joinOrCreate", 64 | roomName, 65 | options, 66 | httpHeaders, 67 | wsHeaders, 68 | )) 69 | } catch (e: Exception) { 70 | onError.invoke(e) 71 | } 72 | } 73 | } 74 | 75 | public suspend fun create( 76 | schema: Class, 77 | roomName: String, 78 | options: LinkedHashMap? = null, 79 | httpHeaders: MutableMap? = null, 80 | wsHeaders: Map? = null, 81 | ): Room { 82 | return withContext(ColyseusAsync.coroutineContext) { 83 | createMatchMakeRequest( 84 | schema, 85 | "create", 86 | roomName, 87 | options, 88 | httpHeaders, 89 | wsHeaders, 90 | ) 91 | } 92 | } 93 | 94 | @JvmOverloads 95 | public fun create( 96 | schema: Class, 97 | roomName: String, 98 | options: LinkedHashMap? = null, 99 | httpHeaders: MutableMap? = null, 100 | wsHeaders: Map? = null, 101 | callback: Function1Void>, 102 | onError: Function1Void, 103 | ) { 104 | ColyseusAsync.launch { 105 | try { 106 | callback(createMatchMakeRequest( 107 | schema, 108 | "create", 109 | roomName, 110 | options, 111 | httpHeaders, 112 | wsHeaders, 113 | )) 114 | } catch (e: Exception) { 115 | onError.invoke(e) 116 | } 117 | } 118 | } 119 | 120 | public suspend fun join( 121 | schema: Class, 122 | roomName: String, 123 | options: LinkedHashMap? = null, 124 | httpHeaders: MutableMap? = null, 125 | wsHeaders: Map? = null, 126 | ): Room { 127 | return withContext(ColyseusAsync.coroutineContext) { 128 | createMatchMakeRequest( 129 | schema, 130 | "join", 131 | roomName, 132 | options, 133 | httpHeaders, 134 | wsHeaders, 135 | ) 136 | } 137 | } 138 | 139 | @JvmOverloads 140 | public fun join( 141 | schema: Class, 142 | roomName: String, 143 | options: LinkedHashMap? = null, 144 | httpHeaders: MutableMap? = null, 145 | wsHeaders: Map? = null, 146 | callback: Function1Void>, 147 | onError: Function1Void, 148 | ) { 149 | ColyseusAsync.launch { 150 | try { 151 | callback(createMatchMakeRequest( 152 | schema, 153 | "join", 154 | roomName, 155 | options, 156 | httpHeaders, 157 | wsHeaders, 158 | )) 159 | } catch (e: Exception) { 160 | onError.invoke(e) 161 | } 162 | } 163 | } 164 | 165 | public suspend fun joinById( 166 | schema: Class, 167 | roomId: String, 168 | options: LinkedHashMap? = null, 169 | httpHeaders: MutableMap? = null, 170 | wsHeaders: Map? = null, 171 | ): Room { 172 | return withContext(ColyseusAsync.coroutineContext) { 173 | createMatchMakeRequest( 174 | schema, 175 | "joinById", 176 | roomId, 177 | options, 178 | httpHeaders, 179 | wsHeaders, 180 | ) 181 | } 182 | } 183 | 184 | @JvmOverloads 185 | public fun joinById( 186 | schema: Class, 187 | roomId: String, 188 | options: LinkedHashMap? = null, 189 | httpHeaders: MutableMap? = null, 190 | wsHeaders: Map? = null, 191 | callback: Function1Void>, 192 | onError: Function1Void, 193 | ) { 194 | ColyseusAsync.launch { 195 | try { 196 | callback(createMatchMakeRequest( 197 | schema, 198 | "joinById", 199 | roomId, 200 | options, 201 | httpHeaders, 202 | wsHeaders, 203 | )) 204 | } catch (e: Exception) { 205 | onError.invoke(e) 206 | } 207 | } 208 | } 209 | 210 | public suspend fun reconnect( 211 | schema: Class, 212 | roomId: String, 213 | sessionId: String, 214 | httpHeaders: MutableMap? = null, 215 | wsHeaders: Map? = null, 216 | ): Room { 217 | val options = LinkedHashMap() 218 | options["sessionId"] = sessionId 219 | return withContext(ColyseusAsync.coroutineContext) { 220 | createMatchMakeRequest( 221 | schema, 222 | "joinById", 223 | roomId, 224 | options, 225 | httpHeaders, 226 | wsHeaders 227 | ) 228 | } 229 | } 230 | 231 | @JvmOverloads 232 | public fun reconnect( 233 | schema: Class, 234 | roomId: String, 235 | sessionId: String, 236 | httpHeaders: MutableMap? = null, 237 | wsHeaders: Map? = null, 238 | callback: Function1Void>, 239 | onError: Function1Void, 240 | ) { 241 | ColyseusAsync.launch { 242 | try { 243 | val options = LinkedHashMap() 244 | options["sessionId"] = sessionId 245 | callback(createMatchMakeRequest( 246 | schema, 247 | "joinById", 248 | roomId, 249 | options, 250 | httpHeaders, 251 | wsHeaders 252 | )) 253 | } catch (e: Exception) { 254 | onError.invoke(e) 255 | } 256 | } 257 | } 258 | 259 | suspend fun getAvailableRooms(roomName: String): List { 260 | return withContext(ColyseusAsync.coroutineContext) { 261 | getAvailableRoomsSync(roomName) 262 | } 263 | } 264 | 265 | fun getAvailableRooms(roomName: String, callback: Function1Void>) { 266 | ColyseusAsync.launch { 267 | callback(getAvailableRoomsSync(roomName)) 268 | } 269 | } 270 | 271 | private fun getAvailableRoomsSync(roomName: String): List { 272 | val url = endpoint.replace("ws", "http") + "/matchmake/" + roomName 273 | val httpHeaders = LinkedHashMap() 274 | httpHeaders["Accept"] = "application/json" 275 | val response: String = request(url = url, method = "GET", httpHeaders = httpHeaders) 276 | val data: List = 277 | objectMapper.readValue(response, ArrayList::class.java) as List 278 | return data 279 | } 280 | 281 | private suspend fun createMatchMakeRequest( 282 | schema: Class, 283 | method: String, 284 | roomName: String, 285 | options: LinkedHashMap? = null, 286 | httpHeaders: MutableMap? = null, 287 | wsHeaders: Map? = null, 288 | ): Room { 289 | return suspendCoroutine { cont: Continuation> -> 290 | var headers: MutableMap? = httpHeaders 291 | try { 292 | val url = endpoint.replace("ws", "http") + 293 | "/matchmake/" + 294 | method + 295 | "/" + 296 | URLEncoder.encode(roomName, "UTF-8") 297 | val body = if (options != null) { 298 | objectMapper.writeValueAsString( 299 | objectMapper.convertValue(options, JsonNode::class.java) 300 | ) 301 | } else "{}" 302 | if (headers == null) headers = LinkedHashMap() 303 | headers["Accept"] = "application/json" 304 | headers["Content-Type"] = "application/json" 305 | val res = request(url = url, method = "POST", httpHeaders = headers, body = body) 306 | val response = objectMapper.readValue(res, JsonNode::class.java) 307 | if (response.has("error")) { 308 | throw MatchMakeException(response["error"].asText(), response["code"].asInt()) 309 | } 310 | val room = Room(schema, roomName) 311 | val roomId = response["room"]["roomId"].asText() 312 | room.id = roomId 313 | val sessionId = response["sessionId"].asText() 314 | room.sessionId = sessionId 315 | room.onError = { code, message -> 316 | cont.resumeWithException(Exception(message)) 317 | } 318 | room.onJoin = { 319 | room.onError = null 320 | room.onJoin = null 321 | cont.resume(room) 322 | } 323 | val wsOptions = LinkedHashMap() 324 | wsOptions["sessionId"] = room.sessionId 325 | val wsUrl = buildEndpoint(response["room"], wsOptions) 326 | // System.out.println("ws url is " + wsUrl) 327 | room.connect(wsUrl, wsHeaders) 328 | } catch (e: Exception) { 329 | cont.resumeWithException(e) 330 | } 331 | } 332 | } 333 | 334 | class MatchMakeException(message: String?, var code: Int) : Exception(message) { 335 | companion object { 336 | // MatchMaking Error Codes 337 | const val ERR_MATCHMAKE_NO_HANDLER = 4210 338 | const val ERR_MATCHMAKE_INVALID_CRITERIA = 4211 339 | const val ERR_MATCHMAKE_INVALID_ROOM_ID = 4212 340 | const val ERR_MATCHMAKE_UNHANDLED = 4213 // generic exception during onCreate/onJoin 341 | const val ERR_MATCHMAKE_EXPIRED = 4214 // generic exception during onCreate/onJoin 342 | } 343 | } 344 | 345 | @Throws(UnsupportedEncodingException::class) 346 | private fun buildEndpoint(room: JsonNode, options: LinkedHashMap): String { 347 | val charset = "UTF-8" 348 | val params = StringBuilder() 349 | for ((i, name) in options.keys.withIndex()) { 350 | if (i > 0) params.append("&") 351 | params.append(URLEncoder.encode(name, charset)).append("=").append(URLEncoder.encode(options[name].toString(), charset)) 352 | } 353 | return "$endpoint/${room["processId"].asText()}/${room["roomId"].asText()}?$params" 354 | } 355 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/Connection.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import org.java_websocket.client.WebSocketClient 5 | import org.java_websocket.drafts.Draft_6455 6 | import org.java_websocket.handshake.ServerHandshake 7 | import org.msgpack.jackson.dataformat.MessagePackFactory 8 | import java.net.URI 9 | import java.nio.ByteBuffer 10 | import java.util.* 11 | 12 | private const val CONNECT_TIMEOUT = 10000 13 | 14 | class Connection internal constructor( 15 | uri: URI, 16 | httpHeaders: Map? = null, 17 | ) : WebSocketClient(uri, Draft_6455(), httpHeaders, CONNECT_TIMEOUT) { 18 | 19 | var onError: ((e: Exception) -> Unit)? = null 20 | var onClose: ((code: Int, reason: String?, remote: Boolean) -> Unit)? = null 21 | var onOpen: (() -> Unit)? = null 22 | var onMessage: ((bytes: ByteBuffer) -> Unit)? = null 23 | 24 | private val _enqueuedCalls = LinkedList() 25 | private val msgpackMapper: ObjectMapper = ObjectMapper(MessagePackFactory())/*.apply { 26 | setConfig(this.deserializationConfig.withFeatures(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)) 27 | }*/ 28 | 29 | fun _send(data: Any) { 30 | if (isOpen) { 31 | try { 32 | val d = msgpackMapper.writeValueAsBytes(data) 33 | // println("sending... " + Arrays.toString(d)) 34 | send(d) 35 | } catch (e: Exception) { 36 | onError(e) 37 | } 38 | } else { 39 | // WebSocket not connected. 40 | // Enqueue data to be sent when readyState == OPEN 41 | _enqueuedCalls.push(data) 42 | } 43 | } 44 | 45 | override fun onOpen(handshakedata: ServerHandshake) { 46 | if (!_enqueuedCalls.isEmpty()) { 47 | _enqueuedCalls.forEach(this::_send) 48 | _enqueuedCalls.clear() 49 | } 50 | onOpen?.invoke() 51 | } 52 | 53 | override fun onMessage(message: String) { 54 | } 55 | 56 | override fun onClose(code: Int, reason: String, remote: Boolean) { 57 | onClose?.invoke(code, reason, remote) 58 | } 59 | 60 | override fun onError(ex: Exception) { 61 | onError?.invoke(ex) 62 | } 63 | 64 | override fun onMessage(buf: ByteBuffer) { 65 | // println("received: $buf") 66 | onMessage?.invoke(buf) 67 | } 68 | 69 | override fun send(data: ByteArray?) { 70 | // println("sending... " + Arrays.toString(data)) 71 | try { 72 | super.send(data) 73 | } catch (e: Exception) { 74 | onError?.invoke(e) 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/Protocol.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus 2 | 3 | object Protocol { 4 | const val USER_ID = 1 5 | const val JOIN_REQUEST = 9 6 | const val JOIN_ROOM = 10 7 | const val ERROR = 11 8 | const val LEAVE_ROOM = 12 9 | const val ROOM_DATA = 13 10 | const val ROOM_STATE = 14 11 | const val ROOM_STATE_PATCH = 15 12 | const val ROOM_DATA_SCHEMA = 16 13 | const val ROOM_LIST = 20 14 | const val BAD_REQUEST = 50 15 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/Room.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import io.colyseus.async.ColyseusAsync 5 | import io.colyseus.serializer.SchemaSerializer 6 | import io.colyseus.serializer.schema.* 7 | import io.colyseus.serializer.schema.Iterator 8 | import io.colyseus.util.byteArrayOfInts 9 | import io.colyseus.util.callbacks.Function0Void 10 | import io.colyseus.util.callbacks.Function1Void 11 | import io.colyseus.util.callbacks.Function2Void 12 | import io.colyseus.util.get 13 | import kotlinx.coroutines.launch 14 | import org.java_websocket.framing.CloseFrame 15 | import org.msgpack.jackson.dataformat.MessagePackFactory 16 | import java.net.URI 17 | import java.net.URISyntaxException 18 | import java.nio.ByteBuffer 19 | 20 | class Room internal constructor(schema: Class, var name: String) { 21 | 22 | public var onLeave: ((code: Int) -> Unit)? = null 23 | public var onError: ((code: Int, message: String?) -> Unit)? = null 24 | public var onJoin: (() -> Unit)? = null 25 | public var onStateChange: ((state: T, isFirstState: Boolean) -> Unit)? = null 26 | public val onMessageHandlers = hashMapOf>() 27 | 28 | public fun setOnLeave(f: Function1Void) { 29 | onLeave = f::invoke 30 | } 31 | 32 | public fun setOnError(f: Function2Void) { 33 | onError = f::invoke 34 | } 35 | 36 | public fun setOnJoin(f: Function0Void) { 37 | onJoin = f::invoke 38 | } 39 | 40 | public fun setOnStateChange(f: Function2Void) { 41 | onStateChange = f::invoke 42 | } 43 | 44 | class MessageHandler constructor( 45 | val type: Class, 46 | val handler: ((message: K) -> Unit)?, 47 | ) 48 | 49 | var id: String? = null 50 | var sessionId: String? = null 51 | private var connection: Connection? = null 52 | 53 | private val msgpackMapper = ObjectMapper(MessagePackFactory()) 54 | private var serializer = SchemaSerializer(schema) 55 | public val state: T = serializer.state as T 56 | 57 | @Throws(URISyntaxException::class) 58 | fun connect(endpoint: String, httpHeaders: Map? = null) { 59 | // System.out.println("Room is connecting to " + endpoint) 60 | connection = Connection(URI(endpoint), httpHeaders) 61 | connection?.onError = { e -> onError?.invoke(-1, e.message) } 62 | connection?.onClose = { code, reason, _ -> 63 | if (code == CloseFrame.PROTOCOL_ERROR && 64 | reason != null && 65 | reason.startsWith("Invalid status code received: 401")) { 66 | onError?.invoke(-1, reason) 67 | } 68 | serializer.teardown() 69 | onLeave?.invoke(code) 70 | } 71 | connection?.onMessage = { buf: ByteBuffer -> 72 | val length = buf.remaining() 73 | val bytes = ByteArray(length) 74 | buf[bytes, 0, length] 75 | this.parseMessage(bytes) 76 | } 77 | connection?.connect() 78 | } 79 | 80 | private fun parseMessage(bytes: ByteArray) { 81 | try { 82 | val code = bytes[0].toInt() and 0xFF 83 | when (code) { 84 | Protocol.JOIN_ROOM -> { 85 | var offset = 1 86 | val serializerId = String(bytes, offset + 1, bytes[offset].toInt() and 0xFF) 87 | offset += serializerId.length + 1 88 | if (serializerId == "fossil-delta") { 89 | throw Error("fossil-delta is not supported") 90 | } 91 | if (bytes.size > offset) { 92 | serializer.handshake(bytes, offset) 93 | } 94 | onJoin?.invoke() 95 | connection?._send(Protocol.JOIN_ROOM) 96 | } 97 | Protocol.ERROR -> { 98 | val it = Iterator(1) 99 | val errorCode: Int = Decoder.decodeNumber(bytes, it).toInt() 100 | val errorMessage = Decoder.decodeString(bytes, it) 101 | onError?.invoke(errorCode, errorMessage) 102 | } 103 | Protocol.ROOM_DATA_SCHEMA -> { 104 | try { 105 | val it = Iterator(1) 106 | val typeId = Decoder.decodeNumber(bytes, it) 107 | val messageType: Class<*> = Context.instance[typeId.toInt()] as Class<*> 108 | val message = messageType.getConstructor().newInstance() as Schema 109 | message.decode(bytes, it) 110 | val messageHandler = onMessageHandlers["s" + messageType.name] as MessageHandler? 111 | if (messageHandler != null) { 112 | messageHandler.handler?.invoke(message) 113 | } else { 114 | println("No handler for type " + messageType.name) 115 | } 116 | } catch (e: Exception) { 117 | onError?.invoke(-1, e.message) 118 | } 119 | } 120 | Protocol.LEAVE_ROOM -> leave() 121 | Protocol.ROOM_STATE -> setState(bytes, 1) 122 | Protocol.ROOM_STATE_PATCH -> patch(bytes, 1) 123 | Protocol.ROOM_DATA -> { 124 | val messageHandler: MessageHandler<*>? 125 | val type: Any 126 | val it = Iterator(1) 127 | if (Decoder.numberCheck(bytes, it)) { 128 | type = Decoder.decodeNumber(bytes, it).toInt() 129 | messageHandler = onMessageHandlers["i$type"] as MessageHandler? 130 | } else { 131 | type = Decoder.decodeString(bytes, it) 132 | messageHandler = onMessageHandlers[type.toString()] as MessageHandler? 133 | } 134 | if (messageHandler != null) { 135 | if (bytes.size > it.offset) { 136 | messageHandler.handler?.invoke( 137 | msgpackMapper.readValue(bytes[it.offset..bytes.size], messageHandler.type)!! 138 | ) 139 | } 140 | } else { 141 | println("No handler for type $type" + if (type is Int) " (${Context.instance.get(type)?.simpleName})" else "") 142 | } 143 | } 144 | } 145 | } catch (e: Exception) { 146 | leave(false) 147 | e.printStackTrace() 148 | onError?.invoke(-1, e.message) 149 | } 150 | } 151 | 152 | // Disconnect from the room. 153 | @JvmOverloads 154 | fun leave(consented: Boolean = true) { 155 | serializer.teardown() 156 | if (connection != null) { 157 | if (consented) { 158 | connection!!._send(Protocol.LEAVE_ROOM) 159 | } else { 160 | connection!!.close() 161 | } 162 | } else { 163 | onLeave?.invoke(4000) // "consented" code 164 | } 165 | } 166 | 167 | // Send a message by number type, without payload 168 | public fun send(type: Int) { 169 | ColyseusAsync.launch { 170 | sendSync(type) 171 | } 172 | } 173 | 174 | public fun sendSync(type: Int) { 175 | connection?.send(byteArrayOfInts(Protocol.ROOM_DATA, type)) 176 | } 177 | 178 | // Send a message by number type with payload 179 | public fun send(type: Int, message: Any) { 180 | ColyseusAsync.launch { 181 | sendSync(type, message) 182 | } 183 | } 184 | 185 | public fun sendSync(type: Int, message: Any) { 186 | val initialBytes: ByteArray = byteArrayOfInts(Protocol.ROOM_DATA, type) 187 | val encodedMessage: ByteArray = msgpackMapper.writeValueAsBytes(message) 188 | connection?.send(initialBytes + encodedMessage) 189 | } 190 | 191 | // Send a message by string type, without payload 192 | public fun send(type: String) { 193 | ColyseusAsync.launch { 194 | sendSync(type) 195 | } 196 | } 197 | 198 | public fun sendSync(type: String) { 199 | val encodedType: ByteArray = type.toByteArray() 200 | val initialBytes: ByteArray = Encoder.getInitialBytesFromEncodedType(encodedType) 201 | connection?.send(initialBytes + encodedType) 202 | } 203 | 204 | // Send a message by string type with payload 205 | public fun send(type: String, message: Any) { 206 | ColyseusAsync.launch { 207 | sendSync(type, message) 208 | } 209 | } 210 | 211 | public fun sendSync(type: String, message: Any) { 212 | val encodedMessage: ByteArray = msgpackMapper.writeValueAsBytes(message) 213 | val encodedType: ByteArray = type.toByteArray() 214 | val initialBytes: ByteArray = Encoder.getInitialBytesFromEncodedType(encodedType) 215 | connection?.send(initialBytes + encodedType + encodedMessage) 216 | } 217 | 218 | public inline fun onMessage( 219 | type: String, 220 | noinline handler: ((message: MessageType) -> Unit)?, 221 | ) { 222 | onMessageHandlers[type] = MessageHandler( 223 | MessageType::class.java, 224 | handler 225 | ) 226 | } 227 | 228 | public fun onMessage( 229 | type: String, 230 | clazz: Class, 231 | handler: Function1Void?, 232 | ) { 233 | onMessageHandlers[type] = MessageHandler( 234 | clazz 235 | ) { message -> handler?.invoke(message) } 236 | } 237 | 238 | public inline fun onMessage( 239 | type: Int, 240 | noinline handler: ((message: MessageType) -> Unit)?, 241 | ) { 242 | onMessageHandlers["i$type"] = MessageHandler( 243 | MessageType::class.java, 244 | handler 245 | ) 246 | } 247 | 248 | public fun onMessage( 249 | type: Int, 250 | clazz: Class, 251 | handler: Function1Void?, 252 | ) { 253 | onMessageHandlers["i$type"] = MessageHandler( 254 | clazz 255 | ) { message -> handler?.invoke(message) } 256 | } 257 | 258 | public inline fun onMessage( 259 | noinline handler: ((message: MessageType) -> Unit)?, 260 | ) { 261 | onMessageHandlers["s" + MessageType::class.java.name] = MessageHandler( 262 | MessageType::class.java, 263 | handler 264 | ) 265 | } 266 | 267 | public fun onMessage( 268 | clazz: Class, 269 | handler: Function1Void?, 270 | ) { 271 | onMessageHandlers["s" + clazz.name] = MessageHandler( 272 | clazz 273 | ) { message -> handler?.invoke(message) } 274 | } 275 | 276 | public fun hasJoined(): Boolean { 277 | return sessionId != null 278 | } 279 | 280 | @Throws(Exception::class) 281 | private fun setState(encodedState: ByteArray, offset: Int = 0) { 282 | serializer.setState(encodedState, offset) 283 | onStateChange?.invoke(serializer.state, true) 284 | } 285 | 286 | @Throws(Exception::class) 287 | private fun patch(delta: ByteArray, offset: Int = 0) { 288 | serializer.patch(delta, offset) 289 | onStateChange?.invoke(serializer.state, false) 290 | } 291 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/annotations/SchemaField.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.annotations 2 | 3 | import kotlin.reflect.KClass 4 | 5 | @Retention(AnnotationRetention.RUNTIME) 6 | @Target(AnnotationTarget.FIELD) 7 | annotation class SchemaField(val type: String, val ref: KClass = Any::class) -------------------------------------------------------------------------------- /src/main/java/io/colyseus/async/ColyseusAsync.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.async 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.newSingleThreadContext 5 | 6 | public object ColyseusAsync : CoroutineScope { 7 | override val coroutineContext = newSingleThreadContext("ColyseusClient-Thread") 8 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/example/java/Main.java: -------------------------------------------------------------------------------- 1 | package io.colyseus.example.java; 2 | 3 | import io.colyseus.Client; 4 | import io.colyseus.example.java.classes.Cell; 5 | import io.colyseus.example.java.classes.MyState; 6 | import io.colyseus.example.java.classes.PrimitivesTest; 7 | 8 | import java.util.Scanner; 9 | 10 | public class Main { 11 | public static void main(String... args) { 12 | Client client = new Client("ws://localhost:2567"); 13 | client.joinOrCreate(MyState.class, "game", room -> { 14 | System.out.println("connected to " + room.getName()); 15 | 16 | room.getState().players.setOnAdd((player, integer) -> { 17 | System.out.println("added player with x = " + player.x + " to index = " + integer); 18 | 19 | room.send("fire", "in the hole!"); 20 | }); 21 | 22 | room.setOnLeave(code -> { 23 | System.out.println("onLeave(" + code + ")"); 24 | }); 25 | 26 | room.setOnError((reason, message) -> { 27 | System.out.println("onError(" + reason + ", " + message + ")"); 28 | }); 29 | 30 | room.getState().primitives.setOnChange(changes -> { 31 | changes.forEach(change -> { 32 | System.out.println(change.getField() + ": " + change.getPreviousValue() + " -> " + change.getValue()); 33 | }); 34 | }); 35 | 36 | room.setOnStateChange((state, isFirstState) -> { 37 | System.out.println("onStateChange()"); 38 | System.out.println("state.primitives._boolean = " + state.primitives._boolean); 39 | System.out.println(isFirstState); 40 | }); 41 | 42 | room.onMessage("sup", Cell.class, cell -> { 43 | System.out.println("cell.x = " + cell.x + ", cell.y = " + cell.y); 44 | }); 45 | 46 | room.onMessage(2, Cell.class, cell -> { 47 | System.out.println("cell.x = " + cell.x + ", cell.y = " + cell.y); 48 | }); 49 | 50 | room.onMessage(PrimitivesTest.class, primitives -> { 51 | System.out.println("primitives._boolean = " + primitives._boolean); 52 | }); 53 | 54 | room.onMessage(Cell.class, cell -> { 55 | System.out.println(" ::::::: cell.x = " + cell.x); 56 | }); 57 | 58 | room.onMessage("hello", Float.class, random -> { 59 | System.out.println("random = " + random); 60 | }); 61 | 62 | }, e -> { 63 | e.printStackTrace(); 64 | }); 65 | 66 | new Scanner(System.in).nextLine(); 67 | } 68 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/example/java/classes/Cell.java: -------------------------------------------------------------------------------- 1 | package io.colyseus.example.java.classes; 2 | 3 | import io.colyseus.annotations.SchemaField; 4 | import io.colyseus.serializer.schema.Schema; 5 | 6 | public class Cell extends Schema { 7 | @SchemaField(type = "0/float32") 8 | public float x; 9 | 10 | @SchemaField(type = "1/float32") 11 | public float y; 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/io/colyseus/example/java/classes/MyState.java: -------------------------------------------------------------------------------- 1 | package io.colyseus.example.java.classes; 2 | 3 | import io.colyseus.annotations.SchemaField; 4 | import io.colyseus.serializer.schema.Schema; 5 | import io.colyseus.serializer.schema.types.ArraySchema; 6 | import io.colyseus.serializer.schema.types.MapSchema; 7 | 8 | public class MyState extends Schema { 9 | @SchemaField(type = "0/ref", ref = PrimitivesTest.class) 10 | public PrimitivesTest primitives = new PrimitivesTest(); 11 | 12 | @SchemaField(type = "1/array/ref", ref = Player.class) 13 | public ArraySchema players = new ArraySchema<>(Player.class); 14 | 15 | @SchemaField(type = "2/map/ref", ref = Cell.class) 16 | public MapSchema cells = new MapSchema<>(Cell.class); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/io/colyseus/example/java/classes/Player.java: -------------------------------------------------------------------------------- 1 | package io.colyseus.example.java.classes; 2 | 3 | import io.colyseus.annotations.SchemaField; 4 | import io.colyseus.serializer.schema.Schema; 5 | 6 | public class Player extends Schema { 7 | @SchemaField(type = "0/int32") 8 | public int x; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/io/colyseus/example/java/classes/PrimitivesTest.java: -------------------------------------------------------------------------------- 1 | package io.colyseus.example.java.classes; 2 | 3 | import io.colyseus.annotations.SchemaField; 4 | import io.colyseus.serializer.schema.Schema; 5 | 6 | public class PrimitivesTest extends Schema { 7 | @SchemaField(type = "0/uint8") 8 | public short _uint8; 9 | 10 | @SchemaField(type = "1/uint16") 11 | public int _uint16; 12 | 13 | @SchemaField(type = "2/uint32") 14 | public long _uint32; 15 | 16 | @SchemaField(type = "3/uint64") 17 | public long _uint64; 18 | 19 | @SchemaField(type = "4/int8") 20 | public byte _int8; 21 | 22 | @SchemaField(type = "5/int16") 23 | public short _int16; 24 | 25 | @SchemaField(type = "6/int32") 26 | public int _int32; 27 | 28 | @SchemaField(type = "7/int64") 29 | public long _int64; 30 | 31 | @SchemaField(type = "8/float32") 32 | public float _float32_n; 33 | 34 | @SchemaField(type = "9/float32") 35 | public float _float32_p; 36 | 37 | @SchemaField(type = "10/float64") 38 | public double _float64_n; 39 | 40 | @SchemaField(type = "11/float64") 41 | public double _float64_p; 42 | 43 | @SchemaField(type = "12/boolean") 44 | public boolean _boolean; 45 | 46 | @SchemaField(type = "13/string") 47 | public String _string; 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/io/colyseus/example/kotlin/Main1.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.example.kotlin 2 | 3 | import io.colyseus.Client 4 | import io.colyseus.serializer.schema.types.ArraySchema 5 | import io.colyseus.serializer.schema.types.MapSchema 6 | import kotlinx.coroutines.runBlocking 7 | 8 | fun main() = runBlocking { 9 | val client = Client("ws://localhost:2567") 10 | with(client.joinOrCreate(MyState::class.java, "game")) { 11 | println("connected to $name") 12 | 13 | // state.onChange = { changes -> println(changes) } 14 | state.onRemove = { println("state.onRemove") } 15 | 16 | state.players.onAdd = { player: Player?, key: Int? -> 17 | println("player added: " + key + " " + player?.x) 18 | } 19 | 20 | state.players.onRemove = { player: Player?, key: Int? -> 21 | println("player removed: " + key + " " + player?.x) 22 | 23 | send(2, Cell().apply { x = 100f; y = 200f }) 24 | } 25 | 26 | onLeave = { code -> println("onLeave $code") } 27 | onError = { code, message -> 28 | println("onError") 29 | println(code) 30 | println(message) 31 | } 32 | 33 | onStateChange = { state, isFirstState -> 34 | println("OnStateChange") 35 | println(state.players.size) 36 | println(isFirstState) 37 | } 38 | 39 | // onMessage("xxxxx") { message: Player -> 40 | // println("xxxxx!!! >> " + message.x) 41 | // } 42 | 43 | onMessage { primitives: PrimitivesTest -> 44 | println("some primitives...") 45 | println(primitives._string) 46 | } 47 | 48 | onMessage { cell: Cell -> 49 | println(" ;;;;; cell.x = ${cell.x}") 50 | } 51 | 52 | // onMessage("hello") { data : Float -> 53 | // println(data) 54 | // } 55 | // 56 | onMessage("hi") { cells: MapSchema -> 57 | println("map size is ") 58 | println(cells.size) 59 | println(cells) 60 | } 61 | 62 | onMessage("hey") { players: ArraySchema -> 63 | println("player array size is ") 64 | println(players.size) 65 | println(players) 66 | } 67 | 68 | onMessage("ahoy") { cell: Cell -> 69 | println("""cell.x = ${cell.x} ,cell.y = ${cell.y}""") 70 | } 71 | 72 | onMessage(2) { cell: Cell -> 73 | println("handler for type 2") 74 | println(" >>>>>>>>>>>>>> " + cell.x) 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/example/kotlin/Main2.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.example.kotlin 2 | 3 | import io.colyseus.Client 4 | import io.colyseus.serializer.schema.types.ArraySchema 5 | import io.colyseus.serializer.schema.types.MapSchema 6 | 7 | fun main() { 8 | val client = Client("ws://localhost:2567") 9 | client.joinOrCreate(MyState::class.java, "game", callback = { room -> 10 | with(room) { 11 | println("connected to $name") 12 | 13 | // state.onChange = { changes -> println(changes) } 14 | state.onRemove = { println("state.onRemove") } 15 | 16 | state.players.onAdd = { player: Player?, key: Int? -> 17 | println("player added: " + key + " " + player?.x) 18 | } 19 | 20 | state.players.onRemove = { player: Player?, key: Int? -> 21 | println("player removed: " + key + " " + player?.x) 22 | 23 | send(2, Cell().apply { x = 100f; y = 200f }) 24 | } 25 | 26 | onLeave = { code -> println("onLeave $code") } 27 | onError = { code, message -> 28 | println("onError") 29 | println(code) 30 | println(message) 31 | } 32 | 33 | // onStateChange = { state, isFirstState -> 34 | // println("OnStateChange") 35 | // println(state.players.size) 36 | // println(isFirstState) 37 | // } 38 | 39 | // onMessage("xxxxx") { message: Player -> 40 | // println("xxxxx!!! >> " + message.x) 41 | // } 42 | 43 | onMessage { primitives: PrimitivesTest -> 44 | println("some primitives...") 45 | println(primitives._string) 46 | } 47 | 48 | // onMessage("hello") { data : Float -> 49 | // println(data) 50 | // } 51 | // 52 | onMessage("hi") { cells: MapSchema -> 53 | println("map size is ") 54 | println(cells.size) 55 | println(cells) 56 | } 57 | 58 | onMessage("hey") { players: ArraySchema -> 59 | println("player array size is ") 60 | println(players.size) 61 | println(players) 62 | } 63 | 64 | onMessage("ahoy") { cell: Cell -> 65 | println("""cell.x = ${cell.x} ,cell.y = ${cell.y}""") 66 | } 67 | 68 | onMessage(2) { cell: Cell -> 69 | println("handler for type 2") 70 | println(" >>>>>>>>>>>>>> " + cell.x) 71 | } 72 | } 73 | }, onError = { e -> 74 | e.printStackTrace() 75 | }) 76 | 77 | readLine() 78 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/example/kotlin/MyState.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.example.kotlin 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | import io.colyseus.serializer.schema.types.ArraySchema 6 | import io.colyseus.serializer.schema.types.MapSchema 7 | import io.colyseus.util.default 8 | 9 | class MyState : Schema() { 10 | @SchemaField("0/ref", PrimitivesTest::class) 11 | var primitives = PrimitivesTest() 12 | 13 | @SchemaField("1/array/ref", Player::class) 14 | var players = ArraySchema(Player::class.java) 15 | 16 | @SchemaField("2/map/ref", Cell::class) 17 | var cells = MapSchema(Cell::class.java) 18 | } 19 | 20 | class Cell : Schema() { 21 | @SchemaField("0/float32") 22 | var x = Float.default 23 | 24 | @SchemaField("1/float32") 25 | var y = Float.default 26 | } 27 | 28 | class PrimitivesTest : Schema() { 29 | @SchemaField("0/uint8") 30 | var _uint8 = Short.default 31 | 32 | @SchemaField("1/uint16") 33 | var _uint16 = Int.default 34 | 35 | @SchemaField("2/uint32") 36 | var _uint32 = Long.default 37 | 38 | @SchemaField("3/uint64") 39 | var _uint64 = Long.default 40 | 41 | @SchemaField("4/int8") 42 | var _int8 = Byte.default 43 | 44 | @SchemaField("5/int16") 45 | var _int16 = Short.default 46 | 47 | @SchemaField("6/int32") 48 | var _int32 = Int.default 49 | 50 | @SchemaField("7/int64") 51 | var _int64 = Long.default 52 | 53 | @SchemaField("8/float32") 54 | var _float32_n = Float.default 55 | 56 | @SchemaField("9/float32") 57 | var _float32_p = Float.default 58 | 59 | @SchemaField("10/float64") 60 | var _float64_n = Double.default 61 | 62 | @SchemaField("11/float64") 63 | var _float64_p = Double.default 64 | 65 | @SchemaField("12/boolean") 66 | var _boolean = Boolean.default 67 | 68 | @SchemaField("13/string") 69 | var _string = String.default 70 | } 71 | 72 | class Player : Schema() { 73 | @SchemaField("0/int32") 74 | var x = Int.default 75 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/serializer/SchemaSerializer.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.serializer 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Context 5 | import io.colyseus.serializer.schema.Iterator 6 | import io.colyseus.serializer.schema.ReferenceTracker 7 | import io.colyseus.serializer.schema.Schema 8 | import io.colyseus.serializer.schema.types.SchemaReflection 9 | import io.colyseus.serializer.schema.types.SchemaReflectionType 10 | import io.colyseus.util.allFields 11 | 12 | class SchemaSerializer(val schema: Class) { 13 | var state: T = schema.getConstructor().newInstance() as T 14 | var refs = ReferenceTracker() 15 | // val lock = Lock() 16 | 17 | fun setState(data: ByteArray, offset: Int = 0) { 18 | // lock.withLock { 19 | state.decode(data, Iterator(offset), refs) 20 | // } 21 | } 22 | 23 | fun patch(data: ByteArray, offset: Int = 0) { 24 | // lock.withLock { 25 | state.decode(data, Iterator(offset), refs) 26 | // } 27 | } 28 | 29 | fun teardown() { 30 | // Clear all stored references. 31 | refs.clear() 32 | Context.instance.clear() 33 | } 34 | 35 | fun handshake(bytes: ByteArray?, offset: Int = 0) { 36 | // lock.withLock { 37 | val reflection = SchemaReflection() 38 | reflection.decode(bytes!!, Iterator(offset)) 39 | Context.instance.clear() 40 | initTypes(reflection, schema = schema as Class) 41 | for (rt in reflection.types) { 42 | Context.instance.setTypeId(rt?.type!!, rt.id) 43 | } 44 | // } 45 | } 46 | 47 | private fun initTypes(reflection: SchemaReflection, index: Int = reflection.rootType, schema: Class) { 48 | val currentType: SchemaReflectionType? = reflection.types[index] 49 | currentType?.type = schema 50 | for (f in currentType?.fields!!) { 51 | if (f?.type in arrayOf("ref", "array", "map")) { 52 | for (field in schema.allFields) { 53 | if (!field.isAnnotationPresent(SchemaField::class.java)) continue 54 | field.isAccessible = true 55 | if (field.name == f?.name) { 56 | val ref = field.getAnnotation(SchemaField::class.java).ref 57 | if (ref == Any::class) throw Exception("Schema error at: ${schema.simpleName}.${field.name}") 58 | initTypes(reflection, f?.referencedType!!, ref.java) 59 | break 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/serializer/schema/Context.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.serializer.schema 2 | 3 | import java.util.* 4 | 5 | class Context { 6 | private var typeIds = LinkedHashMap>() 7 | 8 | operator fun get(typeid: Int): Class<*>? { 9 | return typeIds[typeid] 10 | } 11 | 12 | internal fun setTypeId(type: Class<*>, typeid: Int) { 13 | typeIds[typeid] = type 14 | } 15 | 16 | fun clear() { 17 | typeIds.clear() 18 | } 19 | 20 | companion object { 21 | var instance = Context() 22 | protected set 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/serializer/schema/Decoder.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.serializer.schema 2 | 3 | import java.nio.ByteBuffer 4 | import java.nio.ByteOrder 5 | import java.nio.charset.StandardCharsets 6 | 7 | object Decoder { 8 | fun decodePrimitiveType(type: String?, bytes: ByteArray, it: Iterator): Any { 9 | return when (type) { 10 | "string" -> decodeString(bytes, it) 11 | "number" -> decodeNumber(bytes, it) 12 | "int8" -> decodeInt8(bytes, it) 13 | "uint8" -> decodeUint8(bytes, it) 14 | "int16" -> decodeInt16(bytes, it) 15 | "uint16" -> decodeUint16(bytes, it) 16 | "int32" -> decodeInt32(bytes, it) 17 | "uint32" -> decodeUint32(bytes, it) 18 | "int64" -> decodeInt64(bytes, it) 19 | "uint64" -> decodeUint64(bytes, it) 20 | "float32" -> decodeFloat32(bytes, it) 21 | "float64" -> decodeFloat64(bytes, it) 22 | "boolean" -> decodeBoolean(bytes, it) 23 | else -> Any() 24 | } 25 | } 26 | 27 | fun decodeNumber(bytes: ByteArray, it: Iterator): Float { 28 | val prefix: Int = bytes[it.offset++].toInt() and 0xFF 29 | when { 30 | prefix < 128 -> { 31 | // positive fixint 32 | return prefix.toFloat() 33 | } 34 | prefix == 0xca -> { 35 | // float 32 36 | return decodeFloat32(bytes, it) 37 | } 38 | prefix == 0xcb -> { 39 | // float 64 40 | return decodeFloat64(bytes, it).toFloat() 41 | } 42 | prefix == 0xcc -> { 43 | // uint 8 44 | return decodeUint8(bytes, it).toFloat() 45 | } 46 | prefix == 0xcd -> { 47 | // uint 16 48 | return decodeUint16(bytes, it).toFloat() 49 | } 50 | prefix == 0xce -> { 51 | // uint 32 52 | return decodeUint32(bytes, it).toFloat() 53 | } 54 | prefix == 0xcf -> { 55 | // uint 64 56 | return decodeUint64(bytes, it).toFloat() 57 | } 58 | prefix == 0xd0 -> { 59 | // int 8 60 | return decodeInt8(bytes, it).toFloat() 61 | } 62 | prefix == 0xd1 -> { 63 | // int 16 64 | return decodeInt16(bytes, it).toFloat() 65 | } 66 | prefix == 0xd2 -> { 67 | // int 32 68 | return decodeInt32(bytes, it).toFloat() 69 | } 70 | prefix == 0xd3 -> { 71 | // int 64 72 | return decodeInt64(bytes, it).toFloat() 73 | } 74 | prefix > 0xdf -> { 75 | // negative fixint 76 | return ((0xff - prefix + 1) * -1).toFloat() 77 | } 78 | else -> return Float.NaN 79 | } 80 | } 81 | 82 | fun decodeInt8(bytes: ByteArray, it: Iterator): Byte { 83 | return bytes[it.offset++] 84 | } 85 | 86 | fun decodeUint8(bytes: ByteArray, it: Iterator): Short { 87 | return (bytes[it.offset++].toInt() and 0xFF).toShort() 88 | } 89 | 90 | fun decodeInt16(bytes: ByteArray?, it: Iterator): Short { 91 | val ret = ByteBuffer.wrap(bytes, it.offset, 2).order(ByteOrder.LITTLE_ENDIAN).short 92 | it.offset += 2 93 | return ret 94 | } 95 | 96 | fun decodeUint16(bytes: ByteArray?, it: Iterator): Int { 97 | val ret: Int = ByteBuffer.wrap(bytes, it.offset, 2).order(ByteOrder.LITTLE_ENDIAN).short.toInt() and 0xffff 98 | it.offset += 2 99 | return ret 100 | } 101 | 102 | fun decodeInt32(bytes: ByteArray?, it: Iterator): Int { 103 | val ret = ByteBuffer.wrap(bytes, it.offset, 4).order(ByteOrder.LITTLE_ENDIAN).int 104 | it.offset += 4 105 | return ret 106 | } 107 | 108 | fun decodeUint32(bytes: ByteArray?, it: Iterator): Long { 109 | val ret = (ByteBuffer.wrap(bytes, it.offset, 4).order(ByteOrder.LITTLE_ENDIAN).int.toLong() and 0xffffffffL) 110 | it.offset += 4 111 | return ret 112 | } 113 | 114 | fun decodeFloat32(bytes: ByteArray?, it: Iterator): Float { 115 | val ret = ByteBuffer.wrap(bytes, it.offset, 4).order(ByteOrder.LITTLE_ENDIAN).float 116 | it.offset += 4 117 | return ret 118 | } 119 | 120 | fun decodeFloat64(bytes: ByteArray?, it: Iterator): Double { 121 | val ret = ByteBuffer.wrap(bytes, it.offset, 8).order(ByteOrder.LITTLE_ENDIAN).double 122 | it.offset += 8 123 | return ret 124 | } 125 | 126 | fun decodeInt64(bytes: ByteArray?, it: Iterator): Long { 127 | val ret = ByteBuffer.wrap(bytes, it.offset, 8).order(ByteOrder.LITTLE_ENDIAN).long 128 | it.offset += 8 129 | return ret 130 | } 131 | 132 | fun decodeUint64(bytes: ByteArray?, it: Iterator): Long { 133 | // There is no ulong type in Java so let's use long instead ¯\_(ツ)_/¯ 134 | val ret = ByteBuffer.wrap(bytes, it.offset, 8).order(ByteOrder.LITTLE_ENDIAN).long 135 | it.offset += 8 136 | return ret 137 | } 138 | 139 | fun decodeBoolean(bytes: ByteArray, it: Iterator): Boolean { 140 | return decodeUint8(bytes, it) > 0 141 | } 142 | 143 | fun decodeString(bytes: ByteArray, it: Iterator): String { 144 | val prefix: Int = bytes[it.offset++].toInt() and 0xFF 145 | var length = 0 146 | when { 147 | prefix < 0xc0 -> { 148 | // fixstr 149 | length = prefix and 0x1f 150 | } 151 | prefix == 0xd9 -> { 152 | length = decodeUint8(bytes, it).toInt() 153 | } 154 | prefix == 0xda -> { 155 | length = decodeUint16(bytes, it) 156 | } 157 | prefix == 0xdb -> { 158 | length = decodeUint32(bytes, it).toInt() 159 | } 160 | } 161 | val _bytes = ByteArray(length) 162 | System.arraycopy(bytes, it.offset, _bytes, 0, length) 163 | val str = String(_bytes, StandardCharsets.UTF_8) 164 | it.offset += length 165 | return str 166 | } 167 | 168 | fun switchStructureCheck(bytes: ByteArray, it: Iterator): Boolean { 169 | return bytes[it.offset].toInt() and 0xFF == SPEC.SWITCH_TO_STRUCTURE.value 170 | } 171 | 172 | fun numberCheck(bytes: ByteArray, it: Iterator): Boolean { 173 | val prefix: Int = bytes[it.offset].toInt() and 0xFF 174 | return prefix < 0x80 || prefix in 0xca..0xd3 175 | } 176 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/serializer/schema/Encoder.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.serializer.schema 2 | 3 | import io.colyseus.Protocol 4 | import io.colyseus.util.byteArrayOfInts 5 | 6 | class Encoder { 7 | companion object { 8 | 9 | public fun getInitialBytesFromEncodedType(encodedType: ByteArray): ByteArray { 10 | val size = encodedType.size 11 | return byteArrayOfInts(Protocol.ROOM_DATA) + when { 12 | size < 0x20 -> byteArrayOfInts(size or 0xa0) 13 | size < 0x100 -> byteArrayOfInts(0xd9) + uint8(size) 14 | size < 0x10000 -> byteArrayOfInts(0xda) + uint16(size) 15 | size < 0x7fffffff -> byteArrayOfInts(0xdb) + uint32(size) 16 | else -> throw Exception("String too long") 17 | } 18 | } 19 | 20 | internal fun uint8(value: Int) = byteArrayOfInts(value and 0xFF) 21 | internal fun uint16(value: Int) = uint8(value) + byteArrayOfInts(value shr 8 and 0xFF) 22 | internal fun uint32(value: Int) = uint16(value) + 23 | byteArrayOfInts(value shr 16 and 0xFF, value shr 16 and 0xFF, value shr 24 and 0xFF) 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/serializer/schema/ReferenceTracker.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.serializer.schema 2 | 3 | import io.colyseus.serializer.schema.types.ArraySchema 4 | import io.colyseus.serializer.schema.types.MapSchema 5 | 6 | class ReferenceTracker { 7 | var refs = HashMap() 8 | var refCounts = object : HashMap() { 9 | override fun get(key: Int): Int { 10 | return super.get(key) ?: 0 11 | } 12 | } 13 | var deletedRefs = mutableListOf() 14 | 15 | public fun add(refId: Int, _ref: IRef, incrementCount: Boolean = true) { 16 | val previousCount: Int 17 | if (!refs.containsKey(refId)) { 18 | refs[refId] = _ref 19 | previousCount = 0 20 | } else { 21 | previousCount = refCounts[refId] ?: 0 22 | } 23 | if (incrementCount) { 24 | refCounts[refId] = previousCount + 1 25 | } 26 | } 27 | 28 | public operator fun get(refId: Int): IRef? { 29 | return refs[refId] 30 | } 31 | 32 | public operator fun set(refId: Int, value: IRef?) { 33 | refs.put(refId, value) 34 | } 35 | 36 | public fun has(refId: Int): Boolean { 37 | return refs.containsKey(refId) 38 | } 39 | 40 | fun remove(refId: Int): Boolean? { 41 | refCounts[refId] = refCounts[refId]!! - 1 42 | return if (!deletedRefs.contains(refId)) { 43 | deletedRefs.add(refId) 44 | true 45 | } else { 46 | false 47 | } 48 | } 49 | 50 | public fun garbageCollection() { 51 | var totalDeletedRefs = deletedRefs.size 52 | var i = 0 53 | while (i < totalDeletedRefs) { 54 | val refId = deletedRefs[i] 55 | 56 | if (refCounts[refId]!! <= 0) { 57 | val _ref = refs[refId] 58 | if (_ref is Schema) { 59 | for (field in _ref.fieldChildTypes) { 60 | val fieldValue = _ref[field.key] 61 | if (fieldValue is IRef && remove(fieldValue.__refId)!!) { 62 | totalDeletedRefs++ 63 | } 64 | } 65 | } else if (_ref is ISchemaCollection && _ref.hasSchemaChild()) { 66 | if (_ref is ArraySchema<*>) { 67 | for (item in _ref) { 68 | if (item == null) continue 69 | if (remove((item as IRef).__refId)!!) { 70 | totalDeletedRefs++ 71 | } 72 | } 73 | } else if (_ref is MapSchema<*>) { 74 | for (item in _ref) { 75 | if (item.value == null) continue 76 | if (remove((item.value as IRef).__refId)!!) { 77 | totalDeletedRefs++ 78 | } 79 | } 80 | } 81 | } 82 | refs.remove(refId) 83 | refCounts.remove(refId) 84 | } 85 | i++ 86 | } 87 | 88 | deletedRefs.clear() 89 | } 90 | 91 | fun clear() { 92 | refs.clear() 93 | refCounts.clear() 94 | deletedRefs.clear() 95 | } 96 | 97 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/serializer/schema/Schema.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.serializer.schema 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore 4 | import io.colyseus.annotations.SchemaField 5 | import io.colyseus.serializer.schema.SPEC.SWITCH_TO_STRUCTURE 6 | import io.colyseus.serializer.schema.types.ArraySchema 7 | import io.colyseus.serializer.schema.types.MapSchema 8 | import io.colyseus.util.allFields 9 | import io.colyseus.util.callbacks.Function0Void 10 | import io.colyseus.util.callbacks.Function1Void 11 | import io.colyseus.util.get 12 | import io.colyseus.util.getType 13 | import io.colyseus.util.isPrimary 14 | 15 | 16 | /* 17 | Allowed primitive types: 18 | "string" 19 | "number" 20 | "boolean" 21 | "int8" 22 | "uint8" 23 | "int16" 24 | "uint16" 25 | "int32" 26 | "uint32" 27 | "int64" 28 | "uint64" 29 | "float32" 30 | "float64" 31 | Allowed reference types: 32 | "ref" 33 | "array" 34 | "map" 35 | */ 36 | 37 | public class Iterator(public var offset: Int = 0) 38 | 39 | public enum class SPEC(val value: Int) { 40 | SWITCH_TO_STRUCTURE(255), 41 | TYPE_ID(213) 42 | } 43 | 44 | public enum class OPERATION(val value: Int) { 45 | ADD(128), 46 | REPLACE(0), 47 | DELETE(64), 48 | DELETE_AND_ADD(192), 49 | CLEAR(10), 50 | } 51 | 52 | class DataChange( 53 | var op: Int = 0, 54 | var field: String? = null, 55 | var dynamicIndex: Any? = null, 56 | var value: Any? = null, 57 | var previousValue: Any? = null, 58 | ) 59 | 60 | public interface ISchemaCollection : IRef { 61 | fun invokeOnAdd(item: Any, index: Any) 62 | fun invokeOnChange(item: Any, index: Any) 63 | fun invokeOnRemove(item: Any, index: Any) 64 | 65 | fun triggerAll() 66 | fun _clear(refs: ReferenceTracker?) 67 | 68 | fun getChildType(): Class<*>? 69 | fun getTypeDefaultValue(): Any? 70 | 71 | fun _containsKey(key: Any): Boolean 72 | 73 | fun hasSchemaChild(): Boolean 74 | var childPrimitiveType: String? 75 | 76 | 77 | fun setIndex(index: Int, dynamicIndex: Any) 78 | fun getIndex(index: Int): Any? 79 | fun setByIndex(index: Int, dynamicIndex: Any, value: Any?) 80 | 81 | fun _clone(): ISchemaCollection 82 | 83 | fun _keys(): MutableSet<*> 84 | fun _get(index: Any?): Any? 85 | fun _set(index: Any?, value: Any?) 86 | } 87 | 88 | 89 | public interface IRef { 90 | public var __refId: Int 91 | var __parent: IRef? 92 | 93 | fun getByIndex(index: Int): Any? 94 | 95 | fun deleteByIndex(index: Int) 96 | 97 | fun moveEventHandlers(Iref: IRef) 98 | } 99 | 100 | 101 | open class Schema : IRef { 102 | 103 | @JsonIgnore 104 | val fieldsByIndex = HashMap() 105 | @JsonIgnore 106 | val fieldTypes = HashMap?>() 107 | @JsonIgnore 108 | val fieldTypeNames = HashMap() 109 | @JsonIgnore 110 | val fieldChildPrimitiveTypes = HashMap() 111 | @JsonIgnore 112 | val fieldChildTypes = HashMap?>() 113 | 114 | @JsonIgnore 115 | var onChange: ((changes: List) -> Unit)? = null 116 | 117 | public fun setOnChange(f: Function1Void>) { 118 | onChange = f::invoke 119 | } 120 | 121 | @JsonIgnore 122 | var onRemove: (() -> Unit)? = null 123 | 124 | public fun setOnRemove(f: Function0Void) { 125 | onRemove = f::invoke 126 | } 127 | 128 | @JsonIgnore 129 | public override var __refId: Int = 0 130 | 131 | @JsonIgnore 132 | public override var __parent: IRef? = null 133 | 134 | @JsonIgnore 135 | private var refs: ReferenceTracker? = null 136 | 137 | init { 138 | for (field in javaClass.allFields) { 139 | if (!field.isAnnotationPresent(SchemaField::class.java)) continue 140 | field.isAccessible = true 141 | val fieldName = field.name 142 | val type = field.getAnnotation(SchemaField::class.java).type 143 | val ref = field.getAnnotation(SchemaField::class.java).ref 144 | 145 | val parts = type.split("/").toTypedArray() 146 | val fieldIndex = parts[0].toInt() 147 | val schemaFieldTypeName = parts[1] 148 | 149 | fieldsByIndex[fieldIndex] = fieldName 150 | fieldTypeNames[fieldName] = schemaFieldTypeName 151 | 152 | if (isPrimary(schemaFieldTypeName)) { 153 | fieldTypes[fieldName] = ref.java 154 | } else if (schemaFieldTypeName == "ref") { 155 | fieldTypes[fieldName] = ref.java 156 | fieldChildTypes[fieldName] = ref.java 157 | } else { 158 | // array, map 159 | fieldTypes[fieldName] = getType(schemaFieldTypeName) 160 | fieldChildPrimitiveTypes[fieldName] = parts[2] 161 | fieldChildTypes[fieldName] = ref.java 162 | if (ref == Any::class && parts[2] != "ref") { 163 | fieldChildTypes[fieldName] = getType(parts[2]) 164 | } 165 | } 166 | } 167 | } 168 | 169 | /* allow to retrieve property values by its string name */ 170 | public operator fun get(propertyName: String): Any? { 171 | val field = this::class.java[propertyName] 172 | field?.isAccessible = true 173 | return field?.get(this) 174 | } 175 | 176 | public operator fun set(propertyName: String, value: Any?) { 177 | val field = this::class.java[propertyName] 178 | field?.isAccessible = true 179 | field?.set(this, value) 180 | } 181 | 182 | public override fun moveEventHandlers(previousValue: IRef) { 183 | onChange = (previousValue as Schema).onChange 184 | onRemove = previousValue.onRemove 185 | 186 | for(item in previousValue.fieldsByIndex) { 187 | val child = getByIndex(item.key) 188 | if(child is IRef) { 189 | child.moveEventHandlers(previousValue.getByIndex(item.key) as IRef) 190 | } 191 | } 192 | } 193 | 194 | public fun decode(bytes: ByteArray, it: Iterator? = Iterator(0), refs: ReferenceTracker? = null) { 195 | var it = it 196 | var refs = refs 197 | 198 | if (it == null) it = Iterator() 199 | if (refs == null) refs = ReferenceTracker() 200 | 201 | val totalBytes = bytes.size 202 | 203 | var refId = 0 204 | var _ref: IRef? = this 205 | 206 | this.refs = refs 207 | refs.add(refId, _ref!!) 208 | 209 | var changes = arrayListOf() 210 | val allChanges = linkedMapOf() 211 | allChanges[refId] = changes 212 | 213 | while (it.offset < totalBytes) { 214 | val _byte = bytes[it.offset++].toInt() and 0xFF 215 | 216 | if (_byte == SWITCH_TO_STRUCTURE.value) { 217 | refId = Decoder.decodeNumber(bytes, it).toInt() 218 | _ref = refs[refId] 219 | 220 | // 221 | // Trying to access a reference that haven't been decoded yet. 222 | // 223 | if (_ref == null) { 224 | throw Exception("refId not found: $refId") 225 | } 226 | 227 | // create empty list of changes for this refId. 228 | changes = arrayListOf() 229 | allChanges[refId] = changes 230 | continue 231 | } 232 | 233 | 234 | val isSchema = _ref is Schema 235 | 236 | val operation = if (isSchema) _byte shr 6 shl 6 and 0xFF // "compressed" index + operation 237 | else _byte // "uncompressed" index + operation (array/map items) 238 | 239 | if (operation == OPERATION.CLEAR.value) { 240 | (_ref as ISchemaCollection)._clear(refs) 241 | continue 242 | } 243 | 244 | var fieldIndex: Int 245 | var fieldName: String? = null 246 | var fieldType: String? = null 247 | 248 | var childType: Class<*>? = null 249 | var childPrimitiveType: String? = null 250 | 251 | if (isSchema) { 252 | fieldIndex = _byte % (if (operation == 0) 255 else operation) // FIXME: JS allows (0 || 255) 253 | fieldName = (_ref as Schema).fieldsByIndex[fieldIndex] 254 | 255 | fieldType = _ref.fieldTypeNames[fieldName] 256 | childType = _ref.fieldChildTypes[fieldName] 257 | } else { 258 | fieldName = "" // FIXME 259 | 260 | fieldIndex = Decoder.decodeNumber(bytes, it).toInt() 261 | if ((_ref as ISchemaCollection).hasSchemaChild()) { 262 | fieldType = "ref" 263 | childType = (_ref as ISchemaCollection).getChildType() 264 | } else { 265 | fieldType = (_ref as ISchemaCollection).childPrimitiveType 266 | } 267 | } 268 | 269 | 270 | var value: Any? = null 271 | var previousValue: Any? = null 272 | var dynamicIndex: Any? = null 273 | 274 | if (!isSchema) { 275 | previousValue = _ref.getByIndex(fieldIndex) 276 | if ((operation and OPERATION.ADD.value) == OPERATION.ADD.value) { 277 | // MapSchema dynamic index. 278 | // dynamicIndex = if ((_ref as ISchemaCollection).GetItems() is HashMap) 279 | dynamicIndex = if (_ref is MapSchema<*>) 280 | Decoder.decodeString(bytes, it) 281 | else fieldIndex 282 | 283 | (_ref as ISchemaCollection).setIndex(fieldIndex, dynamicIndex) 284 | } else { 285 | dynamicIndex = (_ref as ISchemaCollection).getIndex(fieldIndex) 286 | } 287 | } else if (fieldName != null) { // FIXME: duplicate check 288 | previousValue = (_ref as Schema)[fieldName] 289 | } 290 | 291 | 292 | // 293 | // Delete operations 294 | // 295 | if ((operation and OPERATION.DELETE.value) == OPERATION.DELETE.value) { 296 | if (operation != OPERATION.DELETE_AND_ADD.value) { 297 | _ref.deleteByIndex(fieldIndex) 298 | } 299 | 300 | // Flag `refId` for garbage collection. 301 | if (previousValue != null && previousValue is IRef) { 302 | refs.remove(previousValue.__refId) 303 | } 304 | 305 | value = null 306 | } 307 | 308 | 309 | if (fieldName == null) { 310 | // 311 | // keep skipping next bytes until reaches a known structure 312 | // by local decoder. 313 | // 314 | val nextIterator = Iterator(offset = it.offset) 315 | 316 | while (it.offset < totalBytes) { 317 | if (Decoder.switchStructureCheck(bytes, it)) { 318 | nextIterator.offset = it.offset + 1 319 | if (refs.has(Decoder.decodeNumber(bytes, nextIterator).toInt())) { 320 | break 321 | } 322 | } 323 | 324 | it.offset++ 325 | } 326 | 327 | continue 328 | 329 | } else if (operation == OPERATION.DELETE.value) { 330 | // 331 | // FIXME: refactor me. 332 | // Don't do anything. 333 | // 334 | } else if (fieldType == "ref") { 335 | refId = Decoder.decodeNumber(bytes, it).toInt() 336 | value = refs[refId] 337 | 338 | if (operation != OPERATION.REPLACE.value) { 339 | val concreteChildType = getSchemaType(bytes, it, childType) 340 | 341 | if (value == null) { 342 | value = createTypeInstance(concreteChildType) 343 | 344 | if (previousValue != null) { 345 | (value as Schema).moveEventHandlers(previousValue as Schema) 346 | 347 | if ((previousValue as IRef).__refId > 0 && refId != previousValue.__refId) { 348 | refs.remove((previousValue as IRef).__refId) 349 | } 350 | } 351 | } 352 | 353 | refs.add(refId, value as IRef, (value != previousValue)) 354 | } 355 | } else if (childType == null) { 356 | // primitive values 357 | value = Decoder.decodePrimitiveType(fieldType, bytes, it) 358 | } else { 359 | refId = Decoder.decodeNumber(bytes, it).toInt() 360 | value = refs[refId] 361 | 362 | val valueRef: ISchemaCollection = if (refs.has(refId)) 363 | previousValue as ISchemaCollection 364 | else { 365 | when (fieldType) { 366 | "array" -> ArraySchema(childType) 367 | "map" -> MapSchema(childType) 368 | else -> throw Error("$fieldType is not supported") 369 | } 370 | } 371 | 372 | // value = valueRef._clone() 373 | value = valueRef 374 | 375 | // keep reference to nested childPrimitiveType. 376 | childPrimitiveType = (_ref as Schema).fieldChildPrimitiveTypes[fieldName] 377 | value.childPrimitiveType = childPrimitiveType!! 378 | 379 | if (previousValue != null) { 380 | value.moveEventHandlers(previousValue as ISchemaCollection) 381 | 382 | if ((previousValue as IRef).__refId > 0 && refId != (previousValue as IRef).__refId) { 383 | refs.remove((previousValue as IRef).__refId) 384 | 385 | val deletes = arrayListOf() 386 | val keys = (previousValue as ISchemaCollection)._keys() 387 | 388 | for (key in keys) { 389 | deletes.add(DataChange( 390 | dynamicIndex = key, 391 | op = OPERATION.DELETE.value, 392 | value = null, 393 | previousValue = previousValue._get(key) 394 | )) 395 | } 396 | 397 | allChanges[(previousValue as IRef).__refId] = deletes 398 | } 399 | } 400 | 401 | refs.add(refId, value as IRef, (valueRef !== previousValue)) 402 | } 403 | 404 | 405 | val hasChange = previousValue !== value 406 | 407 | if (value != null) { 408 | if (value is IRef) { 409 | value.__refId = refId 410 | value.__parent = _ref 411 | } 412 | 413 | if (_ref is Schema) { 414 | _ref[fieldName] = value 415 | } else if (_ref is ISchemaCollection) { 416 | (_ref as ISchemaCollection).setByIndex(fieldIndex, dynamicIndex!!, value) 417 | } 418 | } 419 | 420 | if (hasChange) { 421 | changes.add(DataChange( 422 | op = operation, 423 | field = fieldName, 424 | dynamicIndex = dynamicIndex, 425 | value = value, 426 | previousValue = previousValue 427 | )) 428 | } 429 | } 430 | 431 | triggerChanges(allChanges) 432 | 433 | refs.garbageCollection() 434 | 435 | } 436 | 437 | public fun triggerAll() { 438 | val allChanges = HashMap() 439 | triggerAllFillChanges(this, allChanges) 440 | triggerChanges(allChanges) 441 | } 442 | 443 | 444 | protected fun triggerAllFillChanges(currentRef: IRef, allChanges: HashMap) { 445 | // skip recursive structures... 446 | if (allChanges.contains(currentRef.__refId)) { 447 | return 448 | } 449 | 450 | val changes = arrayListOf() 451 | allChanges[currentRef.__refId as Any] = changes 452 | 453 | if (currentRef is Schema) { 454 | for (fieldName in currentRef.fieldsByIndex.values) { 455 | val value = currentRef[fieldName!!] 456 | changes.add(DataChange( 457 | field = fieldName, 458 | op = OPERATION.ADD.value, 459 | value = value 460 | )) 461 | 462 | if (value is IRef) { 463 | triggerAllFillChanges(value, allChanges) 464 | } 465 | } 466 | } else { 467 | if ((currentRef as ISchemaCollection).hasSchemaChild()) { 468 | val keys = (currentRef as ISchemaCollection)._keys() 469 | for (key in keys) { 470 | val child = currentRef._get(key) 471 | 472 | changes.add(DataChange 473 | ( 474 | field = null, 475 | dynamicIndex = key, 476 | op = OPERATION.ADD.value, 477 | value = child 478 | )) 479 | 480 | triggerAllFillChanges(child as IRef, allChanges) 481 | } 482 | } 483 | } 484 | } 485 | 486 | fun triggerChanges(allChanges: HashMap) { 487 | for (key in allChanges.keys) { 488 | val changes = allChanges[key] as List? 489 | 490 | val _ref = refs!![key as Int] 491 | val isSchema = _ref is Schema 492 | 493 | for (change in changes!!) { 494 | //const listener = ref['$listeners'] && ref['$listeners'][change.field] 495 | 496 | if (!isSchema) { 497 | val container = _ref as ISchemaCollection 498 | 499 | if (change.op == OPERATION.ADD.value && change.previousValue == container.getTypeDefaultValue()) { 500 | container.invokeOnAdd(change.value!!, change.dynamicIndex!!) 501 | 502 | } else if (change.op == OPERATION.DELETE.value) { 503 | // 504 | // FIXME: `previousValue` should always be avaiiable. 505 | // ADD + DELETE operations are still encoding DELETE operation. 506 | // 507 | if (change.previousValue != container.getTypeDefaultValue()) { 508 | container.invokeOnRemove(change.previousValue!!, change.dynamicIndex 509 | ?: change.field!!) 510 | } 511 | } else if (change.op == OPERATION.DELETE_AND_ADD.value) { 512 | if (change.previousValue != container.getTypeDefaultValue()) { 513 | container.invokeOnRemove(change.previousValue!!, change.dynamicIndex!!) 514 | } 515 | container.invokeOnAdd(change.value!!, change.dynamicIndex!!) 516 | 517 | } else if (change.op == OPERATION.REPLACE.value || change.value != change.previousValue) { 518 | container.invokeOnChange(change.value!!, change.dynamicIndex!!) 519 | } 520 | } 521 | 522 | // 523 | // trigger onRemove on child structure. 524 | // 525 | if ((change.op and OPERATION.DELETE.value) == OPERATION.DELETE.value && change.previousValue is Schema) { 526 | (change.previousValue as Schema).onRemove?.invoke() 527 | } 528 | } 529 | 530 | if (isSchema) { 531 | (_ref as Schema).onChange?.invoke(changes) 532 | } 533 | } 534 | } 535 | 536 | fun getSchemaType(bytes: ByteArray, it: Iterator, defaultType: Class<*>?): Class<*>? { 537 | var type: Class<*>? = defaultType 538 | if (it.offset < bytes.size && bytes[it.offset].toInt() and 0xFF == SPEC.TYPE_ID.value) { 539 | it.offset++ 540 | val typeId: Int = Decoder.decodeNumber(bytes, it).toInt() 541 | type = Context.instance[typeId] 542 | } 543 | return type 544 | } 545 | 546 | fun createTypeInstance(type: Class<*>?): Any { 547 | val constructor = type!!.getDeclaredConstructor() 548 | constructor.isAccessible = true 549 | return constructor.newInstance() 550 | } 551 | 552 | 553 | public override fun getByIndex(index: Int): Any? { 554 | val fieldName: String = fieldsByIndex[index] ?: return null 555 | return this[fieldName] 556 | } 557 | 558 | public override fun deleteByIndex(index: Int) { 559 | val fieldName: String = fieldsByIndex[index] ?: return 560 | this[fieldName] = null 561 | } 562 | 563 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/serializer/schema/types/ArraySchema.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.serializer.schema.types 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore 4 | import io.colyseus.serializer.schema.IRef 5 | import io.colyseus.serializer.schema.ISchemaCollection 6 | import io.colyseus.serializer.schema.ReferenceTracker 7 | import io.colyseus.serializer.schema.Schema 8 | import io.colyseus.util.callbacks.Function2Void 9 | import io.colyseus.util.default 10 | import java.util.* 11 | 12 | class ArraySchema( 13 | @JsonIgnore public var ct: Class?, 14 | ) : ArrayList(), ISchemaCollection { 15 | 16 | constructor() : this(null) 17 | 18 | @JsonIgnore 19 | private val items: TreeMap = TreeMap() 20 | 21 | @JsonIgnore 22 | var onAdd: ((value: T, key: Int) -> Unit)? = null 23 | 24 | public fun setOnAdd(f: Function2Void) { 25 | onAdd = f::invoke 26 | } 27 | 28 | @JsonIgnore 29 | var onChange: ((value: T, key: Int) -> Unit)? = null 30 | 31 | public fun setOnChange(f: Function2Void) { 32 | onChange = f::invoke 33 | } 34 | 35 | @JsonIgnore 36 | var onRemove: ((value: T, key: Int) -> Unit)? = null 37 | 38 | public fun setOnRemove(f: Function2Void) { 39 | onRemove = f::invoke 40 | } 41 | 42 | public override fun hasSchemaChild(): Boolean = (Schema::class.java).isAssignableFrom(ct) 43 | 44 | override var childPrimitiveType: String? = null 45 | 46 | public override var __refId: Int = 0 47 | public override var __parent: IRef? = null 48 | 49 | public override fun setIndex(index: Int, dynamicIndex: Any) { 50 | // println("setIndex index=$index dynamicIndex=$dynamicIndex") 51 | } 52 | 53 | public override fun setByIndex(index: Int, dynamicIndex: Any, value: Any?) { 54 | // val ind = dynamicIndex as Int 55 | 56 | // if (ind < 0) return 57 | // 58 | // if (ind < size) { 59 | // this[ind] = value as T? 60 | // return 61 | // } 62 | 63 | // val s = size 64 | // for (i in s until ind) add(null) 65 | // add(value as T?) 66 | 67 | items[dynamicIndex as Int] = value as T? 68 | } 69 | 70 | // override fun put(key: Int, value: T?): T? { 71 | // val value = super.put(key, value) 72 | // valuesList = values.toList() 73 | // return value 74 | // } 75 | 76 | public override operator fun get(key: Int): T? { 77 | // FIXME: should be O(1) 78 | if (key >= size) return null 79 | return items.values.toList().get(key) 80 | } 81 | 82 | public override fun getIndex(index: Int): Int? { 83 | return index 84 | } 85 | 86 | public override fun getByIndex(index: Int): Any? { 87 | return this[index] 88 | } 89 | 90 | public override fun deleteByIndex(index: Int) { 91 | // if (index < 0 || index >= size) return 92 | // this.removeAt(index) 93 | // TODO 94 | items.remove(index) 95 | } 96 | 97 | override fun remove(element: T?): Boolean { 98 | return items.values.remove(element) 99 | } 100 | 101 | override fun removeAt(index: Int): T? { 102 | // FIXME: should be O(1) 103 | if (index >= size) return null 104 | val key = items.keys.toList().get(index) 105 | return items.remove(key) 106 | } 107 | 108 | public override fun _clear(refs: ReferenceTracker?) { 109 | if (refs != null && hasSchemaChild()) { 110 | for (item in this.items.values) { 111 | if (item == null) continue 112 | refs.remove((item as Schema).__refId) 113 | } 114 | } 115 | items.clear() 116 | } 117 | 118 | public override fun _clone(): ISchemaCollection { 119 | val clone = ArraySchema(ct) 120 | clone.onAdd = onAdd 121 | clone.onChange = onChange 122 | clone.onRemove = onRemove 123 | return clone 124 | } 125 | 126 | override fun _keys(): MutableSet<*> { 127 | val set = mutableSetOf() 128 | set.addAll(0 until size) 129 | return set 130 | } 131 | 132 | override fun _get(index: Any?): Any? { 133 | return getByIndex(index as Int) 134 | } 135 | 136 | override fun _set(index: Any?, value: Any?) { 137 | setByIndex(index as Int, index, value) 138 | } 139 | 140 | public override fun getChildType(): Class<*> { 141 | return ct!! 142 | } 143 | 144 | public override fun getTypeDefaultValue(): Any? { 145 | return default(ct!!) 146 | } 147 | 148 | public override fun _containsKey(key: Any): Boolean { 149 | key as Int 150 | return key >= 0 && key < size 151 | } 152 | 153 | public override fun triggerAll() { 154 | if (onAdd == null) { 155 | return 156 | } 157 | for (i in 0 until size) { 158 | if (this[i] == null) continue 159 | onAdd?.invoke(this[i]!!, i) 160 | } 161 | } 162 | 163 | public override fun moveEventHandlers(previousInstance: IRef) { 164 | onAdd = (previousInstance as ArraySchema).onAdd 165 | onChange = previousInstance.onChange 166 | onRemove = previousInstance.onRemove 167 | } 168 | 169 | override fun invokeOnAdd(item: Any, index: Any) { 170 | onAdd?.invoke(item as T, index as Int) 171 | } 172 | 173 | override fun invokeOnChange(item: Any, index: Any) { 174 | onChange?.invoke(item as T, index as Int) 175 | } 176 | 177 | override fun invokeOnRemove(item: Any, index: Any) { 178 | onRemove?.invoke(item as T, index as Int) 179 | } 180 | 181 | override fun toString(): String { 182 | return items.values.toString() 183 | } 184 | 185 | override fun iterator(): MutableIterator { 186 | return items.values.iterator() 187 | } 188 | 189 | override fun add(element: T?): Boolean { 190 | items[try { 191 | items.keys.last() 192 | } catch (e: NoSuchElementException) { 193 | -1 194 | } + 1] = element 195 | return true 196 | } 197 | 198 | override fun set(index: Int, element: T?): T? { 199 | if (index > items.size) return null 200 | if (index == items.size) { 201 | add(element) 202 | return element 203 | } 204 | val key = items.keys.toList()[index] 205 | items.put(key, element) 206 | return element 207 | } 208 | 209 | override val size: Int 210 | get() = items.size 211 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/serializer/schema/types/MapSchema.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.serializer.schema.types 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore 4 | import io.colyseus.serializer.schema.IRef 5 | import io.colyseus.serializer.schema.ISchemaCollection 6 | import io.colyseus.serializer.schema.ReferenceTracker 7 | import io.colyseus.serializer.schema.Schema 8 | import io.colyseus.util.callbacks.Function2Void 9 | import io.colyseus.util.default 10 | import kotlin.collections.set 11 | 12 | class MapSchema( 13 | @JsonIgnore public var ct: Class?, 14 | ) : LinkedHashMap(), ISchemaCollection { 15 | 16 | constructor() : this(null) 17 | 18 | @JsonIgnore 19 | public var onAdd: ((value: T, key: String) -> Unit)? = null 20 | 21 | public fun setOnAdd(f: Function2Void) { 22 | onAdd = f::invoke 23 | } 24 | 25 | @JsonIgnore 26 | public var onChange: ((value: T, key: String) -> Unit)? = null 27 | 28 | public fun setOnChange(f: Function2Void) { 29 | onChange = f::invoke 30 | } 31 | 32 | @JsonIgnore 33 | public var onRemove: ((value: T, key: String) -> Unit)? = null 34 | 35 | public fun setOnRemove(f: Function2Void) { 36 | onRemove = f::invoke 37 | } 38 | 39 | var indexes = HashMap() 40 | 41 | public override var __refId: Int = 0 42 | public override var __parent: IRef? = null 43 | 44 | override fun getByIndex(index: Int): Any? { 45 | val dynamicIndex: String? = getIndex(index) as String? 46 | return if (dynamicIndex != null && contains(dynamicIndex)) get(dynamicIndex) else getTypeDefaultValue() 47 | } 48 | 49 | override fun deleteByIndex(index: Int) { 50 | val dynamicIndex: String? = getIndex(index) as String? 51 | // 52 | // FIXME: 53 | // The schema encoder should not encode a DELETE operation when using ADD + DELETE in the same key. (in the same patch) 54 | // 55 | if (dynamicIndex != null && contains(dynamicIndex)) { 56 | remove(dynamicIndex) 57 | indexes.remove(index) 58 | } 59 | } 60 | 61 | public override fun setIndex(index: Int, dynamicIndex: Any) { 62 | indexes[index] = dynamicIndex as String 63 | } 64 | 65 | public override fun setByIndex(index: Int, dynamicIndex: Any, value: Any?) { 66 | indexes[index] = dynamicIndex as String 67 | this[dynamicIndex] = value as T? 68 | } 69 | 70 | public override fun getIndex(index: Int): Any? { 71 | return indexes[index] 72 | } 73 | 74 | public override fun _clone(): ISchemaCollection { 75 | val clone = MapSchema(ct) 76 | clone.onAdd = onAdd 77 | clone.onAdd = onChange 78 | clone.onAdd = onRemove 79 | return clone 80 | } 81 | 82 | override fun _keys(): MutableSet<*> { 83 | return keys 84 | } 85 | 86 | override fun _get(index: Any?): Any? { 87 | return this[index] 88 | } 89 | 90 | override fun _set(index: Any?, value: Any?) { 91 | this[index as String] = value as T? 92 | } 93 | 94 | public override fun getChildType(): Class<*> { 95 | return ct!! 96 | } 97 | 98 | public override fun getTypeDefaultValue(): Any? { 99 | return default(ct!!::class.java) 100 | } 101 | 102 | public override fun _containsKey(key: Any): Boolean { 103 | return contains(key as String) 104 | } 105 | 106 | public override fun hasSchemaChild(): Boolean = (Schema::class.java).isAssignableFrom(ct) 107 | 108 | public override var childPrimitiveType: String? = null 109 | 110 | public fun _add(item: Pair) { 111 | this[item.first] = item.second 112 | } 113 | 114 | public override fun _clear(refs: ReferenceTracker?) { 115 | if (refs != null && hasSchemaChild()) { 116 | for (item in values) { 117 | if (item == null) continue 118 | refs.remove((item as IRef).__refId) 119 | } 120 | } 121 | 122 | indexes.clear() 123 | clear() 124 | } 125 | 126 | public fun _contains(item: Pair): Boolean { 127 | return contains(item.first) 128 | } 129 | 130 | public fun _remove(item: Pair): Boolean { 131 | val value: T? = this[item.first] 132 | if (value != null && value.equals(item.second)) { 133 | remove(item.first) 134 | return true 135 | } 136 | return false 137 | } 138 | 139 | public fun _count(): Int { 140 | return size 141 | } 142 | 143 | public fun _add(key: String, value: T) { 144 | this[key] = value 145 | } 146 | 147 | public fun _remove(key: String): Boolean { 148 | val result = contains(key) 149 | if (result) { 150 | remove(key) 151 | } 152 | return result 153 | } 154 | 155 | 156 | public override fun triggerAll() { 157 | if (onAdd == null) return 158 | for (item in this) { 159 | if (item.value == null) continue 160 | onAdd?.invoke(item.value as T, item.key) 161 | } 162 | } 163 | 164 | public override fun moveEventHandlers(previousInstance: IRef) { 165 | onAdd = (previousInstance as (MapSchema)).onAdd 166 | onChange = (previousInstance).onChange 167 | onRemove = (previousInstance).onRemove 168 | } 169 | 170 | public override fun invokeOnAdd(item: Any, index: Any) { 171 | onAdd?.invoke(item as T, index as String) 172 | } 173 | 174 | public override fun invokeOnChange(item: Any, index: Any) { 175 | onChange?.invoke(item as T, index as String) 176 | } 177 | 178 | public override fun invokeOnRemove(item: Any, index: Any) { 179 | onRemove?.invoke(item as T, index as String) 180 | } 181 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/serializer/schema/types/Reflection.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.serializer.schema.types 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | 6 | 7 | class SchemaReflectionField : Schema() { 8 | @SchemaField("0/string", String::class) 9 | var name: String? = null 10 | 11 | @SchemaField("1/string", String::class) 12 | var type: String? = null 13 | 14 | @SchemaField("2/uint8", Int::class) 15 | var referencedType = 0 16 | } 17 | 18 | 19 | class SchemaReflectionType : Schema() { 20 | @SchemaField(type = "0/uint8", Int::class) 21 | var id = 0 22 | 23 | @SchemaField("1/array/ref", SchemaReflectionField::class) 24 | var fields = ArraySchema(SchemaReflectionField::class.java) 25 | 26 | var type: Class<*>? = null 27 | } 28 | 29 | 30 | class SchemaReflection : Schema() { 31 | @SchemaField("0/array/ref", SchemaReflectionType::class) 32 | var types = ArraySchema(SchemaReflectionType::class.java) 33 | 34 | @SchemaField("1/uint8", Int::class) 35 | var rootType = 0 36 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/util/Http.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.util 2 | 3 | import java.io.BufferedReader 4 | import java.io.IOException 5 | import java.io.InputStream 6 | import java.io.InputStreamReader 7 | import java.net.HttpURLConnection 8 | import java.net.URL 9 | import java.nio.charset.StandardCharsets 10 | 11 | object Http { 12 | private const val HTTP_CONNECT_TIMEOUT = 10000 13 | private const val HTTP_READ_TIMEOUT = 10000 14 | 15 | @Throws(IOException::class, HttpException::class) 16 | fun request(url: String?, method: String? = "GET", httpHeaders: MutableMap? = null, body: String? = null): String { 17 | // System.out.println("sending http request to server...") 18 | // System.out.println("url is " + url) 19 | // System.out.println("http request body is " + body) 20 | // System.out.println("http request method is " + method) 21 | val con = URL(url).openConnection() as HttpURLConnection 22 | con.requestMethod = method 23 | if (httpHeaders != null) { 24 | for ((key, value) in httpHeaders) { 25 | con.setRequestProperty(key, value) 26 | } 27 | } 28 | con.connectTimeout = HTTP_CONNECT_TIMEOUT 29 | con.readTimeout = HTTP_READ_TIMEOUT 30 | if (body != null) { 31 | con.doOutput = true 32 | val os = con.outputStream 33 | val input = body.toByteArray(StandardCharsets.UTF_8) 34 | os.write(input, 0, input.size) 35 | } 36 | val code = con.responseCode 37 | // System.out.println("http response code is " + code) 38 | val inputStream: InputStream = if (code != HttpURLConnection.HTTP_OK) con.errorStream else con.inputStream 39 | val br = BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8)) 40 | val sb = StringBuilder() 41 | var responseLine: String? 42 | while (br.readLine().also { responseLine = it } != null) { 43 | sb.append(responseLine!!.trim { it <= ' ' }) 44 | } 45 | val response = sb.toString() 46 | if (code != HttpURLConnection.HTTP_OK) { 47 | throw HttpException(response, code) 48 | } 49 | return response 50 | } 51 | 52 | class HttpException internal constructor(response: String?, var code: Int) : Exception(response) 53 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/util/Util.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.util 2 | 3 | import io.colyseus.serializer.schema.types.ArraySchema 4 | import io.colyseus.serializer.schema.types.MapSchema 5 | import java.lang.reflect.Field 6 | import java.util.concurrent.locks.ReentrantLock 7 | 8 | internal fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() } 9 | internal operator fun ByteArray.get(intRange: IntRange): ByteArray = copyOfRange(intRange.first, intRange.last) 10 | internal operator fun ByteArray.get(i: Int, j: Int): ByteArray = copyOfRange(i, j) 11 | 12 | fun default(javaClass: Class<*>): Any? { 13 | return when (javaClass) { 14 | Byte::class.java -> 0.toByte() 15 | Short::class.java -> 0.toShort() 16 | Int::class.java -> 0 17 | Long::class.java -> 0.toLong() 18 | Float::class.java -> 0.toFloat() 19 | Double::class.java -> 0.toDouble() 20 | Boolean::class.java -> false 21 | String::class.java -> null 22 | else -> null 23 | } 24 | } 25 | 26 | public val Byte.Companion.default: Byte get() = 0 27 | public val Short.Companion.default: Short get() = 0 28 | public val Int.Companion.default: Int get() = 0 29 | public val Long.Companion.default: Long get() = 0 30 | public val Float.Companion.default: Float get() = 0f 31 | public val Double.Companion.default: Double get() = 0.0 32 | public val Boolean.Companion.default: Boolean get() = false 33 | public val String.Companion.default: String? get() = null 34 | 35 | fun isPrimary(type: String): Boolean { 36 | return type !in arrayOf("map", "array", "ref") 37 | } 38 | 39 | fun getType(type: String): Class<*>? { 40 | return when (type) { 41 | "int8" -> Byte::class.java 42 | "uint8" -> Short::class.java 43 | "int16" -> Short::class.java 44 | "uint16" -> Int::class.java 45 | "int32" -> Int::class.java 46 | "uint32" -> Long::class.java 47 | "int64" -> Long::class.java 48 | "uint64" -> Long::class.java 49 | "float32" -> Float::class.java 50 | "float64" -> Double::class.java 51 | "number" -> Float::class.java 52 | "string" -> String::class.java 53 | "boolean" -> Boolean::class.java 54 | "map" -> MapSchema::class.java 55 | "array" -> ArraySchema::class.java 56 | else -> null 57 | } 58 | } 59 | 60 | val Class<*>.allFields: List 61 | get() { 62 | val allFields = mutableListOf() 63 | var currentClass = this as Class<*>? 64 | while (currentClass != null) { 65 | val declaredFields: Array = currentClass.declaredFields 66 | allFields.addAll(declaredFields) 67 | currentClass = currentClass.superclass 68 | } 69 | return allFields 70 | } 71 | 72 | operator fun Class<*>.get(name: String): Field? { 73 | for (field in allFields) { 74 | if (field.name == name) return field 75 | } 76 | return null 77 | } 78 | 79 | class Lock { 80 | private val lock = ReentrantLock(true) 81 | fun withLock(block: () -> T): T { 82 | try { 83 | lock.lock() 84 | return block() 85 | } finally { 86 | lock.unlock() 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/util/callbacks/Function0Void.java: -------------------------------------------------------------------------------- 1 | package io.colyseus.util.callbacks; 2 | 3 | public interface Function0Void { 4 | void invoke(); 5 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/util/callbacks/Function1Void.java: -------------------------------------------------------------------------------- 1 | package io.colyseus.util.callbacks; 2 | 3 | public interface Function1Void { 4 | void invoke(T p1); 5 | } -------------------------------------------------------------------------------- /src/main/java/io/colyseus/util/callbacks/Function2Void.java: -------------------------------------------------------------------------------- 1 | package io.colyseus.util.callbacks; 2 | 3 | public interface Function2Void { 4 | void invoke(T p1, K p2); 5 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/mapper/SerializeDeserializeTest.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.mapper 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper 4 | import io.colyseus.test.mapper.types.Cell 5 | import io.colyseus.test.mapper.types.MyState 6 | import io.colyseus.test.mapper.types.Player 7 | import org.junit.jupiter.api.Assertions.assertEquals 8 | import org.junit.jupiter.api.Test 9 | import org.msgpack.jackson.dataformat.MessagePackFactory 10 | 11 | class SerializeDeserializeTest { 12 | @Test 13 | fun `json serialize test`() { 14 | val objectMapper = ObjectMapper() 15 | 16 | val serialized = objectMapper.writeValueAsString(createMyState()) 17 | 18 | println(serialized) 19 | 20 | val deserialized = objectMapper.readValue(serialized, LinkedHashMap::class.java) 21 | 22 | assertMyState(deserialized as LinkedHashMap) 23 | } 24 | 25 | @Test 26 | fun `msgpack serialize test`() { 27 | val objectMapper = ObjectMapper(MessagePackFactory()) 28 | 29 | val serialized = objectMapper.writeValueAsBytes(createMyState()) 30 | 31 | println(serialized.contentToString()) 32 | 33 | val deserialized = objectMapper.readValue(serialized, LinkedHashMap::class.java) 34 | 35 | assertMyState(deserialized as LinkedHashMap) 36 | } 37 | 38 | fun createMyState(): MyState { 39 | return MyState().apply { 40 | player = Player().apply { 41 | _uint8 = 0xFF 42 | _uint16 = 0xFFFF 43 | _uint32 = 0xFFFFFF 44 | _uint64 = 0xFFFFFFFF 45 | _int8 = 100.toByte() 46 | _int16 = 100.toShort() 47 | _int32 = 100 48 | _int64 = 100L 49 | _float32_1 = 100f 50 | _float32_2 = 200f 51 | _float64_1 = 100.0 52 | _float64_2 = 200.0 53 | _number = 100f 54 | _boolean = true 55 | _string1 = "hello world!" 56 | _string2 = "" 57 | _string3 = null 58 | 59 | with(arrayOfCells) { 60 | repeat(5) { 61 | add(Cell().apply { x = 100f; y = 200f; }) 62 | } 63 | } 64 | 65 | with(mapOfCells) { 66 | repeat(5) { 67 | put("index_$it", Cell().apply { x = 100f; y = 200f; }) 68 | } 69 | } 70 | 71 | with(arrayOfPrimitives) { 72 | repeat(10) { 73 | add(Math.random().toFloat() * 1000) 74 | } 75 | } 76 | with(mapOfPrimitives) { 77 | repeat(10) { 78 | put("index_$it", Math.random().toFloat() * 1000) 79 | } 80 | } 81 | } 82 | with(arrayOfPlayers) { 83 | repeat(5) { 84 | add(player) 85 | } 86 | } 87 | with(mapOfPlayers) { 88 | repeat(5) { 89 | put("index_$it", player) 90 | } 91 | } 92 | with(arrayOfPrimitives) { 93 | repeat(10) { 94 | add(Math.random().toFloat() * 1000) 95 | } 96 | } 97 | with(mapOfPrimitives) { 98 | repeat(10) { 99 | put("index_$it", Math.random().toFloat() * 1000) 100 | } 101 | } 102 | } 103 | } 104 | 105 | fun assertMyState(myState: LinkedHashMap) { 106 | assertEquals(5, myState.keys.size) 107 | assertPlayer(myState["player"] as LinkedHashMap) 108 | with(myState["arrayOfPlayers"] as ArrayList>) { 109 | assertEquals(5, size) 110 | forEach { 111 | assertPlayer(it) 112 | } 113 | } 114 | with(myState["mapOfPlayers"] as LinkedHashMap) { 115 | assertEquals(5, size) 116 | forEach { 117 | assertPlayer(it.value as LinkedHashMap) 118 | } 119 | } 120 | with(myState["arrayOfPrimitives"] as ArrayList) { 121 | assertEquals(10, size) 122 | forEach { 123 | assertEquals(true, it is Double) 124 | } 125 | } 126 | with(myState["mapOfPrimitives"] as LinkedHashMap) { 127 | assertEquals(10, size) 128 | forEach { 129 | assertEquals(true, it.value is Double) 130 | } 131 | } 132 | } 133 | 134 | fun assertPlayer(player: LinkedHashMap) { 135 | assertEquals(21, player.keys.size) 136 | assertEquals(0xFF, player["_uint8"]) 137 | assertEquals(0xFFFF, player["_uint16"]) 138 | assertEquals(0xFFFFFF, player["_uint32"]) 139 | assertEquals(0xFFFFFFFF, player["_uint64"]) 140 | assertEquals(100, player["_int8"]) 141 | assertEquals(100, player["_int16"]) 142 | assertEquals(100, player["_int32"]) 143 | assertEquals(100, player["_int64"]) 144 | assertEquals(100.0, player["_float32_1"]) 145 | assertEquals(200.0, player["_float32_2"]) 146 | assertEquals(100.0, player["_float64_1"]) 147 | assertEquals(200.0, player["_float64_2"]) 148 | assertEquals(100.0, player["_number"]) 149 | assertEquals(true, player["_boolean"]) 150 | assertEquals("hello world!", player["_string1"]) 151 | assertEquals("", player["_string2"]) 152 | assertEquals(null, player["_string3"]) 153 | 154 | with(player["arrayOfCells"] as ArrayList>) { 155 | assertEquals(5, size) 156 | forEach { 157 | assertCell(it) 158 | } 159 | } 160 | with(player["mapOfCells"] as LinkedHashMap) { 161 | assertEquals(5, size) 162 | forEach { 163 | assertCell(it.value as LinkedHashMap) 164 | } 165 | } 166 | with(player["arrayOfPrimitives"] as ArrayList) { 167 | assertEquals(10, size) 168 | forEach { 169 | assertEquals(true, it is Double) 170 | } 171 | } 172 | with(player["mapOfPrimitives"] as LinkedHashMap) { 173 | assertEquals(10, size) 174 | forEach { 175 | assertEquals(true, it.value is Double) 176 | } 177 | } 178 | } 179 | 180 | fun assertCell(cell: LinkedHashMap) { 181 | assertEquals(2, cell.keys.size) 182 | assertEquals(100.0, cell["x"]) 183 | assertEquals(200.0, cell["y"]) 184 | } 185 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/mapper/types/MyState.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.mapper.types; 2 | 3 | import io.colyseus.annotations.SchemaField; 4 | import io.colyseus.serializer.schema.Schema 5 | import io.colyseus.serializer.schema.types.ArraySchema 6 | import io.colyseus.serializer.schema.types.MapSchema 7 | import io.colyseus.util.default 8 | 9 | public class MyState : Schema() { 10 | @SchemaField("0/ref", Player::class) 11 | var player = Player() 12 | 13 | @SchemaField("1/array/ref", Player::class) 14 | var arrayOfPlayers = ArraySchema(Player::class.java) 15 | 16 | @SchemaField("2/map/ref", Player::class) 17 | var mapOfPlayers = MapSchema(Player::class.java) 18 | 19 | @SchemaField("3/array/number") 20 | var arrayOfPrimitives = ArraySchema(Float::class.java) 21 | 22 | @SchemaField("4/map/number") 23 | var mapOfPrimitives = MapSchema(Float::class.java) 24 | } 25 | 26 | class Player : Schema() { 27 | @SchemaField("0/uint8") 28 | var _uint8 = Short.default 29 | 30 | @SchemaField("1/uint16") 31 | var _uint16 = Int.default 32 | 33 | @SchemaField("2/uint32") 34 | var _uint32 = Long.default 35 | 36 | @SchemaField("3/uint64") 37 | var _uint64 = Long.default 38 | 39 | @SchemaField("4/int8") 40 | var _int8 = Byte.default 41 | 42 | @SchemaField("5/int16") 43 | var _int16 = Short.default 44 | 45 | @SchemaField("6/int32") 46 | var _int32 = Int.default 47 | 48 | @SchemaField("7/int64") 49 | var _int64 = Long.default 50 | 51 | @SchemaField("8/float32") 52 | var _float32_1 = Float.default 53 | 54 | @SchemaField("9/float32") 55 | var _float32_2 = Float.default 56 | 57 | @SchemaField("10/float64") 58 | var _float64_1 = Double.default 59 | 60 | @SchemaField("11/number") 61 | var _number = Float.default 62 | 63 | @SchemaField("12/float64") 64 | var _float64_2 = Double.default 65 | 66 | @SchemaField("13/boolean") 67 | var _boolean = Boolean.default 68 | 69 | @SchemaField("14/string") 70 | var _string1 = String.default 71 | 72 | @SchemaField("15/string") 73 | var _string2 = String.default 74 | 75 | @SchemaField("16/string") 76 | var _string3 = String.default 77 | 78 | @SchemaField("17/array/ref", Cell::class) 79 | var arrayOfCells = ArraySchema(Cell::class.java) 80 | 81 | @SchemaField("18/map/ref", Cell::class) 82 | var mapOfCells = MapSchema(Cell::class.java) 83 | 84 | @SchemaField("19/array/number") 85 | var arrayOfPrimitives = ArraySchema(Float::class.java) 86 | 87 | @SchemaField("20/map/number") 88 | var mapOfPrimitives = MapSchema(Float::class.java) 89 | } 90 | 91 | class Cell : Schema() { 92 | @SchemaField("0/number") 93 | var x = Float.default 94 | 95 | @SchemaField("1/number") 96 | var y = Float.default 97 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/ArraySchemaTest.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema 2 | 3 | import io.colyseus.serializer.schema.types.ArraySchema 4 | import org.junit.jupiter.api.Assertions.assertEquals 5 | import org.junit.jupiter.api.Test 6 | 7 | class ArraySchemaTest { 8 | @Test 9 | fun `ArraySchema test`() { 10 | fun doSomethingWithArray(array: ArrayList): ArrayList { 11 | return array.apply { 12 | add(3) 13 | add(4000) 14 | add(-100) 15 | removeAt(1) 16 | add(0) 17 | set(0, 200) 18 | remove(200) 19 | set(1, 300) 20 | removeAt(0) 21 | add(-100) 22 | add(400) 23 | } 24 | } 25 | 26 | val arrayList = doSomethingWithArray(ArrayList()) 27 | val arraySchema = doSomethingWithArray(ArraySchema() as ArrayList) 28 | 29 | println(arrayList) 30 | 31 | assertEquals(arrayList.size, arraySchema.size) 32 | 33 | // iterator test 34 | arraySchema.forEachIndexed { index, element -> 35 | assertEquals(element, arrayList[index]) 36 | } 37 | 38 | // get method test 39 | repeat(arrayList.size) { 40 | assertEquals(arraySchema[it], arrayList[it]) 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/SchemaDeserializerTest.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema 2 | 3 | import io.colyseus.serializer.SchemaSerializer 4 | import io.colyseus.serializer.schema.ReferenceTracker 5 | import io.colyseus.util.byteArrayOfInts 6 | import org.junit.jupiter.api.Assertions.assertEquals 7 | import org.junit.jupiter.api.Assertions.assertNotEquals 8 | import org.junit.jupiter.api.Test 9 | 10 | class SchemaDeserializerTest { 11 | @Test 12 | fun `primitive types test`() { 13 | val state = io.colyseus.test.schema.primitive_types.PrimitiveTypes() 14 | val bytes = byteArrayOfInts(128, 128, 129, 255, 130, 0, 128, 131, 255, 255, 132, 0, 0, 0, 128, 133, 255, 255, 255, 255, 134, 0, 0, 0, 0, 0, 0, 0, 128, 135, 255, 255, 255, 255, 255, 255, 31, 0, 136, 204, 204, 204, 253, 137, 255, 255, 255, 255, 255, 255, 239, 127, 138, 208, 128, 139, 204, 255, 140, 209, 0, 128, 141, 205, 255, 255, 142, 210, 0, 0, 0, 128, 143, 203, 0, 0, 224, 255, 255, 255, 239, 65, 144, 203, 0, 0, 0, 0, 0, 0, 224, 195, 145, 203, 255, 255, 255, 255, 255, 255, 63, 67, 146, 203, 61, 255, 145, 224, 255, 255, 239, 199, 147, 203, 153, 153, 153, 153, 153, 153, 185, 127, 148, 171, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 149, 1) 15 | state.decode(bytes) 16 | 17 | assertEquals(-128, state.int8) 18 | assertEquals(255, state.uint8) 19 | assertEquals(-32768, state.int16) 20 | assertEquals(65535, state.uint16) 21 | assertEquals(-2147483648, state.int32) 22 | assertEquals(4294967295, state.uint32) 23 | // assertEquals(-9223372036854775808, state.int64) 24 | assertEquals(9007199254740991, state.uint64) 25 | assertEquals(-3.40282347E+37f, state.float32) 26 | assertEquals(1.7976931348623157e+308, state.float64) 27 | 28 | assertEquals(-128f, state.varint_int8) 29 | assertEquals(255f, state.varint_uint8) 30 | assertEquals(-32768f, state.varint_int16) 31 | assertEquals(65535f, state.varint_uint16) 32 | assertEquals(-2147483648f, state.varint_int32) 33 | assertEquals(4294967295f, state.varint_uint32) 34 | // assertEquals(-9223372036854775808, state.varint_int64) 35 | assertEquals(9007199254740991f, state.varint_uint64) 36 | assertEquals(-3.40282347E+38f, state.varint_float32) 37 | // assertEquals(Mathf.Infinity, state.varint_float64) 38 | 39 | assertEquals("Hello world", state.str) 40 | assertEquals(true, state.boolean) 41 | } 42 | 43 | @Test 44 | fun `child schema types test`() { 45 | val state = io.colyseus.test.schema.child_schema_types.ChildSchemaTypes() 46 | val bytes = byteArrayOfInts(128, 1, 129, 2, 255, 1, 128, 205, 244, 1, 129, 205, 32, 3, 255, 2, 128, 204, 200, 129, 205, 44, 1) 47 | state.decode(bytes) 48 | 49 | assertEquals(500f, state.child.x) 50 | assertEquals(800f, state.child.y) 51 | 52 | assertEquals(200f, state.secondChild.x) 53 | assertEquals(300f, state.secondChild.y) 54 | } 55 | 56 | @Test 57 | fun `array schema types test`() { 58 | val state = io.colyseus.test.schema.array_schema_types.ArraySchemaTypes() 59 | val bytes = byteArrayOfInts(128, 1, 129, 2, 130, 3, 131, 4, 255, 1, 128, 0, 5, 128, 1, 6, 255, 2, 128, 0, 0, 128, 1, 10, 128, 2, 20, 128, 3, 205, 192, 13, 255, 3, 128, 0, 163, 111, 110, 101, 128, 1, 163, 116, 119, 111, 128, 2, 165, 116, 104, 114, 101, 101, 255, 4, 128, 0, 232, 3, 0, 0, 128, 1, 192, 13, 0, 0, 128, 2, 72, 244, 255, 255, 255, 5, 128, 100, 129, 208, 156, 255, 6, 128, 100, 129, 208, 156) 60 | 61 | state.arrayOfSchemas.onAdd = { value, key -> println("onAdd, arrayOfSchemas => $key") } 62 | state.arrayOfNumbers.onAdd = { value, key -> println("onAdd, arrayOfNumbers => $key") } 63 | state.arrayOfStrings.onAdd = { value, key -> println("onAdd, arrayOfStrings => $key") } 64 | state.arrayOfInt32.onAdd = { value, key -> println("onAdd, arrayOfInt32 => $key") } 65 | 66 | val refs = ReferenceTracker() 67 | state.decode(bytes, refs = refs) 68 | 69 | assertEquals(2, state.arrayOfSchemas.size) 70 | assertEquals(100f, state.arrayOfSchemas[0]?.x) 71 | assertEquals(-100f, state.arrayOfSchemas[0]?.y) 72 | assertEquals(100f, state.arrayOfSchemas[1]?.x) 73 | assertEquals(-100f, state.arrayOfSchemas[1]?.y) 74 | 75 | assertEquals(4, state.arrayOfNumbers.size) 76 | assertEquals(0f, state.arrayOfNumbers[0]) 77 | assertEquals(10f, state.arrayOfNumbers[1]) 78 | assertEquals(20f, state.arrayOfNumbers[2]) 79 | assertEquals(3520f, state.arrayOfNumbers[3]) 80 | 81 | assertEquals(3, state.arrayOfStrings.size) 82 | assertEquals("one", state.arrayOfStrings[0]) 83 | assertEquals("two", state.arrayOfStrings[1]) 84 | assertEquals("three", state.arrayOfStrings[2]) 85 | 86 | assertEquals(3, state.arrayOfInt32.size) 87 | assertEquals(1000, state.arrayOfInt32[0]) 88 | assertEquals(3520, state.arrayOfInt32[1]) 89 | assertEquals(-3000, state.arrayOfInt32[2]) 90 | 91 | state.arrayOfSchemas.onRemove = { value, key -> println("onRemove, arrayOfSchemas => $key") } 92 | state.arrayOfNumbers.onRemove = { value, key -> println("onRemove, arrayOfNumbers => $key") } 93 | state.arrayOfStrings.onRemove = { value, key -> println("onRemove, arrayOfStrings => $key") } 94 | state.arrayOfInt32.onRemove = { value, key -> println("onRemove, arrayOfInt32 => $key") } 95 | 96 | val popBytes = byteArrayOfInts(255, 1, 64, 1, 255, 2, 64, 3, 64, 2, 64, 1, 255, 4, 64, 2, 64, 1, 255, 3, 64, 2, 64, 1) 97 | state.decode(popBytes, refs = refs) 98 | 99 | assertEquals(1, state.arrayOfSchemas.size) 100 | assertEquals(1, state.arrayOfNumbers.size) 101 | assertEquals(1, state.arrayOfStrings.size) 102 | assertEquals(1, state.arrayOfInt32.size) 103 | println("FINISHED") 104 | } 105 | 106 | @Test 107 | fun `map schema types test`() { 108 | val state = io.colyseus.test.schema.map_schema_types.MapSchemaTypes() 109 | val bytes = byteArrayOfInts(128, 1, 129, 2, 130, 3, 131, 4, 255, 1, 128, 0, 163, 111, 110, 101, 5, 128, 1, 163, 116, 119, 111, 6, 128, 2, 165, 116, 104, 114, 101, 101, 7, 255, 2, 128, 0, 163, 111, 110, 101, 1, 128, 1, 163, 116, 119, 111, 2, 128, 2, 165, 116, 104, 114, 101, 101, 205, 192, 13, 255, 3, 128, 0, 163, 111, 110, 101, 163, 79, 110, 101, 128, 1, 163, 116, 119, 111, 163, 84, 119, 111, 128, 2, 165, 116, 104, 114, 101, 101, 165, 84, 104, 114, 101, 101, 255, 4, 128, 0, 163, 111, 110, 101, 192, 13, 0, 0, 128, 1, 163, 116, 119, 111, 24, 252, 255, 255, 128, 2, 165, 116, 104, 114, 101, 101, 208, 7, 0, 0, 255, 5, 128, 100, 129, 204, 200, 255, 6, 128, 205, 44, 1, 129, 205, 144, 1, 255, 7, 128, 205, 244, 1, 129, 205, 88, 2) 110 | 111 | state.mapOfSchemas.onAdd = { value, key -> println("OnAdd, mapOfSchemas => $key") } 112 | state.mapOfNumbers.onAdd = { value, key -> println("OnAdd, mapOfNumbers => $key") } 113 | state.mapOfStrings.onAdd = { value, key -> println("OnAdd, mapOfStrings => $key") } 114 | state.mapOfInt32.onAdd = { value, key -> println("OnAdd, mapOfInt32 => $key") } 115 | 116 | state.mapOfSchemas.onRemove = { value, key -> println("OnRemove, mapOfSchemas => $key") } 117 | state.mapOfNumbers.onRemove = { value, key -> println("OnRemove, mapOfNumbers => $key") } 118 | state.mapOfStrings.onRemove = { value, key -> println("OnRemove, mapOfStrings => $key") } 119 | state.mapOfInt32.onRemove = { value, key -> println("OnRemove, mapOfInt32 => $key") } 120 | 121 | val refs = ReferenceTracker() 122 | state.decode(bytes, refs = refs) 123 | 124 | assertEquals(3, state.mapOfSchemas.size) 125 | assertEquals(100f, state.mapOfSchemas["one"]?.x) 126 | assertEquals(200f, state.mapOfSchemas["one"]?.y) 127 | assertEquals(300f, state.mapOfSchemas["two"]?.x) 128 | assertEquals(400f, state.mapOfSchemas["two"]?.y) 129 | assertEquals(500f, state.mapOfSchemas["three"]?.x) 130 | assertEquals(600f, state.mapOfSchemas["three"]?.y) 131 | 132 | assertEquals(3, state.mapOfNumbers.size) 133 | assertEquals(1f, state.mapOfNumbers["one"]) 134 | assertEquals(2f, state.mapOfNumbers["two"]) 135 | assertEquals(3520f, state.mapOfNumbers["three"]) 136 | 137 | assertEquals(3, state.mapOfStrings.size) 138 | assertEquals("One", state.mapOfStrings["one"]) 139 | assertEquals("Two", state.mapOfStrings["two"]) 140 | assertEquals("Three", state.mapOfStrings["three"]) 141 | 142 | assertEquals(3, state.mapOfInt32.size) 143 | assertEquals(3520, state.mapOfInt32["one"]) 144 | assertEquals(-1000, state.mapOfInt32["two"]) 145 | assertEquals(2000, state.mapOfInt32["three"]) 146 | 147 | val deleteBytes = byteArrayOfInts(255, 2, 64, 1, 64, 2, 255, 1, 64, 1, 64, 2, 255, 3, 64, 1, 64, 2, 255, 4, 64, 1, 64, 2) 148 | state.decode(deleteBytes, refs = refs) 149 | 150 | assertEquals(1, state.mapOfSchemas.size) 151 | assertEquals(1, state.mapOfNumbers.size) 152 | assertEquals(1, state.mapOfStrings.size) 153 | assertEquals(1, state.mapOfInt32.size) 154 | } 155 | 156 | @Test 157 | fun `map schema int8 test`() { 158 | val state = io.colyseus.test.schema.map_schema_int8.MapSchemaInt8() 159 | val bytes = byteArrayOfInts(128, 171, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 129, 1, 255, 1, 128, 0, 163, 98, 98, 98, 1, 128, 1, 163, 97, 97, 97, 1, 128, 2, 163, 50, 50, 49, 1, 128, 3, 163, 48, 50, 49, 1, 128, 4, 162, 49, 53, 1, 128, 5, 162, 49, 48, 1) 160 | 161 | val refs = ReferenceTracker() 162 | state.decode(bytes, refs = refs) 163 | 164 | assertEquals("Hello world", state.status) 165 | assertEquals(1, state.mapOfInt8["bbb"]) 166 | assertEquals(1, state.mapOfInt8["aaa"]) 167 | assertEquals(1, state.mapOfInt8["221"]) 168 | assertEquals(1, state.mapOfInt8["021"]) 169 | assertEquals(1, state.mapOfInt8["15"]) 170 | assertEquals(1, state.mapOfInt8["10"]) 171 | 172 | val addBytes = byteArrayOfInts(255, 1, 0, 5, 2) 173 | state.decode(addBytes, refs = refs) 174 | 175 | assertEquals(1, state.mapOfInt8["bbb"]) 176 | assertEquals(1, state.mapOfInt8["aaa"]) 177 | assertEquals(1, state.mapOfInt8["221"]) 178 | assertEquals(1, state.mapOfInt8["021"]) 179 | assertEquals(1, state.mapOfInt8["15"]) 180 | assertEquals(2, state.mapOfInt8["10"]) 181 | } 182 | 183 | @Test 184 | fun `inherited types test`() { 185 | val serializer = SchemaSerializer(io.colyseus.test.schema.inherited_types.InheritedTypes::class.java) 186 | val handshake = byteArrayOfInts(128, 1, 129, 3, 255, 1, 128, 0, 2, 128, 1, 3, 128, 2, 4, 128, 3, 5, 255, 2, 129, 6, 128, 0, 255, 3, 129, 7, 128, 1, 255, 4, 129, 8, 128, 2, 255, 5, 129, 9, 128, 3, 255, 6, 128, 0, 10, 128, 1, 11, 255, 7, 128, 0, 12, 128, 1, 13, 128, 2, 14, 255, 8, 128, 0, 15, 128, 1, 16, 128, 2, 17, 128, 3, 18, 255, 9, 128, 0, 19, 128, 1, 20, 128, 2, 21, 128, 3, 22, 255, 10, 128, 161, 120, 129, 166, 110, 117, 109, 98, 101, 114, 255, 11, 128, 161, 121, 129, 166, 110, 117, 109, 98, 101, 114, 255, 12, 128, 161, 120, 129, 166, 110, 117, 109, 98, 101, 114, 255, 13, 128, 161, 121, 129, 166, 110, 117, 109, 98, 101, 114, 255, 14, 128, 164, 110, 97, 109, 101, 129, 166, 115, 116, 114, 105, 110, 103, 255, 15, 128, 161, 120, 129, 166, 110, 117, 109, 98, 101, 114, 255, 16, 128, 161, 121, 129, 166, 110, 117, 109, 98, 101, 114, 255, 17, 128, 164, 110, 97, 109, 101, 129, 166, 115, 116, 114, 105, 110, 103, 255, 18, 128, 165, 112, 111, 119, 101, 114, 129, 166, 110, 117, 109, 98, 101, 114, 255, 19, 128, 166, 101, 110, 116, 105, 116, 121, 130, 0, 129, 163, 114, 101, 102, 255, 20, 128, 166, 112, 108, 97, 121, 101, 114, 130, 1, 129, 163, 114, 101, 102, 255, 21, 128, 163, 98, 111, 116, 130, 2, 129, 163, 114, 101, 102, 255, 22, 128, 163, 97, 110, 121, 130, 0, 129, 163, 114, 101, 102) 187 | serializer.handshake(handshake, 0) 188 | 189 | val bytes = byteArrayOfInts(128, 1, 129, 2, 130, 3, 131, 4, 213, 2, 255, 1, 128, 205, 244, 1, 129, 205, 32, 3, 255, 2, 128, 204, 200, 129, 205, 44, 1, 130, 166, 80, 108, 97, 121, 101, 114, 255, 3, 128, 100, 129, 204, 150, 130, 163, 66, 111, 116, 131, 204, 200, 255, 4, 131, 100) 190 | serializer.setState(bytes) 191 | 192 | val state = serializer.state 193 | 194 | assertEquals(500f, state.entity.x) 195 | assertEquals(800f, state.entity.y) 196 | 197 | assertEquals(200f, state.player.x) 198 | assertEquals(300f, state.player.y) 199 | assertEquals("Player", state.player.name) 200 | 201 | assertEquals(100f, state.bot.x) 202 | assertEquals(150f, state.bot.y) 203 | assertEquals("Bot", state.bot.name) 204 | assertEquals(200f, state.bot.power) 205 | } 206 | 207 | @Test 208 | fun `backwards forwards test`() { 209 | val statev1bytes = byteArrayOfInts(129, 1, 128, 171, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 255, 1, 128, 0, 163, 111, 110, 101, 2, 255, 2, 128, 203, 232, 229, 22, 37, 231, 231, 209, 63, 129, 203, 240, 138, 15, 5, 219, 40, 223, 63) 210 | val statev2bytes = byteArrayOfInts(128, 171, 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 130, 10) 211 | 212 | val statev2 = io.colyseus.test.schema.backwards_forwards.StateV2() 213 | statev2.decode(statev1bytes) 214 | assertEquals("Hello world", statev2.str) 215 | 216 | val statev1 = io.colyseus.test.schema.backwards_forwards.StateV1() 217 | statev1.decode(statev2bytes) 218 | assertEquals("Hello world", statev1.str) 219 | } 220 | 221 | @Test 222 | fun `filtered types test`() { 223 | val client1 = io.colyseus.test.schema.filtered_types.State() 224 | client1.decode(byteArrayOfInts(255, 0, 130, 1, 128, 2, 128, 2, 255, 1, 128, 0, 4, 255, 2, 128, 163, 111, 110, 101, 255, 2, 128, 163, 111, 110, 101, 255, 4, 128, 163, 111, 110, 101)) 225 | assertEquals("one", client1.playerOne.name) 226 | assertEquals("one", client1.players[0]?.name) 227 | assertEquals(null, client1.playerTwo.name) 228 | 229 | val client2 = io.colyseus.test.schema.filtered_types.State() 230 | client2.decode(byteArrayOfInts(255, 0, 130, 1, 129, 3, 129, 3, 255, 1, 128, 1, 5, 255, 3, 128, 163, 116, 119, 111, 255, 3, 128, 163, 116, 119, 111, 255, 5, 128, 163, 116, 119, 111)) 231 | assertEquals("two", client2.playerTwo.name) 232 | assertEquals("two", client2.players[0]?.name) 233 | assertEquals(null, client2.playerOne.name) 234 | } 235 | 236 | @Test 237 | fun `instance sharing types test`() { 238 | val refs = ReferenceTracker() 239 | val client = io.colyseus.test.schema.instance_sharing_types.State() 240 | 241 | client.decode(byteArrayOfInts(130, 1, 131, 2, 128, 3, 129, 3, 255, 1, 255, 2, 255, 3, 128, 4, 255, 3, 128, 4, 255, 4, 128, 10, 129, 10, 255, 4, 128, 10, 129, 10), refs = refs) 242 | assertEquals(client.player1, client.player2) 243 | assertEquals(client.player1.position, client.player2.position) 244 | assertEquals(2, refs.refCounts[client.player1.__refId]) 245 | assertEquals(5, refs.refs.size) 246 | 247 | client.decode(byteArrayOfInts(130, 1, 131, 2, 64, 65), refs = refs) 248 | assertEquals(null, client.player1) 249 | assertEquals(null, client.player2) 250 | assertEquals(3, refs.refs.size) 251 | 252 | client.decode(byteArrayOfInts(255, 1, 128, 0, 5, 128, 1, 5, 128, 2, 5, 128, 3, 6, 255, 5, 128, 7, 255, 6, 128, 8, 255, 7, 128, 10, 129, 10, 255, 8, 128, 10, 129, 10), refs = refs) 253 | assertEquals(client.arrayOfPlayers[0], client.arrayOfPlayers[1]) 254 | assertEquals(client.arrayOfPlayers[1], client.arrayOfPlayers[2]) 255 | assertNotEquals(client.arrayOfPlayers[3], client.arrayOfPlayers[2]) 256 | assertEquals(7, refs.refs.size) 257 | 258 | client.decode(byteArrayOfInts(255, 1, 64, 3, 64, 2, 64, 1), refs = refs) 259 | assertEquals(1, client.arrayOfPlayers.size) 260 | assertEquals(5, refs.refs.size) 261 | val previousArraySchemaRefId = client.arrayOfPlayers.__refId 262 | 263 | // Replacing ArraySchema 264 | client.decode(byteArrayOfInts(130, 9, 255, 9, 128, 0, 10, 255, 10, 128, 11, 255, 11, 128, 10, 129, 20), refs = refs) 265 | assertEquals(false, refs.refs.containsKey(previousArraySchemaRefId)) 266 | assertEquals(1, client.arrayOfPlayers.size) 267 | assertEquals(5, refs.refs.size) 268 | 269 | // Clearing ArraySchema 270 | client.decode(byteArrayOfInts(255, 9, 10), refs = refs) 271 | assertEquals(0, client.arrayOfPlayers.size) 272 | assertEquals(3, refs.refs.size) 273 | 274 | } 275 | 276 | @Test 277 | fun `map schema move nullify type test`(){ 278 | val state = io.colyseus.test.schema.map_schema_move_nullify_type.State() 279 | val bytes = byteArrayOfInts(129, 1, 64, 255, 1, 128, 0, 161, 48, 0) 280 | 281 | val refs = ReferenceTracker() 282 | state.decode(bytes, null, refs) 283 | 284 | // Assert.DoesNotThrow 285 | // FIXME: this test only passes because empty 286 | val moveAndNullifyBytes = byteArrayOfInts(128, 1, 65) 287 | state.decode(moveAndNullifyBytes, null, refs) 288 | } 289 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/array_schema_types/ArraySchemaTypes.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.array_schema_types 2 | 3 | 4 | import io.colyseus.annotations.SchemaField 5 | import io.colyseus.serializer.schema.Schema 6 | import io.colyseus.serializer.schema.types.ArraySchema 7 | 8 | 9 | class ArraySchemaTypes : Schema() { 10 | @SchemaField("0/array/ref", IAmAChild::class) 11 | public var arrayOfSchemas = ArraySchema(IAmAChild::class.java) 12 | 13 | @SchemaField("1/array/number") 14 | public var arrayOfNumbers = ArraySchema(Float::class.java) 15 | 16 | @SchemaField("2/array/string") 17 | public var arrayOfStrings = ArraySchema(String::class.java) 18 | 19 | @SchemaField("3/array/int32") 20 | public var arrayOfInt32 = ArraySchema(Int::class.java) 21 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/array_schema_types/IAmAChild.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.array_schema_types 2 | 3 | 4 | import io.colyseus.annotations.SchemaField 5 | import io.colyseus.serializer.schema.Schema 6 | import io.colyseus.util.default 7 | 8 | 9 | class IAmAChild : Schema() { 10 | @SchemaField("0/number") 11 | public var x: Float = Float.default 12 | 13 | @SchemaField("1/number") 14 | public var y: Float = Float.default 15 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/backwards_forwards/PlayerV1.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.backwards_forwards 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | import io.colyseus.util.default 6 | 7 | class PlayerV1 : Schema() { 8 | @SchemaField("0/number") 9 | var x = Float.default 10 | 11 | @SchemaField("1/number") 12 | var y = Float.default 13 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/backwards_forwards/PlayerV2.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.backwards_forwards 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | import io.colyseus.serializer.schema.types.ArraySchema 6 | import io.colyseus.util.default 7 | 8 | class PlayerV2 : Schema() { 9 | @SchemaField("0/number") 10 | var x = Float.default 11 | 12 | @SchemaField("1/number") 13 | var y = Float.default 14 | 15 | @SchemaField("2/string") 16 | var name = String.default 17 | 18 | @SchemaField("3/array/string") 19 | var arrayOfStrings = ArraySchema(String::class.java) 20 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/backwards_forwards/StateV1.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.backwards_forwards 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | import io.colyseus.serializer.schema.types.MapSchema 6 | import io.colyseus.util.default 7 | 8 | class StateV1 : Schema() { 9 | @SchemaField("0/string") 10 | var str = String.default 11 | 12 | @SchemaField("1/map/ref", PlayerV1::class) 13 | var map = MapSchema(PlayerV1::class.java) 14 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/backwards_forwards/StateV2.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.backwards_forwards 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | import io.colyseus.serializer.schema.types.MapSchema 6 | import io.colyseus.util.default 7 | 8 | class StateV2 : Schema() { 9 | @SchemaField("0/string") 10 | var str = String.default 11 | 12 | @SchemaField("1/map/ref", PlayerV2::class) 13 | var map = MapSchema(PlayerV1::class.java) 14 | 15 | @SchemaField("2/number") 16 | var countdown = Float.default 17 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/child_schema_types/ChildSchemaTypes.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.child_schema_types 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | 6 | 7 | class ChildSchemaTypes : Schema() { 8 | @SchemaField("0/ref", IAmAChild::class) 9 | public var child = IAmAChild() 10 | 11 | @SchemaField("1/ref", IAmAChild::class) 12 | public var secondChild = IAmAChild() 13 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/child_schema_types/IAmAChild.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.child_schema_types 2 | 3 | 4 | import io.colyseus.annotations.SchemaField 5 | import io.colyseus.serializer.schema.Schema 6 | import io.colyseus.util.default 7 | 8 | 9 | class IAmAChild : Schema() { 10 | @SchemaField("0/number") 11 | public var x: Float = Float.default 12 | 13 | @SchemaField("1/number") 14 | public var y: Float = Float.default 15 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/filtered_types/Player.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.filtered_types 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | import io.colyseus.util.default 6 | 7 | class Player : Schema() { 8 | @SchemaField("0/string") 9 | var name = String.default 10 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/filtered_types/State.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.filtered_types 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | import io.colyseus.serializer.schema.types.ArraySchema 6 | 7 | class State : Schema() { 8 | @SchemaField("0/ref", Player::class) 9 | var playerOne = Player() 10 | 11 | @SchemaField("1/ref", Player::class) 12 | var playerTwo = Player() 13 | 14 | @SchemaField("2/array/ref", Player::class) 15 | var players = ArraySchema(Player::class.java) 16 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/inherited_types/Bot.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.inherited_types 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.util.default 5 | 6 | class Bot : Player() { 7 | @SchemaField("3/number") 8 | var power = Float.default 9 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/inherited_types/Entity.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.inherited_types 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | import io.colyseus.util.default 6 | 7 | open class Entity : Schema() { 8 | @SchemaField("0/number") 9 | var x = Float.default 10 | 11 | @SchemaField("1/number") 12 | var y = Float.default 13 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/inherited_types/InheritedTypes.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.inherited_types 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | 6 | class InheritedTypes : Schema() { 7 | @SchemaField("0/ref", Entity::class) 8 | var entity = Entity() 9 | 10 | @SchemaField("1/ref", Player::class) 11 | var player = Player() 12 | 13 | @SchemaField("2/ref", Bot::class) 14 | var bot = Bot() 15 | 16 | @SchemaField("3/ref", Entity::class) 17 | var any = Entity() 18 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/inherited_types/Player.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.inherited_types 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.util.default 5 | 6 | open class Player : Entity() { 7 | @SchemaField("2/string") 8 | var name = String.default 9 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/instance_sharing_types/Player.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.instance_sharing_types 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | 6 | class Player : Schema() { 7 | @SchemaField("0/ref", Position::class) 8 | var position = Position() 9 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/instance_sharing_types/Position.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.instance_sharing_types 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | import io.colyseus.util.default 6 | 7 | class Position : Schema() { 8 | @SchemaField("0/number") 9 | var x = Float.default 10 | 11 | @SchemaField("1/number") 12 | var y = Float.default 13 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/instance_sharing_types/State.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.instance_sharing_types 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | import io.colyseus.serializer.schema.types.ArraySchema 6 | import io.colyseus.serializer.schema.types.MapSchema 7 | 8 | class State : Schema() { 9 | @SchemaField("0/ref", Player::class) 10 | var player1 = Player() 11 | 12 | @SchemaField("1/ref", Player::class) 13 | var player2 = Player() 14 | 15 | @SchemaField("2/array/ref", Player::class) 16 | var arrayOfPlayers = ArraySchema(Player::class.java) 17 | 18 | @SchemaField("3/map/ref", Player::class) 19 | var mapOfPlayers = MapSchema(Player::class.java) 20 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/map_schema_int8/MapSchemaInt8.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.map_schema_int8 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | import io.colyseus.serializer.schema.types.MapSchema 6 | import io.colyseus.util.default 7 | 8 | class MapSchemaInt8 : Schema() { 9 | @SchemaField("0/string") 10 | var status = String.default 11 | 12 | @SchemaField("1/map/int8") 13 | var mapOfInt8 = MapSchema(Byte::class.java) 14 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/map_schema_move_nullify_type/State.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.map_schema_move_nullify_type 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | import io.colyseus.serializer.schema.types.MapSchema 6 | import io.colyseus.test.schema.map_schema_types.IAmAChild 7 | 8 | class State : Schema() { 9 | @SchemaField("0/map/number") 10 | var previous = MapSchema(Float::class.java) 11 | 12 | @SchemaField("1/map/number") 13 | var current = MapSchema(Float::class.java) 14 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/map_schema_types/IAmAChild.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.map_schema_types 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | import io.colyseus.util.default 6 | 7 | 8 | class IAmAChild : Schema() { 9 | @SchemaField("0/number") 10 | public var x: Float = Float.default 11 | 12 | @SchemaField("1/number") 13 | public var y: Float = Float.default 14 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/map_schema_types/MapSchemaTypes.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.map_schema_types 2 | 3 | 4 | import io.colyseus.annotations.SchemaField 5 | import io.colyseus.serializer.schema.Schema 6 | import io.colyseus.serializer.schema.types.MapSchema 7 | 8 | 9 | class MapSchemaTypes : Schema() { 10 | @SchemaField("0/map/ref", IAmAChild::class) 11 | public var mapOfSchemas = MapSchema(IAmAChild::class.java) 12 | 13 | @SchemaField("1/map/number") 14 | public var mapOfNumbers = MapSchema(Float::class.java) 15 | 16 | @SchemaField("2/map/string") 17 | public var mapOfStrings = MapSchema(String::class.java) 18 | 19 | @SchemaField("3/map/int32") 20 | public var mapOfInt32 = MapSchema(Int::class.java) 21 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/colyseus/test/schema/primitive_types/PrimitiveTypes.kt: -------------------------------------------------------------------------------- 1 | package io.colyseus.test.schema.primitive_types 2 | 3 | import io.colyseus.annotations.SchemaField 4 | import io.colyseus.serializer.schema.Schema 5 | import io.colyseus.util.default 6 | 7 | class PrimitiveTypes : Schema() { 8 | 9 | @SchemaField("0/int8") 10 | public var int8: Byte = Byte.default 11 | 12 | @SchemaField("1/uint8") 13 | public var uint8: Short = Short.default 14 | 15 | @SchemaField("2/int16") 16 | public var int16: Short = Short.default 17 | 18 | @SchemaField("3/uint16") 19 | public var uint16: Int = Int.default 20 | 21 | @SchemaField("4/int32") 22 | public var int32: Int = Int.default 23 | 24 | @SchemaField("5/uint32") 25 | public var uint32: Long = Long.default 26 | 27 | @SchemaField("6/int64") 28 | public var int64: Long = Long.default 29 | 30 | @SchemaField("7/uint64") 31 | public var uint64: Long = Long.default 32 | 33 | @SchemaField("8/float32") 34 | public var float32: Float = Float.default 35 | 36 | @SchemaField("9/float64") 37 | public var float64: Double = Double.default 38 | 39 | @SchemaField("10/number") 40 | public var varint_int8: Float = Float.default 41 | 42 | @SchemaField("11/number") 43 | public var varint_uint8: Float = Float.default 44 | 45 | @SchemaField("12/number") 46 | public var varint_int16: Float = Float.default 47 | 48 | @SchemaField("13/number") 49 | public var varint_uint16: Float = Float.default 50 | 51 | @SchemaField("14/number") 52 | public var varint_int32: Float = Float.default 53 | 54 | @SchemaField("15/number") 55 | public var varint_uint32: Float = Float.default 56 | 57 | @SchemaField("16/number") 58 | public var varint_int64: Float = Float.default 59 | 60 | @SchemaField("17/number") 61 | public var varint_uint64: Float = Float.default 62 | 63 | @SchemaField("18/number") 64 | public var varint_float32: Float = Float.default 65 | 66 | @SchemaField("19/number") 67 | public var varint_float64: Float = Float.default 68 | 69 | @SchemaField("20/string") 70 | public var str: String? = String.default 71 | 72 | @SchemaField("21/boolean") 73 | public var boolean: Boolean = Boolean.default 74 | } 75 | --------------------------------------------------------------------------------