├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── buildSrc ├── build.gradle ├── settings.gradle └── src │ └── main │ └── groovy │ ├── sim-ethereal.java-conventions.gradle │ └── sim-ethereal.published-library.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── license.md ├── release-notes.md ├── settings.gradle └── src └── main ├── java └── com │ └── simsilica │ ├── ethereal │ ├── ConnectionStats.java │ ├── DebugUtils.java │ ├── EtherealClient.java │ ├── EtherealHost.java │ ├── IdIndex.java │ ├── LocalZoneIndex.java │ ├── NanoTimeSource.java │ ├── NetworkStateListener.java │ ├── SharedObject.java │ ├── SharedObjectListener.java │ ├── SharedObjectSpace.java │ ├── Statistics.java │ ├── SynchedTimeSource.java │ ├── TimeSource.java │ ├── io │ │ ├── BitInputStream.java │ │ ├── BitOutputStream.java │ │ └── BitStreamTester.java │ ├── net │ │ ├── ClientStateMessage.java │ │ ├── FrameState.java │ │ ├── ObjectState.java │ │ ├── ObjectStateMessage.java │ │ ├── ObjectStateProtocol.java │ │ ├── RemoteTimeSource.java │ │ ├── SentState.java │ │ ├── StateReceiver.java │ │ └── StateWriter.java │ └── zone │ │ ├── StateBlock.java │ │ ├── StateCollector.java │ │ ├── StateFrame.java │ │ ├── StateListener.java │ │ ├── Zone.java │ │ ├── ZoneGrid.java │ │ ├── ZoneKey.java │ │ └── ZoneManager.java │ └── util │ └── BufferedHashSet.java └── resources └── license.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | # Edit backup directories 26 | .backups/ 27 | 28 | # gradle related directories 29 | .gradle/ 30 | build/ 31 | 32 | # We shouldn't be checking in DLLs but we will find them in our JME source 33 | # try all the time... best to skip them 34 | *.dll 35 | *.so 36 | 37 | # In our project, logs directories are generated by the app and we never 38 | # want to check them in. 39 | logs/ 40 | 41 | # My own back directories 42 | *.backup*/ 43 | 44 | # Screen shot files 45 | Main[0-9]*.png 46 | 47 | # Windows poop 48 | Thumbs.db 49 | 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2015, Simsilica, LLC 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its contributors 17 | may be used to endorse or promote products derived from this software without 18 | specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimEthereal 2 | 3 | ![SimEthereal Logo](http://imgur.com/MV1WjxT.png) 4 | A high performance library for real-time networked object synching. 5 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /** 2 | * Root-level SimEthereal build script. 3 | */ 4 | 5 | plugins { 6 | id 'sim-ethereal.published-library' 7 | } 8 | 9 | version='1.8.1-SNAPSHOT' 10 | group='com.simsilica' 11 | 12 | ext.jmeVersion = "3.1.0-stable" 13 | ext.slf4jVersion = '1.7.32' 14 | ext.simMathVersion = '1.4.0' 15 | 16 | // Set this module's maven pom description 17 | publishing.publications.library(MavenPublication).pom { 18 | description = 'A real-time network sync library in Java.' 19 | } 20 | 21 | dependencies { 22 | // Based on JME networking 23 | api "org.jmonkeyengine:jme3-networking:$jmeVersion" 24 | 25 | // Uses SimMath for bit streaming utilities and transition buffers 26 | api "com.simsilica:sim-math:$simMathVersion" 27 | 28 | // Base logging 29 | implementation "org.slf4j:slf4j-api:$slf4jVersion" 30 | } 31 | 32 | compileJava.doLast { 33 | def buildDate = new Date().format('yyyyMMdd') 34 | println "Writing sim-ethereal.build.date:" + buildDate 35 | new File(destinationDir, 'sim-ethereal.build.date').text = buildDate 36 | } 37 | 38 | -------------------------------------------------------------------------------- /buildSrc/build.gradle: -------------------------------------------------------------------------------- 1 | /** 2 | * Build file for the buildSrc convention plugins. 3 | * buildSrc is a special gradle sub-project that builds local 4 | * convention plugins for use in the regular project build 5 | * files. 6 | * See: 7 | * https://docs.gradle.org/current/userguide/sharing_build_logic_between_subprojects.html 8 | * https://docs.gradle.org/current/samples/sample_convention_plugins.html 9 | */ 10 | 11 | plugins { 12 | id 'groovy-gradle-plugin' 13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /buildSrc/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='sim-ethereal-conventions' 2 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/sim-ethereal.java-conventions.gradle: -------------------------------------------------------------------------------- 1 | /** 2 | * Conventions for all sim-ethereal java-based modules. 3 | */ 4 | 5 | plugins { 6 | id 'java' 7 | //id 'checkstyle' 8 | } 9 | 10 | // Projects should use Maven Central for external dependencies 11 | repositories { 12 | mavenLocal() 13 | mavenCentral() 14 | } 15 | 16 | //checkstyle { 17 | // config = ... 18 | // maxWarnings = 0 19 | //} 20 | 21 | compileJava { // compile-time options: 22 | options.encoding = 'UTF-8' 23 | options.compilerArgs << '-Xlint:unchecked' 24 | options.deprecation = true 25 | if( JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_1_10) ) { 26 | options.release = 7 27 | } 28 | } 29 | 30 | java { 31 | sourceCompatibility = 1.7 32 | targetCompatibility = 1.7 33 | withJavadocJar() 34 | withSourcesJar() 35 | } 36 | 37 | javadoc { 38 | // Disable doclint for JDK8+. 39 | if( JavaVersion.current().isJava8Compatible() ) { 40 | options.addStringOption('Xdoclint:none', '-quiet') 41 | } 42 | } 43 | 44 | test { 45 | testLogging { 46 | // I want to see the tests that are run and pass, etc. 47 | events "passed", "skipped", "failed", "standardOut", "standardError" 48 | } 49 | } 50 | 51 | sourceSets { 52 | main { 53 | resources { 54 | exclude "**/.backups/**" 55 | } 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/sim-ethereal.published-library.gradle: -------------------------------------------------------------------------------- 1 | /** 2 | * Conventions for all sim-ethereal maven-published modules. 3 | */ 4 | 5 | plugins { 6 | id 'java-library' 7 | id 'maven-publish' 8 | id 'sim-ethereal.java-conventions' 9 | id 'signing' 10 | } 11 | 12 | group = 'com.simsilica' 13 | 14 | publishing { 15 | publications { 16 | library(MavenPublication) { 17 | from components.java 18 | pom { 19 | developers { 20 | developer { 21 | name = 'Paul Speed' 22 | } 23 | } 24 | inceptionYear = '2011' 25 | licenses { 26 | license { 27 | distribution = 'repo' 28 | name = 'New BSD (3-clause) License' 29 | url = 'https://github.com/Simsilica/SimEthereal/blob/master/license.md' 30 | } 31 | } 32 | name = project.group + ':' + project.name 33 | scm { 34 | connection = 'scm:git:git://github.com/Simsilica/SimEthereal.git' 35 | developerConnection = 'scm:git:ssh://github.com:Simsilica/SimEthereal.git' 36 | url = 'https://github.com/Simsilica/SimEthereal/tree/master' 37 | } 38 | url = 'https://github.com/Simsilica/SimEthereal' 39 | } 40 | } 41 | } 42 | 43 | // Staging to OSSRH relies on the existence of 2 properties 44 | // (ossrhUsername and ossrhPassword) 45 | // which should be stored in ~/.gradle/gradle.properties 46 | repositories { 47 | maven { 48 | credentials { 49 | username = project.hasProperty('ossrhUsername') ? ossrhUsername : 'Unknown user' 50 | password = project.hasProperty('ossrhPassword') ? ossrhPassword : 'Unknown password' 51 | } 52 | name = 'OSSRH' 53 | 54 | def releasesRepoUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2' 55 | def snapshotsRepoUrl = 'https://oss.sonatype.org/content/repositories/snapshots' 56 | 57 | // Have to evaluate the project version late because when the conventions 58 | // are configured the project build file hasn't set the version yet. 59 | afterEvaluate { 60 | url = project.version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl 61 | } 62 | } 63 | } 64 | } 65 | 66 | 67 | tasks.register('install') { 68 | dependsOn 'publishToMavenLocal' 69 | description 'Installs Maven artifacts to the local repository.' 70 | } 71 | 72 | // signing tasks 73 | // Signing relies on the existence of 3 properties 74 | // (signing.keyId, signing.password, and signing.secretKeyRingFile) 75 | // which should be stored in ~/.gradle/gradle.properties 76 | 77 | signing { 78 | sign publishing.publications 79 | } 80 | tasks.withType(Sign) { 81 | onlyIf { project.hasProperty('signing.keyId') } 82 | } 83 | 84 | // Customize some tasks 85 | tasks.sourcesJar { 86 | exclude "**/.backups/**" 87 | } 88 | 89 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simsilica/SimEthereal/555d511fc177ab4976aefc0ecd7598e90290409d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2015, Simsilica, LLC 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in 15 | the documentation and/or other materials provided with the 16 | distribution. 17 | 18 | 3. Neither the name of the copyright holder nor the names of its 19 | contributors may be used to endorse or promote products derived 20 | from this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 27 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 28 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 29 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 30 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 31 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 32 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 33 | OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | 36 | -------------------------------------------------------------------------------- /release-notes.md: -------------------------------------------------------------------------------- 1 | Version 1.8.1 (unreleased) 2 | -------------- 3 | * Updated BitOutputStream and BitInputStream to be AutoCloseable 4 | * Updated StateReceiving to only log the full stale state if trace 5 | logging is on. Also changed to info because it's not really a sign 6 | of a big problem unless it happens a lot. 7 | * Fixed an NPE in SharedObject.getWorldPosition() when called before 8 | the object has a zone. Now it will return null in this case and 9 | the caller can deal with the issue. 10 | * Downgraded the "Removing old unmatched message" WARN log to INFO. 11 | 12 | 13 | Version 1.8.0 (latest) 14 | -------------- 15 | * fixed ObjectStateMessages to actually be UDP. 16 | * fixed a bug in how the ping time was calculated by modifying 17 | ClientStateMessage.resetReceivedTime() to take a timestamp instead of 18 | getting it directly from System.nanoTime(). 19 | * Converted a println() in ZoneManager to a log.warn() related to exceeding 20 | history size watchdog threshold. 21 | * Fixed a long-standing bug related to objects moving so far that none of 22 | their old zones are valid anymore. This left the old StateListener 23 | stranded if the object was what gave the state listener its 'center'. 24 | * Added StateListener.objectWarped() and the NetworkStateListener.objectWarped() 25 | implementation to fix the bug mentioned above. This is a breaking change 26 | for any apps implementing their own StateListener. 27 | 28 | 29 | Version 1.7.0 30 | -------------- 31 | * fixed an ID comparison bug in some watchdog code 32 | * Upgraded the build to use gradle 7.4.2 33 | * Migrated publishing to maven central instead of jcenter 34 | 35 | 36 | Version 1.6.0 37 | -------------- 38 | * Added parent/child relationships between network shared objects. 39 | * Fixed a bug in the assertion logic in BufferedHashSet's thread check. 40 | 41 | 42 | Version 1.5.0 43 | -------------- 44 | * Internally moved ZoneManager creation to the EtherealHost constructor 45 | so that it could be accessed prior to EtherealHost service initialization. 46 | * Refactored ZoneManager to allow different internal ZoneRange implementations. 47 | Renamed the old internal ZoneRange to OctZoneRange to denote that it can 48 | track a maximum of eight zones per object (2x2x2). This limited objects 49 | to never be bigger than a zone. 50 | * Modified ZoneManager to have a new internal DynamicZoneRange that can 51 | support objects of virtually any size relative to zone size. (Subject 52 | to positional bit resolution, etc.) 53 | * Added a (hopefully temporary) ZoneManager.setSupportLargeObjects() that 54 | defaults to false. When true this will use the new DynamicZoneRange. 55 | This defaults to false because the older (uglier) ZoneRange code has 56 | had a LOT more real world testing. Note: the new code is actually way 57 | cleaner and more elegant. 58 | * Modified the "received acks" watchdog in StateWriter to take message ID lag 59 | into account. This should fix the cases where the exception would be 60 | thrown for cases where client ACKs are just lagging by a wide margin. 61 | * Upgraded to SimMath 1.4.0 to get the new IntRangeSet for receved acks tracking. 62 | * Converted the tracked ACK message ID sets to IntRangeSets for efficient storage 63 | and processing. (During normal processing, ACK IDs will almost always be 64 | a single contiguous range so a good candidate for IntRangeSet: one entry) 65 | * Fixed a bug in SentState message writing/receiving where more than 255 ACK 66 | IDs was causing overflow and randomly lost acks. (Fixed by sending ranges 67 | instead of every individual ACK set.) This also fixed a message size 68 | overflow issue. 69 | 70 | 71 | Version 1.4.0 72 | -------------- 73 | * Fixed zone ID calculation for non-uniform grids. See PR #2. 74 | * Modified ZoneManager to automatically send "no-change" updates for 75 | objects it is managing but didn't receive updates for. See PR #5 76 | (Note: this could seem like a 'breaking change' to any apps relying 77 | on objects to auto-expire in this way... just know that doing so was 78 | leaving extra garbage around and so not really a solution.) 79 | * Added a thread-safe BufferedHashSet for creating "one writer, many readers" 80 | fast thread safe hash sets. 81 | * Added a double-buffered thread safe active IDs set to the NetworkStateListener. 82 | * Added an error log message when updating an object with a bounds bigger than 83 | supported by the current grid settings. 84 | 85 | 86 | Version 1.3.0 87 | -------------- 88 | * Fixed a bug where the newer state messages would fail if the game hadn't 89 | already registered Vec3d as a serializable class. 90 | * Fixed NetworkStateListener to properly pay attention to zone extents. 91 | * Fixed EtherealHost to pass the specified client zone extents on to the 92 | NetworkStateListener. 93 | * Added additional trace logging to NetworkStateListener. 94 | * Fixed an issue where raw nanoTime() was being used to timestamp messages. 95 | Now applications can specify their own TimeSource on the server. 96 | See issue #4. 97 | * Refactored the TimeSource interface to be just a time provider and added 98 | a SynchedTimeSource interface to provide the extra drift/offset methods 99 | that it previously had. RemoteTimeSource now implements SynchedTimeSource. 100 | * Upgraded to sim-math 1.2.0 101 | * Set sourceCompatibility to 1.7 102 | 103 | 104 | Version 1.2.1 105 | -------------- 106 | * Added EtherealHost.setStateCollectionInterval() to configure how often 107 | state is retrieved from the ZoneManager and sent to the clients. 108 | * Added TimeSource.set/getOffset() to make it easier for clients to configure 109 | how far in history they'd like time to represent. Defaults to -100 ms. 110 | * Expanded ZoneManager's javadoc. 111 | * Made StateCollector's idle sleep time configurable and exposed it as 112 | an EtherealHost property. 113 | * Flipped the StateCollector's update loop to sleep when idle instead of 114 | only after a valid state collection was made. 115 | * Added a ConnectionStats object to the NetworkStateListener that currently collects 116 | the average ping time, percentage of missed ACKs, and the average message size. 117 | * Modified the StateReceiver to add message size stats to a Statistics tracker. This 118 | let's clients see the average and total message sizes. 119 | * Modified the build.gradle to replace the JME version with a specific 120 | version instead of letting it float. I think alpha4 is generally 121 | the minimum accepted 3.1 version at this point. 122 | Did the same for all of the floating version references. 123 | 124 | 125 | Version 1.1.1 126 | -------------- 127 | * Added an EtherealHost.setConnectionObject() to make it easier 128 | to set the connections object ID and initial position. The older 129 | way required some tighter coupling to Ethereal internals. 130 | Old way to start hosting on a connection: 131 | getService(EtherealHost.class).startHostingOnConnection(conn); 132 | getService(EtherealHost.class).getStateListener(conn).setSelf(ship.getId(), new Vec3d()); 133 | New way: 134 | getService(EtherealHost.class).startHostingOnConnection(conn); 135 | getService(EtherealHost.class).setConnectionObject(conn, ship.getId(), new Vec3d()) 136 | * Improved an exception message on LocalZoneIndex's out of bounds checking. 137 | * Improved an exception message in FrameStat's split() regarding a failure to split. The message 138 | now includes the caller specified limit so we can see if it's even a sane value in these 139 | error cases. 140 | * Attempted fix for a message split bug (untested). Based on the logic, there were some 141 | book-keeping items not properly updated which could lead to exactly the error cases 142 | reported. Hopefully fixing those calculations fixes the problem but I've also left 143 | a ton of comments in place. 144 | * Added a StateWriter.get/setMaxMessageSize() method(s) for controlling the MTU size used 145 | to calculate when messages are split. So even if the above fix doesn't work then callers 146 | can effectively turn off the split behavior by setting a huge max message size. 147 | * Exposed get/setMaxMessageSize() at the NetworkStateListener level so that application code 148 | might set this value on its own, or even dynamically, based on application-specific 149 | requirements. 150 | 151 | 152 | Version 1.0.1 153 | -------------- 154 | * Initial public release with maven artifacts 155 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'sim-ethereal' 2 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/ConnectionStats.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2016, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal; 38 | 39 | import java.util.concurrent.atomic.AtomicLong; 40 | 41 | /** 42 | * Keeps track of server-side statistics for a particular 43 | * HostedConnection. 44 | * 45 | * @author Paul Speed 46 | */ 47 | public class ConnectionStats { 48 | 49 | private RollingAverage ping = new RollingAverage(5); 50 | private RollingAverage messageSize = new RollingAverage(5); 51 | private MissCounter acks = new MissCounter(); 52 | 53 | public ConnectionStats() { 54 | } 55 | 56 | public final void addPingTime( long time ) { 57 | ping.add(time); 58 | } 59 | 60 | public long getAveragePingTime() { 61 | return ping.average.get(); 62 | } 63 | 64 | public final void addMessageSize( long size ) { 65 | messageSize.add(size); 66 | } 67 | 68 | public long getAverageMessageSize() { 69 | return messageSize.average.get(); 70 | } 71 | 72 | public long getTotalMessageBytes() { 73 | return messageSize.total.get(); 74 | } 75 | 76 | public final void incrementAcks() { 77 | acks.incrementTotal(); 78 | } 79 | 80 | public final void incrementAckMisses() { 81 | acks.incrementMisses(); 82 | } 83 | 84 | public final double getAckMissPercent() { 85 | return acks.percentTimes10.get() / 10.0; 86 | } 87 | 88 | private class RollingAverage { 89 | private int windowSize; 90 | private int count; 91 | private long accumulator; 92 | private AtomicLong average = new AtomicLong(); 93 | private AtomicLong total = new AtomicLong(); 94 | 95 | public RollingAverage( int windowSize ) { 96 | this.windowSize = windowSize; 97 | } 98 | 99 | public void add( long value ) { 100 | long size = Math.min(count, windowSize); 101 | count++; 102 | long roll = (accumulator * size + value) / (size + 1); 103 | average.set(roll); 104 | total.addAndGet(value); 105 | } 106 | } 107 | 108 | private class MissCounter { 109 | private long total; 110 | private long misses; 111 | private AtomicLong percentTimes10 = new AtomicLong(); 112 | 113 | public final void incrementTotal() { 114 | total++; 115 | percentTimes10.set(misses * 1000 / total); 116 | } 117 | 118 | public final void incrementMisses() { 119 | misses++; 120 | percentTimes10.set(misses * 1000 / total); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/DebugUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal; 38 | 39 | import java.text.DecimalFormat; 40 | import java.text.NumberFormat; 41 | 42 | 43 | /** 44 | * 45 | * 46 | * @author Paul Speed 47 | */ 48 | public class DebugUtils { 49 | 50 | private static String nanoFormat = "#,### nanos"; 51 | private static String msFormat = "#,###.## ms"; 52 | private static ThreadLocal nano = new ThreadLocal() { 53 | @Override 54 | protected NumberFormat initialValue() { 55 | return new DecimalFormat(nanoFormat); 56 | } 57 | }; 58 | private static ThreadLocal ms = new ThreadLocal() { 59 | @Override 60 | protected NumberFormat initialValue() { 61 | return new DecimalFormat(msFormat); 62 | } 63 | }; 64 | 65 | public static String nanoString( long nanos ) { 66 | return nano.get().format(nanos); 67 | } 68 | 69 | public static String msString( long nanos ) { 70 | return ms.get().format(nanos / 1000000.0); 71 | } 72 | 73 | public static String timeString( long nanos ) { 74 | return nanoString(nanos) + " (" + msString(nanos) + ")"; 75 | } 76 | 77 | public static void main( String... args ) { 78 | long time = System.nanoTime(); 79 | System.out.println("Nano test:" + nanoString(time)); 80 | System.out.println("MS test:" + msString(time)); 81 | System.out.println("Time test:" + timeString(time)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/EtherealClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal; 38 | 39 | import com.jme3.network.Client; 40 | import com.jme3.network.service.AbstractClientService; 41 | import com.jme3.network.service.ClientServiceManager; 42 | import com.jme3.network.util.ObjectMessageDelegator; 43 | 44 | import com.simsilica.mathd.Vec3i; 45 | import com.simsilica.mathd.bits.QuatBits; 46 | import com.simsilica.mathd.bits.Vec3Bits; 47 | import com.simsilica.ethereal.net.ObjectStateProtocol; 48 | import com.simsilica.ethereal.net.StateReceiver; 49 | import com.simsilica.ethereal.zone.ZoneGrid; 50 | import com.simsilica.ethereal.zone.ZoneManager; 51 | 52 | 53 | /** 54 | * 55 | * 56 | * @author Paul Speed 57 | */ 58 | public class EtherealClient extends AbstractClientService { 59 | 60 | private ZoneGrid grid; 61 | private StateReceiver stateReceiver; 62 | private ObjectMessageDelegator delegator; 63 | private ObjectStateProtocol objectProtocol; 64 | private Vec3i clientZoneExtents; 65 | private SharedObjectSpace space; 66 | 67 | public EtherealClient() { 68 | this(new ObjectStateProtocol(8, 64, new Vec3Bits(-10, 42, 16), new QuatBits(12)), 69 | new ZoneGrid(32), new Vec3i(1, 1, 1)); 70 | } 71 | 72 | public EtherealClient( ZoneGrid grid ) { 73 | this(new ObjectStateProtocol(8, 64, new Vec3Bits(-10, 42, 16), new QuatBits(12)), 74 | grid, new Vec3i(1, 1, 1)); 75 | } 76 | 77 | public EtherealClient( ObjectStateProtocol objectProtocol, ZoneGrid grid, Vec3i clientZoneExtents ) { 78 | this.objectProtocol = objectProtocol; 79 | this.grid = grid; 80 | this.clientZoneExtents = clientZoneExtents; 81 | } 82 | 83 | public TimeSource getTimeSource() { 84 | return stateReceiver.getTimeSource(); 85 | } 86 | 87 | public void addObjectListener( SharedObjectListener l ) { 88 | space.addObjectListener(l); 89 | } 90 | 91 | public void removeObjectListener( SharedObjectListener l ) { 92 | space.removeObjectListener(l); 93 | } 94 | 95 | @Override 96 | protected void onInitialize( ClientServiceManager s ) { 97 | 98 | this.space = new SharedObjectSpace(objectProtocol); 99 | this.stateReceiver = new StateReceiver(getClient(), new LocalZoneIndex(grid, clientZoneExtents), 100 | space); 101 | this.delegator = new ObjectMessageDelegator(stateReceiver, true); 102 | getClient().addMessageListener(delegator, delegator.getMessageTypes()); 103 | } 104 | 105 | @Override 106 | public void terminate( ClientServiceManager serviceManager ) { 107 | getClient().removeMessageListener(delegator, delegator.getMessageTypes()); 108 | } 109 | 110 | } 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/EtherealHost.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal; 38 | 39 | import com.jme3.network.HostedConnection; 40 | import com.jme3.network.serializing.Serializer; 41 | import com.jme3.network.serializing.serializers.FieldSerializer; 42 | import com.jme3.network.service.AbstractHostedConnectionService; 43 | import com.jme3.network.service.HostedServiceManager; 44 | import com.jme3.network.util.SessionDataDelegator; 45 | import com.simsilica.mathd.Vec3d; 46 | import com.simsilica.mathd.Vec3i; 47 | import com.simsilica.mathd.bits.QuatBits; 48 | import com.simsilica.mathd.bits.Vec3Bits; 49 | import com.simsilica.ethereal.net.ClientStateMessage; 50 | import com.simsilica.ethereal.net.ObjectStateMessage; 51 | import com.simsilica.ethereal.net.ObjectStateProtocol; 52 | import com.simsilica.ethereal.zone.StateCollector; 53 | import com.simsilica.ethereal.zone.StateListener; 54 | import com.simsilica.ethereal.zone.ZoneGrid; 55 | import com.simsilica.ethereal.zone.ZoneManager; 56 | import java.util.Arrays; 57 | 58 | 59 | /** 60 | * 61 | * 62 | * @author Paul Speed 63 | */ 64 | public class EtherealHost extends AbstractHostedConnectionService { 65 | 66 | private ZoneGrid grid; 67 | private ZoneManager zones; 68 | private StateCollector stateCollector; 69 | private ObjectStateProtocol objectProtocol; 70 | private Vec3i clientZoneExtents; 71 | private long stateCollectionInterval; 72 | private long stateCollectionSleepTime = 1; 73 | 74 | private SessionDataDelegator delegator; 75 | 76 | /** 77 | * This is the time source that will be used for timestamping outbound 78 | * object state messages. This time source should be compatible with whatever 79 | * is providing time to the object update messages. Otherwise, clients will 80 | * see a wide difference between what they see from RemoteTimeSource and 81 | * the timestamps in the object updates. 82 | */ 83 | private TimeSource timeSource = new NanoTimeSource(); 84 | 85 | public EtherealHost() { 86 | this(new ObjectStateProtocol(8, 64, new Vec3Bits(-10, 42, 16), new QuatBits(12)), 87 | new ZoneGrid(32), new Vec3i(1, 1, 1)); 88 | } 89 | 90 | public EtherealHost( ZoneGrid grid ) { 91 | this(new ObjectStateProtocol(8, 64, new Vec3Bits(-10, 42, 16), new QuatBits(12)), 92 | grid, new Vec3i(1, 1, 1)); 93 | } 94 | 95 | public EtherealHost( ObjectStateProtocol objectProtocol, ZoneGrid grid, Vec3i clientZoneExtents ) { 96 | super(false); 97 | this.objectProtocol = objectProtocol; 98 | this.grid = grid; 99 | this.clientZoneExtents = clientZoneExtents; 100 | this.zones = new ZoneManager(grid); 101 | 102 | Serializer.registerClasses(ClientStateMessage.class, ObjectStateMessage.class); 103 | Serializer.registerClass(Vec3d.class, new FieldSerializer()); 104 | } 105 | 106 | /** 107 | * Returns the aurhoritative time source on the server. This should match the timestamps 108 | * that are being supplied in object update notifications. 109 | */ 110 | public TimeSource getTimeSource() { 111 | return timeSource; 112 | } 113 | 114 | /** 115 | * Returns the aurhoritative time source on the server. This should match the timestamps 116 | * that are being supplied in object update notifications. Applications should set their 117 | * own timesource if they are using a different time tracking basis than System.nanoTime(). 118 | */ 119 | public void setTimeSource( TimeSource timeSource ) { 120 | this.timeSource = timeSource == null ? new NanoTimeSource() : timeSource; 121 | } 122 | 123 | public ZoneManager getZones() { 124 | return zones; 125 | } 126 | 127 | public void setObjectProtocol( ObjectStateProtocol objectProtocol ) { 128 | this.objectProtocol = objectProtocol; 129 | } 130 | 131 | public ObjectStateProtocol getObjectProtocol() { 132 | return objectProtocol; 133 | } 134 | 135 | public void addListener( StateListener l ) { 136 | stateCollector.addListener(l); 137 | } 138 | 139 | public void removeListener( StateListener l ) { 140 | stateCollector.removeListener(l); 141 | } 142 | 143 | /** 144 | * Sets the state collection interval that the StateCollector will use 145 | * to pull history from the ZoneManager and deliver it to the clients. 146 | * By default this is 1/20th of a second, or 50,000,000 nanoseconds. 147 | */ 148 | public void setStateCollectionInterval( long nanos ) { 149 | if( stateCollector != null ) { 150 | throw new RuntimeException("The state collection interval cannot be set once the service is initialized."); 151 | } 152 | this.stateCollectionInterval = nanos; 153 | } 154 | 155 | public long getStateCollectionInterval() { 156 | return stateCollectionInterval; 157 | } 158 | 159 | /** 160 | * Sets the sleep() time for the state collector's idle periods. 161 | * This defaults to 1 which keeps the CPU happier while also providing 162 | * timely updates. However, for higher rates of state collection (lower stateCollectionIntervals) 163 | * on windows, (such as 60 FPS) sleep(1) may take longer than 1/60th of a second or close 164 | * enough to still cause collection frame drops. In that case, it can be configured 165 | * to 0 which should provide timelier updates. 166 | */ 167 | public void setStateCollectionSleepTime( long millis ) { 168 | this.stateCollectionSleepTime = millis; 169 | if( stateCollector != null ) { 170 | stateCollector.setIdleSleepTime(millis); 171 | } 172 | } 173 | 174 | public long getStateCollectionSleepTime() { 175 | return stateCollectionSleepTime; 176 | } 177 | 178 | @Override 179 | protected void onInitialize( HostedServiceManager s ) { 180 | this.stateCollector = new StateCollector(zones, stateCollectionInterval); 181 | stateCollector.setIdleSleepTime(stateCollectionSleepTime); 182 | 183 | // A general listener for forwarding the messages 184 | // to the client-specific handler 185 | this.delegator = new SessionDataDelegator(NetworkStateListener.class, 186 | NetworkStateListener.ATTRIBUTE_KEY, 187 | true); 188 | //System.out.println("network state message types:" + Arrays.asList(delegator.getMessageTypes())); 189 | getServer().addMessageListener(delegator, delegator.getMessageTypes()); 190 | 191 | } 192 | 193 | @Override 194 | public void terminate(HostedServiceManager serviceManager) { 195 | getServer().removeMessageListener(delegator, delegator.getMessageTypes()); 196 | } 197 | 198 | 199 | @Override 200 | public void start() { 201 | stateCollector.start(); 202 | } 203 | 204 | @Override 205 | public void stop() { 206 | stateCollector.shutdown(); 207 | } 208 | 209 | public NetworkStateListener getStateListener( HostedConnection hc ) { 210 | return hc.getAttribute(NetworkStateListener.ATTRIBUTE_KEY); 211 | } 212 | 213 | @Override 214 | public void startHostingOnConnection( HostedConnection hc ) { 215 | 216 | // See if we've already got one 217 | NetworkStateListener nsl = hc.getAttribute(NetworkStateListener.ATTRIBUTE_KEY); 218 | if( nsl != null ) { 219 | return; 220 | } 221 | 222 | nsl = new NetworkStateListener(this, hc, grid, clientZoneExtents); 223 | hc.setAttribute(NetworkStateListener.ATTRIBUTE_KEY, nsl); 224 | 225 | // Now add it to the state collector 226 | stateCollector.addListener(nsl); 227 | 228 | // FIXME/TODO: send the object protocol, grid configuration, etc. 229 | // to the client to avoid accidental mismatches. 230 | } 231 | 232 | /** 233 | * Called during connection setup to tell SimEthereal which tracked object 234 | * is the player and what their initial position is. This allows the per-connection 235 | * state listener to properly find the center zone for the player. 236 | */ 237 | public void setConnectionObject( HostedConnection hc, Long selfId, Vec3d initialPosition ) { 238 | getStateListener(hc).setSelf(selfId, initialPosition); 239 | } 240 | 241 | @Override 242 | public void stopHostingOnConnection( HostedConnection hc ) { 243 | NetworkStateListener nsl = hc.getAttribute(NetworkStateListener.ATTRIBUTE_KEY); 244 | if( nsl == null ) { 245 | return; 246 | } 247 | 248 | stateCollector.removeListener(nsl); 249 | hc.setAttribute(NetworkStateListener.ATTRIBUTE_KEY, null); 250 | } 251 | } 252 | 253 | 254 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/IdIndex.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal; 38 | 39 | import java.util.HashMap; 40 | import java.util.Map; 41 | 42 | 43 | /** 44 | * Provides a unique limited range set of integer IDs for a set 45 | * of Long object IDs. Generated IDs can be reused later if the 46 | * caller returns them to the pool with retireId(). 47 | * 48 | * @author Paul Speed 49 | */ 50 | public class IdIndex { 51 | 52 | private final Map entityIdMap = new HashMap<>(); 53 | private final Map idMap = new HashMap<>(); 54 | private int minId; 55 | private int maxId; 56 | private int nextId; 57 | 58 | public IdIndex( int minId ) { 59 | this(minId, 65536); 60 | } 61 | 62 | public IdIndex( int minId, int maxId ) { 63 | this.minId = minId; 64 | this.maxId = maxId; 65 | this.nextId = minId; 66 | } 67 | 68 | protected void incrementNextId() { 69 | nextId++; 70 | if( nextId > maxId ) { 71 | nextId = minId; 72 | } 73 | } 74 | 75 | protected int nextId( Long entity ) { 76 | 77 | // Skip IDs that are already in use. If this 78 | // happens often then we should potentially do 79 | // something smarter. A way to skip large ranges 80 | // of in-use values. 81 | while( entityIdMap.containsKey(nextId) ) { 82 | System.out.println( "******** ID already in use:" + nextId ); 83 | incrementNextId(); 84 | } 85 | 86 | int result = nextId; 87 | entityIdMap.put(result, entity); 88 | idMap.put(entity,result); 89 | incrementNextId(); 90 | 91 | return result; 92 | } 93 | 94 | public int getId( Long entity, boolean create ) { 95 | Integer result = idMap.get(entity); 96 | if( result == null && create ) { 97 | return nextId(entity); 98 | } 99 | return result == null ? -1 : result; 100 | } 101 | 102 | public Long getEntityId( int id ) { 103 | return entityIdMap.get(id); 104 | } 105 | 106 | public void retireId( int id ) { 107 | Long removed = entityIdMap.remove(id); 108 | if( removed == null ) { 109 | System.out.println( "**** Retired id:" + id + " with no mapped entity." ); 110 | } 111 | idMap.remove(removed); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/LocalZoneIndex.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal; 38 | 39 | import com.simsilica.mathd.Vec3d; 40 | import com.simsilica.mathd.Vec3i; 41 | import com.simsilica.ethereal.zone.ZoneGrid; 42 | import com.simsilica.ethereal.zone.ZoneKey; 43 | import java.util.ArrayList; 44 | import java.util.Arrays; 45 | import java.util.HashSet; 46 | import java.util.Iterator; 47 | import java.util.List; 48 | import java.util.Objects; 49 | import java.util.Set; 50 | 51 | 52 | /** 53 | * Keeps track of the local zone space around the player to 54 | * provide smaller zoneId values for particular ZoneKeys. This 55 | * also serves as a functional limit to what a particular client 56 | * can see. 57 | * 58 | * @author Paul Speed 59 | */ 60 | public class LocalZoneIndex { 61 | 62 | private ZoneGrid grid; 63 | private int xExtent; 64 | private int xSize; 65 | private int yExtent; 66 | private int ySize; 67 | private int zExtent; 68 | private int zSize; 69 | private final int minZoneId = 1; 70 | 71 | private ZoneKey center; 72 | private ZoneKey[] keyIndex; 73 | private final Set keySet = new HashSet<>(); 74 | 75 | public LocalZoneIndex( ZoneGrid grid, int gridRadius ) { 76 | this(grid, gridRadius, gridRadius, gridRadius); 77 | } 78 | 79 | public LocalZoneIndex( ZoneGrid grid, Vec3i zoneExtents ) { 80 | this(grid, zoneExtents.x, zoneExtents.y, zoneExtents.z ); 81 | } 82 | 83 | public LocalZoneIndex( ZoneGrid grid, int xRadius, int yRadius, int zRadius ) { 84 | this.grid = grid; 85 | 86 | // Check to see if any of the grid dimensions are flat and do 87 | // similar to our index radius 88 | if( grid.getZoneSize().x == 0 ) { 89 | xRadius = 0; 90 | } 91 | if( grid.getZoneSize().y == 0 ) { 92 | yRadius = 0; 93 | } 94 | if( grid.getZoneSize().z == 0 ) { 95 | zRadius = 0; 96 | } 97 | 98 | this.xExtent = xRadius; 99 | this.yExtent = yRadius; 100 | this.zExtent = zRadius; 101 | xSize = xRadius * 2 + 1; 102 | ySize = yRadius * 2 + 1; 103 | zSize = zRadius * 2 + 1; 104 | keyIndex = new ZoneKey[xSize * ySize * zSize]; 105 | } 106 | 107 | public int getIndexSize() { 108 | return keyIndex.length; 109 | } 110 | 111 | public int getMinimumZoneId() { 112 | return minZoneId; 113 | } 114 | 115 | public ZoneGrid getGrid() { 116 | return grid; 117 | } 118 | 119 | public ZoneKey getZone( int zoneId ) { 120 | return getZone(zoneId, null); 121 | } 122 | 123 | public ZoneKey getZone( int zoneId, ZoneKey defaultValue ) { 124 | if( zoneId <= 0 ) { 125 | return defaultValue; 126 | } 127 | if( center == null ) { 128 | return defaultValue; 129 | } 130 | int index = zoneId - minZoneId; 131 | if( index >= keyIndex.length ) { 132 | throw new IllegalArgumentException("ZoneID out of bounds:" + zoneId + " keyIndex size:" + keyIndex.length); 133 | } 134 | return keyIndex[index]; 135 | } 136 | 137 | public int getZoneId( ZoneKey zone ) { 138 | if( center == null ) { 139 | return -1; 140 | } 141 | 142 | int xBase = center.x - xExtent; 143 | int yBase = center.y - yExtent; 144 | int zBase = center.z - zExtent; 145 | int x = zone.x - xBase; 146 | int y = zone.y - yBase; 147 | int z = zone.z - zBase; 148 | 149 | return minZoneId + z * (xSize * ySize) + y * xSize + x; 150 | } 151 | 152 | public ZoneKey getCenter() { 153 | return center; 154 | } 155 | 156 | public boolean setCenter( Vec3d pos, List entered, List exited ) { 157 | ZoneKey key = grid.worldToKey(pos.x, pos.y, pos.z); 158 | return setCenter(key, entered, exited); 159 | } 160 | 161 | public boolean setCenter( ZoneKey center, List entered, List exited ) { 162 | if( Objects.equals(this.center, center) ) { 163 | return false; 164 | } 165 | 166 | exited.clear(); 167 | entered.clear(); 168 | if( this.center != null ) { 169 | // Seed it with all of them... we'll remove the 170 | // ones 171 | exited.addAll(keySet); 172 | } 173 | 174 | this.center = center; 175 | int index = 0; 176 | for( int z = center.z - zExtent; z <= center.z + zExtent; z++ ) { 177 | for( int y = center.y - yExtent; y <= center.y + yExtent; y++ ) { 178 | for( int x = center.x - xExtent; x <= center.x + xExtent; x++ ) { 179 | 180 | ZoneKey k = new ZoneKey(grid, x, y, z); 181 | keyIndex[index] = k; 182 | index++; 183 | 184 | if( keySet.add(k) ) { 185 | // It's a new zone 186 | entered.add(k); 187 | } 188 | } 189 | } 190 | } 191 | keySet.clear(); 192 | keySet.addAll(Arrays.asList(keyIndex)); 193 | 194 | // Clean out the exited array of stuff that wasn't actually 195 | // exited 196 | for( Iterator it = exited.iterator(); it.hasNext(); ) { 197 | if( keySet.contains(it.next()) ) { 198 | it.remove(); 199 | } 200 | } 201 | 202 | return true; 203 | } 204 | 205 | public static void main( String... args ) { 206 | ZoneGrid grid = new ZoneGrid(32, 32, 32); 207 | LocalZoneIndex zones = new LocalZoneIndex(grid, 1); 208 | List entered = new ArrayList<>(); 209 | List exited = new ArrayList<>(); 210 | 211 | ZoneKey center = new ZoneKey(grid, 2, 2, 2); 212 | zones.setCenter(center, entered, exited); 213 | 214 | System.out.println("Entered:" + entered); 215 | System.out.println("Exited:" + exited); 216 | 217 | for( int i = 0; i < zones.keyIndex.length; i++ ) { 218 | ZoneKey k = zones.keyIndex[i]; 219 | System.out.println(" key[" + i + "] = " + k); 220 | int zoneId = zones.getZoneId(k); 221 | System.out.println(" id:" + zoneId + " relookup:" + zones.getZone(zoneId)); 222 | } 223 | 224 | center = new ZoneKey(grid, 3, 2, 2); 225 | zones.setCenter(center, entered, exited); 226 | System.out.println("Entered:" + entered); 227 | System.out.println("Exited:" + exited); 228 | } 229 | 230 | } 231 | 232 | 233 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/NanoTimeSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal; 38 | 39 | 40 | /** 41 | * A TimeSource that directly returns System.nanoTime() as the time. 42 | * 43 | * @author Paul Speed 44 | */ 45 | public class NanoTimeSource implements TimeSource { 46 | 47 | public NanoTimeSource() { 48 | } 49 | 50 | /** 51 | * Returns the current time in nanoseconds. 52 | */ 53 | public long getTime() { 54 | return System.nanoTime(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/SharedObjectListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal; 38 | 39 | 40 | /** 41 | * 42 | * 43 | * @author Paul Speed 44 | */ 45 | public interface SharedObjectListener { 46 | 47 | public void beginFrame( long time ); 48 | 49 | public void objectUpdated( SharedObject obj ); 50 | 51 | public void objectRemoved( SharedObject obj ); 52 | 53 | public void endFrame(); 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/SharedObjectSpace.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal; 38 | 39 | import com.simsilica.ethereal.net.FrameState; 40 | import com.simsilica.ethereal.net.ObjectState; 41 | import com.simsilica.ethereal.net.ObjectStateProtocol; 42 | import java.util.ArrayList; 43 | import java.util.Collection; 44 | import java.util.HashMap; 45 | import java.util.List; 46 | import java.util.Map; 47 | import java.util.concurrent.ConcurrentLinkedQueue; 48 | 49 | 50 | /** 51 | * The synched shared object space that is local to the player. 52 | * A copy of this is kept per-player on the server and in the client. 53 | * Each SharedObject contains a baseline and a set of current values. 54 | * These are used to form the actual network messages that are sent 55 | * around. The baseline is updated when the client and server agree 56 | * about the level of messages they have both seen. 57 | * 58 | * @author Paul Speed 59 | */ 60 | public class SharedObjectSpace { 61 | 62 | private final ObjectStateProtocol objectProtocol; 63 | private final Map objects = new HashMap<>(); 64 | 65 | private final ConcurrentLinkedQueue toAdd = new ConcurrentLinkedQueue<>(); 66 | private final ConcurrentLinkedQueue toRemove = new ConcurrentLinkedQueue<>(); 67 | private final List listeners = new ArrayList<>(); 68 | private SharedObjectListener[] listenerArray; 69 | 70 | public SharedObjectSpace( ObjectStateProtocol objectProtocol ) { 71 | this.objectProtocol = objectProtocol; 72 | } 73 | 74 | public final ObjectStateProtocol getObjectProtocol() { 75 | return objectProtocol; 76 | } 77 | 78 | public SharedObject getObject( int networkId, Long entityId ) { 79 | SharedObject result = objects.get(networkId); 80 | if( result == null ) { 81 | result = new SharedObject(this, networkId, entityId); 82 | objects.put(networkId, result); 83 | } 84 | return result; 85 | } 86 | 87 | public SharedObject getObject( int networkId ) { 88 | return objects.get(networkId); 89 | } 90 | 91 | public void removeObject( SharedObject so ) { 92 | objects.remove(so.getNetworkId()); 93 | } 94 | 95 | public Collection objects() { 96 | return objects.values(); 97 | } 98 | 99 | public void addObjectListener( SharedObjectListener l ) { 100 | toAdd.add(l); 101 | toRemove.remove(l); 102 | } 103 | 104 | public void removeObjectListener( SharedObjectListener l ) { 105 | toRemove.add(l); 106 | toAdd.remove(l); 107 | } 108 | 109 | private SharedObjectListener[] getListeners() { 110 | if( listenerArray == null ) { 111 | listenerArray = new SharedObjectListener[listeners.size()]; 112 | listenerArray = listeners.toArray(listenerArray); 113 | } 114 | return listenerArray; 115 | } 116 | 117 | /** 118 | * Used by the state receiver to notify client-side SharedObjectListeners 119 | * about a new frame. 120 | */ 121 | public final void beginFrame( long time ) { 122 | 123 | // This makes sure that we don't notify some listener about 124 | // an object before they got a beginFrame() 125 | while( !toAdd.isEmpty() ) { 126 | SharedObjectListener l = toAdd.poll(); 127 | listeners.add(l); 128 | listenerArray = null; 129 | } 130 | while( !toRemove.isEmpty() ) { 131 | SharedObjectListener l = toRemove.poll(); 132 | listeners.remove(l); 133 | listenerArray = null; 134 | } 135 | 136 | for( SharedObjectListener l : getListeners() ) { 137 | l.beginFrame(time); 138 | } 139 | } 140 | 141 | /** 142 | * Used by the shared object to notify client-side SharedObjectListeners 143 | * about an object change. 144 | */ 145 | protected final void objectUpdated( SharedObject obj ) { 146 | for( SharedObjectListener l : getListeners() ) { 147 | l.objectUpdated(obj); 148 | } 149 | } 150 | 151 | /** 152 | * Used by the shared object to notify client-side SharedObjectListeners 153 | * about an object removal. 154 | */ 155 | protected final void objectRemoved( SharedObject obj ) { 156 | for( SharedObjectListener l : getListeners() ) { 157 | l.objectRemoved(obj); 158 | } 159 | } 160 | 161 | /** 162 | * Used by the state receiver to notify client-side SharedObjectListeners 163 | * about an ended frame. 164 | */ 165 | public final void endFrame() { 166 | for( SharedObjectListener l : getListeners() ) { 167 | l.endFrame(); 168 | } 169 | } 170 | 171 | /** 172 | * Update the shared object baselines with the specified frame 173 | * data. 174 | */ 175 | public void updateBaseline( List frames ) { 176 | for( FrameState frame : frames ) { 177 | for( ObjectState state : frame.states ) { 178 | 179 | SharedObject so = getObject(state.networkId); 180 | if( so == null ) { 181 | // This can happen if we receive duplicate state.. 182 | // which we will... often. 183 | continue; 184 | } 185 | 186 | so.updateBaseline(frame.time, state); 187 | } 188 | } 189 | } 190 | } 191 | 192 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/Statistics.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal; 38 | 39 | //import com.simsilica.lemur.core.VersionedObject; 40 | //import com.simsilica.lemur.core.VersionedReference; 41 | import java.util.Collections; 42 | import java.util.Set; 43 | import java.util.concurrent.ConcurrentHashMap; 44 | import java.util.concurrent.ConcurrentLinkedQueue; 45 | import java.util.concurrent.atomic.AtomicLong; 46 | import java.util.concurrent.locks.ReadWriteLock; 47 | import java.util.concurrent.locks.ReentrantReadWriteLock; 48 | 49 | 50 | /** 51 | * 52 | * 53 | * @author Paul Speed 54 | */ 55 | public class Statistics { 56 | 57 | private static final ConcurrentHashMap counters = new ConcurrentHashMap<>(); 58 | private static final ConcurrentHashMap trackers = new ConcurrentHashMap<>(); 59 | private static final ConcurrentHashMap sequences = new ConcurrentHashMap<>(); 60 | 61 | public static Counter getCounter( String name, boolean create ) { 62 | Counter result = counters.get(name); 63 | if( result == null && create ) { 64 | synchronized(counters) { 65 | result = counters.get(name); 66 | if( result == null ) { 67 | result = new Counter(name); 68 | counters.put(name, result); 69 | } 70 | } 71 | } 72 | return result; 73 | } 74 | 75 | public static long getCounterValue( String name ) { 76 | Counter result = counters.get(name); 77 | return result == null ? -1 : result.get(); 78 | } 79 | 80 | /* public static VersionedReference createCounterRef( String name, boolean create ) { 81 | Counter counter = getCounter(name, create); 82 | if( counter == null ) { 83 | throw new IllegalArgumentException("Counter not found for:" + name); 84 | } 85 | return counter.createReference(); 86 | }*/ 87 | 88 | public static Set counterNames() { 89 | return Collections.unmodifiableSet(counters.keySet()); 90 | } 91 | 92 | public static Tracker getTracker( String name, boolean create ) { 93 | return getTracker(name, 0, create); 94 | } 95 | 96 | public static Tracker getTracker( String name, int windowSize, boolean create ) { 97 | Tracker result = trackers.get(name); 98 | if( result == null && create ) { 99 | synchronized(trackers) { 100 | result = trackers.get(name); 101 | if( result == null ) { 102 | result = new Tracker(name, windowSize); 103 | trackers.put(name, result); 104 | } 105 | } 106 | } 107 | return result; 108 | } 109 | 110 | public static long getTrackerValue( String name ) { 111 | Tracker result = trackers.get(name); 112 | return result == null ? -1 : result.get(); 113 | } 114 | 115 | /*public static VersionedReference createTrackerRef( String name, boolean create ) { 116 | Tracker tracker = getTracker(name, create); 117 | if( tracker == null ) { 118 | throw new IllegalArgumentException("Tracker not found for:" + name); 119 | } 120 | return tracker.createReference(); 121 | }*/ 122 | 123 | public static Set trackerNames() { 124 | return Collections.unmodifiableSet(trackers.keySet()); 125 | } 126 | 127 | public static Sequence getSequence( String name, boolean create ) { 128 | Sequence result = sequences.get(name); 129 | if( result == null && create ) { 130 | synchronized(sequences) { 131 | result = sequences.get(name); 132 | if( result == null ) { 133 | result = new Sequence(name); 134 | sequences.put(name, result); 135 | } 136 | } 137 | } 138 | return result; 139 | } 140 | 141 | public static Set sequenceNames() { 142 | return Collections.unmodifiableSet(sequences.keySet()); 143 | } 144 | 145 | protected abstract static class AbstractValue { //implements VersionedObject { 146 | private final String name; 147 | private final AtomicLong version; 148 | 149 | protected AbstractValue( String name ) { 150 | this.name = name; 151 | this.version = new AtomicLong(); 152 | } 153 | 154 | public String getName() { 155 | return name; 156 | } 157 | 158 | protected void incrementVersion() { 159 | version.incrementAndGet(); 160 | } 161 | 162 | /*@Override 163 | public long getVersion() { 164 | return version.get(); 165 | } 166 | 167 | @Override 168 | public abstract T getObject(); 169 | 170 | @Override 171 | public VersionedReference createReference() { 172 | return new VersionedReference<>(this); 173 | }*/ 174 | } 175 | 176 | public static class Counter extends AbstractValue { 177 | private final AtomicLong counter; 178 | 179 | public Counter( String name ) { 180 | super(name); 181 | this.counter = new AtomicLong(); 182 | } 183 | 184 | public long get() { 185 | return counter.get(); 186 | } 187 | 188 | public long increment() { 189 | incrementVersion(); 190 | return counter.incrementAndGet(); 191 | } 192 | 193 | public long decrement() { 194 | incrementVersion(); 195 | return counter.decrementAndGet(); 196 | } 197 | 198 | public long add( long value ) { 199 | incrementVersion(); 200 | return counter.incrementAndGet(); 201 | } 202 | 203 | /*@Override 204 | public Long getObject() { 205 | return get(); 206 | }*/ 207 | 208 | @Override 209 | public String toString() { 210 | return "Counter[" + getName() + "=" + counter.get() + "]"; 211 | } 212 | } 213 | 214 | public static class Tracker extends AbstractValue { 215 | private long value; 216 | private final ReadWriteLock lock = new ReentrantReadWriteLock(); 217 | private long size; 218 | private final long windowSize; 219 | 220 | public Tracker( String name, long windowSize ) { 221 | super(name); 222 | this.windowSize = windowSize; 223 | } 224 | 225 | public long get() { 226 | lock.readLock().lock(); 227 | try { 228 | return value; 229 | } finally { 230 | lock.readLock().unlock(); 231 | } 232 | } 233 | 234 | public long update( long newValue ) { 235 | lock.writeLock().lock(); 236 | try { 237 | // Calculate a weighted average given a statistical window size 238 | long count = Math.min(size, windowSize); 239 | size++; 240 | value = (value * count + newValue) / (count + 1); 241 | return value; 242 | } finally { 243 | lock.writeLock().unlock(); 244 | } 245 | } 246 | 247 | /*@Override 248 | public Long getObject() { 249 | return get(); 250 | }*/ 251 | 252 | @Override 253 | public String toString() { 254 | return "Tracker[" + getName() + "=" + get() + "]"; 255 | } 256 | } 257 | 258 | public static class Sequence { 259 | private final String name; 260 | private final ConcurrentLinkedQueue values = new ConcurrentLinkedQueue<>(); 261 | private final int maxSize = 1000; // keep from gobbling all available space. 262 | 263 | public Sequence( String name ) { 264 | this.name = name; 265 | } 266 | 267 | public String getName() { 268 | return name; 269 | } 270 | 271 | public boolean isEmpty() { 272 | return values.isEmpty(); 273 | } 274 | 275 | public void add( Long value ) { 276 | values.add(value); 277 | while(values.size() > maxSize) { 278 | values.poll(); 279 | } 280 | } 281 | 282 | public Long poll() { 283 | return values.poll(); 284 | } 285 | 286 | @Override 287 | public String toString() { 288 | return "Sequence[" + getName() + " size=" + values.size() + "]"; 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/SynchedTimeSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2018, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal; 38 | 39 | 40 | /** 41 | * A time source that represents time that has been synched with 42 | * another time source relative to local system time. It has 43 | * methods that indicate the amount of drift, offset, etc. that 44 | * maps local system time to TimeSource time. 45 | * 46 | * @author Paul Speed 47 | */ 48 | public interface SynchedTimeSource extends TimeSource { 49 | 50 | /** 51 | * Returns the current drift if there is one. In a remote 52 | * time source, the drift is the offset from current time 53 | * and is related in a loose way to both the ping time of the 54 | * server and the general time difference of the server. 55 | */ 56 | public long getDrift(); 57 | 58 | /** 59 | * Sets a time that will be automatically added into the 60 | * normally returned time. This allows getTime() to return 61 | * time in the past for interpolating object state buffers. 62 | */ 63 | public void setOffset( long offset ); 64 | 65 | /** 66 | * Returns the current time offset for this TimeSource. 67 | */ 68 | public long getOffset(); 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/TimeSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal; 38 | 39 | 40 | /** 41 | * Provides local or networked time as needed with certain 42 | * implied constraints (like time always moves forward, ie: 43 | * never backwards, etc.) 44 | * 45 | * @author Paul Speed 46 | */ 47 | public interface TimeSource { 48 | 49 | /** 50 | * Returns the current time in nanoseconds. 51 | */ 52 | public long getTime(); 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/io/BitInputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.io; 38 | 39 | import java.io.*; 40 | 41 | /** 42 | * Reads bit strings of any length from an 43 | * underlying stream. 44 | * 45 | * @version $Revision: 4022 $ 46 | * @author Paul Speed 47 | */ 48 | public class BitInputStream implements AutoCloseable { 49 | private final InputStream in; 50 | private int lastByte; 51 | private int bits = 0; 52 | 53 | public BitInputStream( InputStream in ) { 54 | this.in = in; 55 | } 56 | 57 | public long readLongBits( int count ) throws IOException { 58 | if( count == 0 ) 59 | throw new IllegalArgumentException( "Cannot read 0 bits." ); 60 | 61 | if( count > 64 ) 62 | throw new IllegalArgumentException( "Bit count overflow:" + count ); 63 | 64 | long result = 0; 65 | 66 | // While we still have bits remaining... 67 | int remainingCount = count; 68 | while( remainingCount > 0 ) { 69 | // See if we need to refill the current read byte 70 | if( bits == 0 ) { 71 | int b = in.read(); 72 | if( b < 0 ) 73 | throw new IOException( "End of stream reached." ); 74 | lastByte = b; 75 | bits = 8; 76 | } 77 | 78 | // Copy the smaller of the two: remaining bits 79 | // or bits left in lastByte. 80 | int bitsToCopy = bits < remainingCount ? bits : remainingCount; 81 | 82 | // How much do we have to shift the read byte to just 83 | // get the high bits we want? 84 | int sourceShift = bits - bitsToCopy; 85 | 86 | // And how much do we have to shift those bits to graft 87 | // them onto our result? 88 | int targetShift = remainingCount - bitsToCopy; 89 | 90 | // Copy the bits 91 | result |= ((long)lastByte >> sourceShift) << targetShift; 92 | 93 | // Keep track of how many bits we have left 94 | remainingCount -= bitsToCopy; 95 | bits -= bitsToCopy; 96 | 97 | // Now we need to mask off the bits we just copied from 98 | // lastByte. Just keep the bits that are left. 99 | lastByte = lastByte & (0xff >> (8 - bits)); 100 | } 101 | 102 | return result; 103 | } 104 | 105 | public int readBits( int count ) throws IOException { 106 | if( count == 0 ) 107 | throw new IllegalArgumentException( "Cannot read 0 bits." ); 108 | 109 | if( count > 32 ) 110 | throw new IllegalArgumentException( "Bit count overflow:" + count ); 111 | 112 | int result = 0; 113 | 114 | // While we still have bits remaining... 115 | int remainingCount = count; 116 | while( remainingCount > 0 ) { 117 | // See if we need to refill the current read byte 118 | if( bits == 0 ) { 119 | int b = in.read(); 120 | if( b < 0 ) 121 | throw new IOException( "End of stream reached." ); 122 | lastByte = b; 123 | bits = 8; 124 | } 125 | 126 | // Copy the smaller of the two: remaining bits 127 | // or bits left in lastByte. 128 | int bitsToCopy = bits < remainingCount ? bits : remainingCount; 129 | 130 | // How much do we have to shift the read byte to just 131 | // get the high bits we want? 132 | int sourceShift = bits - bitsToCopy; 133 | 134 | // And how much do we have to shift those bits to graft 135 | // them onto our result? 136 | int targetShift = remainingCount - bitsToCopy; 137 | 138 | // Copy the bits 139 | result |= (lastByte >> sourceShift) << targetShift; 140 | 141 | // Keep track of how many bits we have left 142 | remainingCount -= bitsToCopy; 143 | bits -= bitsToCopy; 144 | 145 | // Now we need to mask off the bits we just copied from 146 | // lastByte. Just keep the bits that are left. 147 | lastByte = lastByte & (0xff >> (8 - bits)); 148 | } 149 | 150 | return result; 151 | } 152 | 153 | @Override 154 | public void close() throws IOException { 155 | in.close(); 156 | } 157 | 158 | public static void main( String... args ) throws Exception { 159 | 160 | for( int count = 1; count <= 32; count++ ) { 161 | System.out.println( "Count:" + count ); 162 | byte[] bytes = new byte[] { (byte)0x12, (byte)0x34, (byte)0x56, (byte)0x78, (byte)0x9a, (byte)0xbc, (byte)0xde, (byte)0xff }; 163 | int total = 8 * 8; 164 | 165 | ByteArrayInputStream bIn = new ByteArrayInputStream(bytes); 166 | BitInputStream in = new BitInputStream(bIn); 167 | 168 | int bitsRead = 0; 169 | while( bitsRead <= (total - count) ) { 170 | System.out.println( Integer.toHexString( in.readBits(count) ) ); 171 | bitsRead += count; 172 | } 173 | } 174 | 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/io/BitOutputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.io; 38 | 39 | import java.io.*; 40 | 41 | /** 42 | * Writes bit strings of any length to an 43 | * underlying stream. 44 | * 45 | * @version $Revision: 4022 $ 46 | * @author Paul Speed 47 | */ 48 | public class BitOutputStream implements AutoCloseable { 49 | private final OutputStream out; 50 | private int currentByte = 0; 51 | private int bits = 8; 52 | 53 | public BitOutputStream( OutputStream out ) { 54 | this.out = out; 55 | } 56 | 57 | public int getPendingBits() { 58 | return bits; 59 | } 60 | 61 | public void writeBits( int value, int count ) throws IOException { 62 | if( count == 0 ) 63 | throw new IllegalArgumentException( "Cannot write 0 bits." ); 64 | 65 | // Make sure the value is clean of extra high bits 66 | value = value & (0xffffffff >>> (32 - count)); 67 | 68 | int remaining = count; 69 | while( remaining > 0 ) { 70 | int bitsToCopy = bits < remaining ? bits : remaining; 71 | 72 | int sourceShift = remaining - bitsToCopy; 73 | int targetShift = bits - bitsToCopy; 74 | 75 | currentByte |= (value >>> sourceShift) << targetShift; 76 | 77 | remaining -= bitsToCopy; 78 | bits -= bitsToCopy; 79 | 80 | value = value & (0xffffffff >>> (32 - remaining)); 81 | 82 | // If there are no more bits left to write to in our 83 | // working byte then write it out and clear it. 84 | if( bits == 0 ) { 85 | flush(); 86 | } 87 | } 88 | } 89 | 90 | public void writeLongBits( long value, int count ) throws IOException { 91 | if( count == 0 ) 92 | throw new IllegalArgumentException( "Cannot write 0 bits." ); 93 | 94 | // Make sure the value is clean of extra high bits 95 | value = value & (0xffffffffffffffffL >>> (64 - count)); 96 | 97 | int remaining = count; 98 | while( remaining > 0 ) { 99 | int bitsToCopy = bits < remaining ? bits : remaining; 100 | 101 | int sourceShift = remaining - bitsToCopy; 102 | int targetShift = bits - bitsToCopy; 103 | 104 | currentByte |= (value >>> sourceShift) << targetShift; 105 | 106 | remaining -= bitsToCopy; 107 | bits -= bitsToCopy; 108 | 109 | value = value & (0xffffffffffffffffL >>> (64 - remaining)); 110 | 111 | // If there are no more bits left to write to in our 112 | // working byte then write it out and clear it. 113 | if( bits == 0 ) { 114 | flush(); 115 | } 116 | } 117 | } 118 | 119 | protected void flush() throws IOException { 120 | out.write(currentByte); 121 | bits = 8; 122 | currentByte = 0; 123 | } 124 | 125 | @Override 126 | public void close() throws IOException { 127 | flush(); 128 | out.close(); 129 | } 130 | 131 | public static void main( String... args ) throws Exception { 132 | test2(); 133 | } 134 | 135 | public static void test2() throws Exception { 136 | byte[] bytes = new byte[] { (byte)0x12, (byte)0x34, (byte)0x56, (byte)0x78, (byte)0x9a, (byte)0xbc, (byte)0xde, (byte)0xff }; 137 | 138 | ByteArrayOutputStream bOut = new ByteArrayOutputStream(); 139 | BitOutputStream out = new BitOutputStream(bOut); 140 | for( int i = 0; i < bytes.length; i++ ) { 141 | out.writeBits( 1, 1 ); 142 | out.writeBits( bytes[i], 8 ); 143 | out.writeLongBits( 0x123456789abcdef0L, 64 ); 144 | out.writeLongBits( -1, 64 ); 145 | out.writeLongBits( 0x80123456789abcdeL, 64 ); 146 | } 147 | out.close(); 148 | 149 | byte[] toRead = bOut.toByteArray(); 150 | System.out.println( "Written length:" + toRead.length ); 151 | 152 | ByteArrayInputStream bIn = new ByteArrayInputStream(toRead); 153 | BitInputStream in = new BitInputStream(bIn); 154 | for( int i = 0; i < bytes.length; i++ ) { 155 | int test = in.readBits(1); 156 | int val = in.readBits(8); 157 | long l1 = in.readLongBits(64); 158 | long l2 = in.readLongBits(64); 159 | long l3 = in.readLongBits(64); 160 | System.out.print( "[" + Integer.toHexString( val ) + "]" ); 161 | System.out.println( "(" + Long.toHexString(l1) + ", " + Long.toHexString(l2) + ", " + Long.toHexString(l3) + ")" ); 162 | } 163 | System.out.println(); 164 | } 165 | 166 | public static void test1() throws Exception { 167 | 168 | for( int count = 1; count <= 32; count++ ) { 169 | System.out.println( "Count:" + count ); 170 | byte[] bytes = new byte[] { (byte)0x12, (byte)0x34, (byte)0x56, (byte)0x78, (byte)0x9a, (byte)0xbc, (byte)0xde, (byte)0xff }; 171 | int total = 8 * 8; 172 | 173 | ByteArrayInputStream bIn = new ByteArrayInputStream(bytes); 174 | BitInputStream in = new BitInputStream(bIn); 175 | 176 | ByteArrayOutputStream bOut = new ByteArrayOutputStream(); 177 | BitOutputStream out = new BitOutputStream(bOut); 178 | 179 | int bitsRead = 0; 180 | while( bitsRead <= (total - count) ) { 181 | int val = in.readBits(count); 182 | out.writeBits( val, count ); 183 | //System.out.println( Integer.toHexString( in.readBits(count) ) ); 184 | bitsRead += count; 185 | } 186 | 187 | byte[] result = bOut.toByteArray(); 188 | for( int i = 0; i < result.length; i++ ) 189 | System.out.print( "[" + Integer.toHexString(result[i] & 0xff) + "]" ); 190 | System.out.println(); 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/io/BitStreamTester.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.io; 38 | 39 | import java.io.*; 40 | import java.util.Random; 41 | 42 | /** 43 | * 44 | * 45 | * @author Paul Speed 46 | */ 47 | public class BitStreamTester { 48 | 49 | public static void main( String... args ) throws Exception { 50 | 51 | Random rand = new Random(1); 52 | 53 | // Create an array of test values and test bit sizes 54 | int count = 1000; 55 | int[] bits = new int[count]; 56 | long[] values = new long[count]; 57 | int totalBits = 0; 58 | 59 | for( int i = 0; i < count; i++ ) { 60 | int size = rand.nextInt(63) + 1; 61 | bits[i] = size; 62 | totalBits += size; 63 | long mask = 0xffffffffffffffffL >>> (64 - size); 64 | long value = rand.nextLong(); 65 | value = value & mask; 66 | 67 | // No reason to let negatives get in the way... testing the 68 | // high bit will happen anyway. 69 | value = value & 0x7fffffffffffffffL; 70 | 71 | //System.out.println("[" + i + "] bits:" + size + " value:" + Long.toHexString(value) + " mask:" + Long.toHexString(mask)); 72 | values[i] = value; 73 | } 74 | 75 | System.out.println("Writing " + count + " values as " + totalBits + " bits... ~" + (totalBits / 8) + " bytes."); 76 | 77 | // Write the data 78 | ByteArrayOutputStream bOut = new ByteArrayOutputStream(); 79 | BitOutputStream out = new BitOutputStream(bOut); 80 | try { 81 | for( int i = 0; i < count; i++ ) { 82 | out.writeLongBits(values[i], bits[i]); 83 | } 84 | } finally { 85 | out.close(); 86 | } 87 | 88 | byte[] raw = bOut.toByteArray(); 89 | System.out.println("Array size:" + raw.length); 90 | 91 | // Read it back 92 | long[] readValues = new long[count]; 93 | ByteArrayInputStream bIn = new ByteArrayInputStream(raw); 94 | BitInputStream in = new BitInputStream(bIn); 95 | for( int i = 0; i < count; i++ ) { 96 | readValues[i] = in.readLongBits(bits[i]); 97 | } 98 | 99 | // Now compare the values 100 | boolean good = true; 101 | for( int i = 0; i < count; i++ ) { 102 | if( readValues[i] != values[i] ) { 103 | System.out.println("Index " + i + " differs, read:" + readValues[i] + " wrote:" + values[i] + " size:" + bits[i]); 104 | good = false; 105 | break; 106 | } 107 | } 108 | if( good ) { 109 | System.out.println("Input matches written data."); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/net/ClientStateMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.net; 38 | 39 | 40 | import com.jme3.network.*; 41 | import com.jme3.network.serializing.Serializable; 42 | 43 | /** 44 | * 45 | * 46 | * @author Paul Speed 47 | */ 48 | @Serializable 49 | public class ClientStateMessage extends AbstractMessage { 50 | 51 | private int ackId; // ID ofo the message we are ack'ing 52 | private long time; // for ping tracking... nano time 53 | // of the message we received from the server 54 | private long controlBits; 55 | private transient long receivedTime; 56 | 57 | public ClientStateMessage() { 58 | } 59 | 60 | public ClientStateMessage( ObjectStateMessage ack, long controlBits ) { 61 | this.ackId = ack.getId(); 62 | this.time = ack.getTime(); 63 | this.controlBits = controlBits; 64 | } 65 | 66 | public void resetReceivedTime( long timestamp ) { 67 | receivedTime = timestamp; 68 | } 69 | 70 | public long getReceivedTime() { 71 | return receivedTime; 72 | } 73 | 74 | public int getId() { 75 | return ackId; 76 | } 77 | 78 | public long getTime() { 79 | return time; 80 | } 81 | 82 | public long getControlBits() { 83 | return controlBits; 84 | } 85 | 86 | @Override 87 | public String toString() { 88 | return "ClientStateMessage[id=" + ackId + ", time=" + time + ", controlBits=" + Long.toBinaryString(controlBits) + "]"; 89 | } 90 | } 91 | 92 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/net/FrameState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.net; 38 | 39 | import com.simsilica.ethereal.io.BitInputStream; 40 | import com.simsilica.ethereal.io.BitOutputStream; 41 | import java.io.IOException; 42 | import java.util.ArrayList; 43 | import java.util.List; 44 | 45 | 46 | /** 47 | * 48 | * 49 | * @author Paul Speed 50 | */ 51 | public class FrameState { 52 | 53 | public long legacySequence; 54 | public long time; 55 | public long columnId; 56 | public List states = new ArrayList<>(); 57 | public long estimatedBitSize; 58 | 59 | public FrameState() { 60 | this( -1, -1, -1 ); 61 | } 62 | 63 | public FrameState( long time, long legacySequence, long columnId ) { 64 | this.time = time; 65 | 66 | // Not sure sequence is really the way to go as client will 67 | // need some kind of time and sequence is at worse superfluous 68 | // and at best too many bits for what it does. 69 | this.legacySequence = legacySequence; 70 | this.columnId = columnId; 71 | 72 | // Header + list size 73 | this.estimatedBitSize = getHeaderBitSize(); 74 | } 75 | 76 | public static int getHeaderBitSize() { 77 | // extra 16 bits for the list size 78 | return 64 + 64 + 16; 79 | } 80 | 81 | public long getEstimatedBitSize() { 82 | return estimatedBitSize; 83 | } 84 | 85 | public void addState( ObjectState state, ObjectStateProtocol protocol ) { 86 | if( state.networkId == -1 ) { 87 | throw new IllegalArgumentException("Incomplete state added to frame:" + state); 88 | } 89 | 90 | states.add(state); 91 | estimatedBitSize += protocol.getEstimatedBitSize(state); 92 | } 93 | 94 | public FrameState split( long limit, ObjectStateProtocol protocol ) { 95 | if( estimatedBitSize <= limit ) { 96 | return null; 97 | } 98 | 99 | // Else split us at the right place... which we need to find 100 | long size = getHeaderBitSize(); 101 | int split = 0; 102 | while( split < states.size() ) { 103 | ObjectState s = states.get(split); 104 | int bits = protocol.getEstimatedBitSize(s); 105 | if( size + bits > limit ) 106 | break; 107 | size += bits; 108 | split++; 109 | } 110 | if( split == 0 || split == states.size() ) { 111 | throw new RuntimeException( "Error splitting message. split:" + split + " limit:" + limit); 112 | } 113 | long leftOverBits = estimatedBitSize - size; 114 | 115 | // Create a new frame with the left-overs 116 | FrameState result = new FrameState(time, legacySequence+1, columnId); 117 | result.states = states.subList(split, states.size()); 118 | result.estimatedBitSize += leftOverBits; // add them to the header size 119 | 120 | // Now reset this frame's fields accordingly 121 | estimatedBitSize = size; 122 | states = states.subList(0, split); 123 | 124 | return result; 125 | } 126 | 127 | 128 | public void writeBits( BitOutputStream out, ObjectStateProtocol protocol ) throws IOException { 129 | out.writeLongBits(time, 64); 130 | out.writeLongBits(legacySequence, 64); 131 | out.writeLongBits(columnId, 64); 132 | 133 | // Write the list size as 16 bits... we could probably get 134 | // away with much less but it's hard to be sure 135 | out.writeBits(states.size(), 16); 136 | 137 | for( ObjectState s : states ) { 138 | protocol.writeBits(s, out); 139 | } 140 | } 141 | 142 | public void readBits( BitInputStream in, ObjectStateProtocol protocol ) throws IOException { 143 | 144 | this.time = in.readLongBits(64); 145 | this.legacySequence = in.readLongBits(64); 146 | this.columnId = in.readLongBits(64); 147 | 148 | int size = in.readBits(16); 149 | states.clear(); 150 | for( int i = 0; i < size; i++ ) { 151 | ObjectState s = protocol.readBits(in); 152 | states.add(s); 153 | } 154 | } 155 | 156 | @Override 157 | public String toString() { 158 | return "FrameState[time=" + time + ", legacySequence=" + legacySequence + ", columnId=" + columnId + ", states=" + states + "]"; 159 | } 160 | } 161 | 162 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/net/ObjectState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.net; 38 | 39 | import java.util.Objects; 40 | 41 | 42 | /** 43 | * 44 | * 45 | * @author Paul Speed 46 | */ 47 | public class ObjectState implements Cloneable { 48 | 49 | /** 50 | * For a field like 'parent', null indicates that there is no change in state. 51 | * So we'll use a special constant to denote the lack of a parent. This 52 | * makes it easier to deal with the value reading/writing since there is no 53 | * real way to write a "null" because 0 is a valid ID. We could keep a separate 54 | * flag for whether a parent has been set or not but I think burning the ID of -1 55 | * for regular use is a small price to pay to avoid an extra field. -pspeed:2020-07-04 56 | */ 57 | public static long NO_PARENT = -1L; 58 | 59 | public int networkId; 60 | public int zoneId; 61 | public Long realId; 62 | public Long parentId; 63 | public long positionBits; 64 | public long rotationBits; 65 | 66 | public ObjectState() { 67 | this(0, null); 68 | } 69 | 70 | public ObjectState( int networkId ) { 71 | this(networkId, null); 72 | } 73 | 74 | public ObjectState( int networkId, Long realId ) { 75 | this.networkId = networkId; 76 | this.realId = realId; 77 | this.zoneId = -1; 78 | this.positionBits = -1; 79 | this.rotationBits = -1; 80 | } 81 | 82 | @Override 83 | public ObjectState clone() { 84 | try { 85 | return (ObjectState)super.clone(); 86 | } catch( CloneNotSupportedException e ) { 87 | throw new RuntimeException("Should never happen", e); 88 | } 89 | } 90 | 91 | public void set( ObjectState state ) { 92 | this.networkId = state.networkId; 93 | this.zoneId = state.zoneId; 94 | this.realId = state.realId; 95 | this.parentId = state.parentId; 96 | this.positionBits = state.positionBits; 97 | this.rotationBits = state.rotationBits; 98 | } 99 | 100 | /** 101 | * Returns true if this object is marked for removal at this 102 | * state. 103 | */ 104 | public boolean isMarkedRemoved() { 105 | //return parentId == null && zoneId == 0; 106 | // I think now that we are implementing parentId 'for real' that 107 | // it's not right to clear it for removal. -pspeed:2020-06-15 108 | return zoneId == 0; 109 | } 110 | 111 | public void markRemoved() { 112 | // I think this clearing of the parent ID is wrong in today's light. 113 | // But it is many years later and parentId was only used 'for real' 114 | // now. -pspeed:2020-06-15 115 | //this.parentId = null; 116 | this.zoneId = 0; 117 | } 118 | 119 | public ObjectState getDelta( ObjectState baseline ) { 120 | if( baseline == null ) { 121 | return clone(); 122 | } 123 | 124 | ObjectState result = new ObjectState(networkId); 125 | 126 | if( zoneId != baseline.zoneId ) { 127 | result.zoneId = zoneId; 128 | } 129 | 130 | if( !Objects.equals(realId, baseline.realId) ) { 131 | result.realId = realId; 132 | } 133 | 134 | if( !Objects.equals(parentId, baseline.parentId) ) { 135 | result.parentId = parentId; 136 | } 137 | 138 | if( positionBits != baseline.positionBits ) { 139 | result.positionBits = positionBits; 140 | } 141 | 142 | if( rotationBits != baseline.rotationBits ) { 143 | result.rotationBits = rotationBits; 144 | } 145 | 146 | return result; 147 | } 148 | 149 | public void applyDelta( ObjectState delta ) { 150 | 151 | if( delta.zoneId != -1 ) { 152 | zoneId = delta.zoneId; 153 | } 154 | if( delta.parentId != null ) { 155 | parentId = delta.parentId; 156 | } 157 | if( delta.positionBits != -1 ) { 158 | positionBits = delta.positionBits; 159 | } 160 | if( delta.rotationBits != -1 ) { 161 | rotationBits = delta.rotationBits; 162 | } 163 | } 164 | 165 | @Override 166 | public String toString() { 167 | StringBuilder sb = new StringBuilder(); 168 | sb.append(getClass().getName()); 169 | sb.append("[id=").append(networkId); 170 | if( realId != null ) 171 | sb.append(", realId=").append(realId); 172 | if( parentId != null ) 173 | sb.append(", parentId=").append(parentId); 174 | if( zoneId == 0 && parentId == null ) 175 | sb.append(", REMOVED"); 176 | else if( zoneId != -1 ) 177 | sb.append(", zoneId=").append(zoneId); 178 | if( positionBits != -1 ) 179 | sb.append(", positionBits=").append(Long.toHexString(positionBits)); 180 | if( rotationBits != -1 ) 181 | sb.append(", rotationBits=").append(Long.toHexString(rotationBits)); 182 | sb.append("]"); 183 | return sb.toString(); 184 | } 185 | } 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/net/ObjectStateMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.net; 38 | 39 | import com.jme3.network.AbstractMessage; 40 | import com.jme3.network.serializing.Serializable; 41 | import java.io.IOException; 42 | 43 | 44 | /** 45 | * 46 | * 47 | * @author Paul Speed 48 | */ 49 | @Serializable 50 | public class ObjectStateMessage extends AbstractMessage { 51 | 52 | public static final int HEADER_SIZE = 2 + 8 + 1 + 4; // id, time, null byte for buffer, size of buffer 53 | 54 | private int id; // the message sequence number 55 | private long time; // for ping tracking 56 | private byte[] buffer; 57 | 58 | public ObjectStateMessage() { 59 | } 60 | 61 | public ObjectStateMessage( int id, long nanoTime, byte[] buffer ) { 62 | this.id = id; 63 | this.time = nanoTime; 64 | this.buffer = buffer; 65 | } 66 | 67 | public int getId() { 68 | return id; 69 | } 70 | 71 | public long getTime() { 72 | return time; 73 | } 74 | 75 | public byte[] getBuffer() { 76 | return buffer; 77 | } 78 | 79 | public SentState getState( ObjectStateProtocol protocol ) { 80 | if( buffer == null ) { 81 | return null; 82 | } 83 | try { 84 | return SentState.fromByteArray(id, buffer, protocol); 85 | } catch( IOException e ) { 86 | throw new RuntimeException("Error reading frame states", e); 87 | } 88 | } 89 | 90 | public void setState( SentState state, ObjectStateProtocol protocol ) { 91 | if( state == null ) { 92 | buffer = null; 93 | return; 94 | } 95 | try { 96 | state.messageId = id; 97 | this.buffer = SentState.toByteArray(state, protocol); 98 | } catch( IOException e ) { 99 | throw new RuntimeException("Error writing frame states", e); 100 | } 101 | } 102 | 103 | @Override 104 | public String toString() { 105 | if( buffer == null ) { 106 | return "ObjectStateMessage[]"; 107 | } 108 | 109 | return "ObjectStateMessage[id=" + id + ", time=" + time + ", size=" + buffer.length + "]"; 110 | } 111 | } 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/net/ObjectStateProtocol.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.net; 38 | 39 | import com.simsilica.mathd.Quatd; 40 | import com.simsilica.mathd.Vec3d; 41 | import com.simsilica.mathd.bits.QuatBits; 42 | import com.simsilica.mathd.bits.Vec3Bits; 43 | import com.simsilica.ethereal.io.BitInputStream; 44 | import com.simsilica.ethereal.io.BitOutputStream; 45 | import java.io.IOException; 46 | 47 | 48 | /** 49 | * Holds information about the bit sizes of various 50 | * ObjectState fields. 51 | * 52 | * @author Paul Speed 53 | */ 54 | public class ObjectStateProtocol { 55 | 56 | public int zoneIdBitSize; 57 | public int idBitSize; 58 | public Vec3Bits positionBits; 59 | public QuatBits rotationBits; 60 | 61 | public ObjectStateProtocol( int zoneIdBitSize, int idBitSize, 62 | Vec3Bits posBits, QuatBits rotBits ) { 63 | this.zoneIdBitSize = zoneIdBitSize; 64 | this.idBitSize = idBitSize; 65 | this.positionBits = posBits; 66 | this.rotationBits = rotBits; 67 | } 68 | 69 | public void setPosition( ObjectState state, Vec3d pos ) { 70 | state.positionBits = positionBits.toBits(pos); 71 | } 72 | 73 | public Vec3d getPosition( ObjectState state ) { 74 | return positionBits.fromBits(state.positionBits); 75 | } 76 | 77 | public void setRotation( ObjectState state, Quatd rot ) { 78 | state.rotationBits = rotationBits.toBits(rot); 79 | } 80 | 81 | public Quatd getRotation( ObjectState state ) { 82 | return rotationBits.fromBits(state.rotationBits); 83 | } 84 | 85 | public int getEstimatedBitSize( ObjectState state ) { 86 | // Basic state layout 87 | // networkdId: 16 bits 88 | // ?hasRealId: 1 bit 89 | // -realId: idBitSize bits 90 | // ?hasZone: 1 bit 91 | // -parent: zoneIdBitSize bits 92 | // ?hasParent: 1 bit 93 | // -parent: idBitSize bits 94 | // ?hasPosition: 1 bit 95 | // -position: posBits bits 96 | // ?hasRotation: 1 bit 97 | // -rotation: rotBits bits 98 | int size = 16; 99 | 100 | size++; 101 | if( state.zoneId != -1 ) { 102 | size += zoneIdBitSize; 103 | } 104 | 105 | size++; 106 | if( state.realId != null ) { 107 | size += idBitSize; 108 | } 109 | 110 | size++; 111 | if( state.parentId != null ) { 112 | size += idBitSize; 113 | } 114 | 115 | size++; 116 | if( state.positionBits != -1 ) { 117 | size += positionBits.getBitSize(); 118 | } 119 | 120 | size++; 121 | if( state.rotationBits != -1 ) { 122 | size += rotationBits.getBitSize(); 123 | } 124 | 125 | return size; 126 | } 127 | 128 | public void writeBits( ObjectState state, BitOutputStream out ) throws IOException { 129 | 130 | if( state == null ) { 131 | out.writeBits(0, 16); 132 | return; 133 | } 134 | 135 | out.writeBits(state.networkId, 16); 136 | if( state.networkId == 0 ) { 137 | // Nothing else to write... and it might be a bug 138 | throw new IllegalArgumentException("Object state networkId is 0"); 139 | } 140 | 141 | if( state.zoneId == -1 ) { 142 | out.writeBits(0, 1); 143 | } else { 144 | out.writeBits(1, 1); 145 | out.writeBits(state.zoneId, zoneIdBitSize); 146 | } 147 | 148 | if( state.realId == null ) { 149 | out.writeBits(0, 1); 150 | } else { 151 | out.writeBits(1, 1); 152 | out.writeLongBits(state.realId, idBitSize); 153 | } 154 | 155 | if( state.parentId == null ) { 156 | out.writeBits(0, 1); 157 | } else { 158 | out.writeBits(1, 1); 159 | out.writeLongBits(state.parentId, idBitSize); 160 | } 161 | 162 | if( state.positionBits == -1 ) { 163 | out.writeBits(0, 1); 164 | } else { 165 | out.writeBits(1, 1); 166 | out.writeLongBits(state.positionBits, positionBits.getBitSize()); 167 | } 168 | 169 | if( state.rotationBits == -1 ) { 170 | out.writeBits(0, 1); 171 | } else { 172 | out.writeBits(1, 1); 173 | out.writeLongBits(state.rotationBits, rotationBits.getBitSize()); 174 | } 175 | } 176 | 177 | public ObjectState readBits( BitInputStream in ) throws IOException { 178 | 179 | int networkId = in.readBits(16); 180 | if( networkId == 0 ) { 181 | return null; 182 | } 183 | ObjectState state = new ObjectState(); 184 | state.networkId = networkId; 185 | 186 | int bit; 187 | bit = in.readBits(1); 188 | if( bit != 0 ) { 189 | state.zoneId = in.readBits(zoneIdBitSize); 190 | } 191 | 192 | bit = in.readBits(1); 193 | if( bit != 0 ) { 194 | state.realId = in.readLongBits(idBitSize); 195 | } 196 | 197 | bit = in.readBits(1); 198 | if( bit != 0 ) { 199 | state.parentId = in.readLongBits(idBitSize); 200 | } 201 | 202 | bit = in.readBits(1); 203 | if( bit != 0 ) { 204 | state.positionBits = in.readLongBits(positionBits.getBitSize()); 205 | } 206 | 207 | bit = in.readBits(1); 208 | if( bit != 0 ) { 209 | state.rotationBits = in.readLongBits(rotationBits.getBitSize()); 210 | } 211 | 212 | return state; 213 | } 214 | 215 | } 216 | 217 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/net/RemoteTimeSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.net; 38 | 39 | import com.simsilica.ethereal.DebugUtils; 40 | import com.simsilica.ethereal.Statistics; 41 | import com.simsilica.ethereal.Statistics.Sequence; 42 | import com.simsilica.ethereal.SynchedTimeSource; 43 | import org.slf4j.Logger; 44 | import org.slf4j.LoggerFactory; 45 | 46 | 47 | /** 48 | * Keeps track of the latest time included in messages from 49 | * the server to guess at a consistent time offset for this client. 50 | * The idea is to roughly simulate that the times coming in from the 51 | * server represent "now" when they arrive. 52 | * 53 | * @author Paul Speed 54 | */ 55 | public class RemoteTimeSource implements SynchedTimeSource { 56 | 57 | static Logger log = LoggerFactory.getLogger(RemoteTimeSource.class); 58 | 59 | // Even if multiple threads are calling getTime() this should 60 | // be safe to leave unvolatile because the limit is ok to be 61 | // thread specific in cases where the lastTime value is out of sync. 62 | private long lastTime = 0; // time should never go backwards 63 | 64 | // Adjusted by updateDrift() and used by getTime()... needs to be 65 | // volatile to make sure everyone is up to date. 66 | private volatile long drift = 0; 67 | private volatile boolean uninitialized = true; 68 | 69 | // Should be set once during init or early on. No need for 70 | // volatile and anyway all of the other volatiles will force a 71 | // memory barrier. 72 | private long offset = 0; 73 | 74 | // Only used from the updateDrift() call which should be single-threaded 75 | // so volatile is not needed. 76 | private long lastServerTime = 0; 77 | private long windowMax = 100; 78 | private long windowSize = 0; 79 | 80 | // For debug tracking 81 | private Sequence syncTime; 82 | 83 | public RemoteTimeSource() { 84 | this(0); 85 | } 86 | 87 | public RemoteTimeSource( long offset ) { 88 | this.offset = offset; 89 | 90 | // Need to have a flag for this somehow 91 | this.syncTime = Statistics.getSequence("syncTime", true); 92 | } 93 | 94 | @Override 95 | public void setOffset( long offset ) { 96 | this.offset = offset; 97 | } 98 | 99 | @Override 100 | public long getOffset() { 101 | return offset; 102 | } 103 | 104 | protected void updateDrift( long serverTime ) { 105 | 106 | syncTime.add(serverTime); 107 | 108 | this.lastServerTime = serverTime; 109 | long t = System.nanoTime(); 110 | 111 | // What do we have to 'add' to our time to get server time. 112 | long delta = serverTime - t; 113 | 114 | // Calculate the running average for drift... we want 115 | // drift to vary slowly. 116 | long newDrift = (delta + drift * windowSize) / (windowSize + 1); 117 | if( log.isDebugEnabled() ) { 118 | log.debug("======== Time delta:" + DebugUtils.timeString(delta) + " drift:" + DebugUtils.timeString(newDrift) + " windowSize:" + windowSize); 119 | log.debug("=== oldDrift:" + drift + " drift change:" + DebugUtils.timeString(newDrift - drift)); 120 | } 121 | drift = newDrift; 122 | if( windowSize < windowMax ) { 123 | windowSize++; 124 | } 125 | 126 | uninitialized = false; 127 | } 128 | 129 | public void update( ObjectStateMessage msg ) { 130 | long t = msg.getTime(); 131 | if( t > lastServerTime ) { 132 | updateDrift(t); 133 | } 134 | } 135 | 136 | @Override 137 | public long getTime() { 138 | if( uninitialized ) { 139 | return 0; 140 | } 141 | long raw = System.nanoTime(); 142 | long t = raw + drift + offset; 143 | if( t > lastTime ) { 144 | lastTime = t; 145 | } else { 146 | if( log.isWarnEnabled() ) { 147 | log.warn("Time didn't advance:" + (lastTime - t) 148 | + " nanos. Current:" + t 149 | + " Last:" + lastTime 150 | + " Raw:" + raw 151 | + " Drift:" + drift 152 | + " Offset:" + offset); 153 | } 154 | } 155 | return lastTime; 156 | } 157 | 158 | @Override 159 | public long getDrift() { 160 | return drift; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/net/SentState.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.net; 38 | 39 | import java.io.ByteArrayInputStream; 40 | import java.io.ByteArrayOutputStream; 41 | import java.io.IOException; 42 | import java.util.ArrayList; 43 | import java.util.Arrays; 44 | import java.util.List; 45 | 46 | import com.simsilica.mathd.util.*; 47 | 48 | import com.simsilica.ethereal.io.BitInputStream; 49 | import com.simsilica.ethereal.io.BitOutputStream; 50 | 51 | /** 52 | * 53 | * 54 | * @author Paul Speed 55 | */ 56 | public class SentState { 57 | 58 | public long created = System.nanoTime(); 59 | public int messageId; 60 | public IntRange[] acked; 61 | public List frames; 62 | 63 | public SentState( int messageId, IntRange[] acked, List frames ) { 64 | this.messageId = messageId; 65 | this.acked = acked; 66 | this.frames = frames; 67 | } 68 | 69 | public boolean isBefore( SentState state ) { 70 | return isBefore(state.messageId); 71 | } 72 | 73 | /** 74 | * Returns true if this state is before the specified messageId. Message IDs 75 | * wrap so a simple < check is not enough. 76 | */ 77 | public boolean isBefore( int compare ) { 78 | if( Math.abs(messageId - compare) > 32000 ) { 79 | // Then we'll assume that the IDs have wrapped 80 | // so our comparison is backwards. 81 | return true; 82 | } 83 | return messageId < compare; 84 | } 85 | 86 | 87 | /** 88 | * Returns the number of bits that will be written for the header. 89 | */ 90 | public int getEstimatedHeaderSize() { 91 | int result = 0; 92 | result += 8; // array size 93 | result += acked == null ? 0 : acked.length * 48; // array values 94 | return result; 95 | } 96 | 97 | public static SentState fromByteArray( int sequenceId, byte[] buffer, ObjectStateProtocol protocol ) throws IOException { 98 | 99 | ByteArrayInputStream bIn = new ByteArrayInputStream(buffer); 100 | BitInputStream in = new BitInputStream(bIn); 101 | try { 102 | List frames = new ArrayList<>(); 103 | 104 | // First read the acks array 105 | int size = in.readBits(8); 106 | IntRange[] acks = new IntRange[size]; 107 | for( int i = 0; i < size; i++ ) { 108 | int min = in.readBits(32); 109 | int length = in.readBits(16); 110 | acks[i] = new FixedIntRange(min, min + length - 1); 111 | } 112 | 113 | // Then read each frame... this protocol presumes that a bit 114 | // per frame will ultimately be smaller than a fixed size count, on average. 115 | while( in.readBits(1) == 1 ) { 116 | FrameState frame = new FrameState(); 117 | frame.readBits(in, protocol); 118 | frames.add(frame); 119 | } 120 | 121 | return new SentState(sequenceId, acks, frames); 122 | } finally { 123 | in.close(); 124 | } 125 | } 126 | 127 | public static byte[] toByteArray( SentState state, ObjectStateProtocol protocol ) throws IOException { 128 | ByteArrayOutputStream bOut = new ByteArrayOutputStream(); 129 | BitOutputStream out = new BitOutputStream(bOut); 130 | 131 | // Write the acks array first 132 | out.writeBits(state.acked.length, 8); 133 | for( IntRange range : state.acked ) { 134 | out.writeBits(range.getMinValue(), 32); 135 | out.writeBits(range.getLength(), 16); 136 | } 137 | 138 | // Now write the frames with their marker bit 139 | for( FrameState frame : state.frames ) { 140 | out.writeBits(1, 1); 141 | frame.writeBits(out, protocol); 142 | } 143 | 144 | // And the empty bit 145 | out.writeBits(0, 1); 146 | 147 | out.close(); 148 | 149 | return bOut.toByteArray(); 150 | } 151 | 152 | @Override 153 | public String toString() { 154 | StringBuilder sb = new StringBuilder(); 155 | sb.append(Arrays.asList(acked).toString()); 156 | return "SentState[messageId=" + messageId + ", created=" + created + ", acked=[" + sb + "], frames=" + frames + "]"; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/net/StateReceiver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.net; 38 | 39 | import java.util.*; 40 | 41 | import org.slf4j.Logger; 42 | import org.slf4j.LoggerFactory; 43 | 44 | import com.jme3.network.Client; 45 | 46 | import com.simsilica.mathd.util.*; 47 | 48 | import com.simsilica.ethereal.LocalZoneIndex; 49 | import com.simsilica.ethereal.SharedObject; 50 | import com.simsilica.ethereal.SharedObjectSpace; 51 | import com.simsilica.ethereal.Statistics; 52 | import com.simsilica.ethereal.Statistics.Sequence; 53 | import com.simsilica.ethereal.Statistics.Tracker; 54 | import com.simsilica.ethereal.zone.ZoneGrid; 55 | import com.simsilica.ethereal.zone.ZoneKey; 56 | 57 | 58 | 59 | /** 60 | * Handles the incoming ObjectStateMessage and uses it to update 61 | * the local SharedObjectSpace. 62 | * 63 | * @author Paul Speed 64 | */ 65 | public class StateReceiver { 66 | 67 | static Logger log = LoggerFactory.getLogger(StateReceiver.class); 68 | 69 | private final Client client; 70 | private final ObjectStateProtocol objectProtocol; 71 | 72 | private final SharedObjectSpace space; 73 | private final LocalZoneIndex zoneIndex; 74 | private final ZoneGrid grid; 75 | 76 | private final RemoteTimeSource timeSource; 77 | 78 | /** 79 | * Track the states we've received by message ID so that we 80 | * can update our baseline with the double-acks from the server. 81 | */ 82 | private final Map receivedStates = new TreeMap<>(); 83 | 84 | private long lastFrameTime; 85 | 86 | private final Sequence frameTime; 87 | private final Tracker messageSize; 88 | 89 | public StateReceiver( Client client, LocalZoneIndex zoneIndex, SharedObjectSpace space ) { 90 | this.client = client; 91 | this.space = space; 92 | this.zoneIndex = zoneIndex; 93 | this.grid = zoneIndex.getGrid(); 94 | this.objectProtocol = space.getObjectProtocol(); 95 | this.timeSource = new RemoteTimeSource(-100 * 1000000L); // -100 ms in the past to start with 96 | 97 | this.frameTime = Statistics.getSequence("stateTime", true); 98 | this.messageSize = Statistics.getTracker("messageSize", 5, true); 99 | } 100 | 101 | public RemoteTimeSource getTimeSource() { 102 | return timeSource; 103 | } 104 | 105 | public void handleMessage( ObjectStateMessage msg ) { 106 | 107 | timeSource.update(msg); 108 | 109 | if( log.isDebugEnabled() ) { 110 | log.debug("Update state:" + msg); 111 | } 112 | 113 | // Very first thing we do is acknowledge the message 114 | // 2022-11-05 - I believe that sending these reliably is why we can 115 | // just ack this single message and not keep a running set of all 116 | // acknowledged messages to send back. We know the server will eventually 117 | // see this and we don't particularly care how long it takes. 118 | client.send(new ClientStateMessage(msg, 0)); 119 | 120 | // Collect the statistics 121 | messageSize.update(ObjectStateMessage.HEADER_SIZE + msg.getBuffer().length); 122 | 123 | // Grab the state and track it for later. We will 124 | // apply this state to our 'current' versions but we will 125 | // also keep it around to update the baseline version when 126 | // we are sure the server knows that we have received it. 127 | SentState state = msg.getState(objectProtocol); 128 | 129 | if( log.isDebugEnabled() ) { 130 | log.debug("State:" + state); 131 | } 132 | receivedStates.put(state.messageId, state); 133 | 134 | processAcks(state.acked); 135 | 136 | 137 | // Now the baselines are properly setup for interpretation 138 | // of the incoming delta messages. All messages are interpretted 139 | // as deltas from the object baselines so it is important to make 140 | // sure they are synched with what the server thinks they are... 141 | // and that's what the ACK message list is for. The server 142 | // is telling us: 143 | // "These are the messages that you said you already received that 144 | // I'm basing my baselines on" 145 | // 146 | // ...which in the end is all about reducing state update sizes by 147 | // leaving out information that is relatively stable. And reducing 148 | // state update sizes means we can fit more object updates in a single 149 | // message. 150 | 151 | // For through all of the frames 152 | for( FrameState frame : state.frames ) { 153 | 154 | if( frame.time < lastFrameTime ) { 155 | continue; 156 | } 157 | lastFrameTime = frame.time; 158 | 159 | if( log.isDebugEnabled() ) { 160 | log.debug("** frame begin:" + frame.time); 161 | } 162 | 163 | frameTime.add(frame.time); 164 | 165 | space.beginFrame(lastFrameTime); 166 | 167 | ZoneKey center = grid.fromLongId(frame.columnId); 168 | 169 | // Make sure the local zone grid is updated so that 170 | // zoneIds can be reinterpretted 171 | if( zoneIndex.setCenter(center, new ArrayList(), new ArrayList()) ) { 172 | // The zone index has changed centers and so any zoneIds 173 | // now mean something different. However, we can only update 174 | // the ones we get state changes for because the ZoneKeys for 175 | // an object may not have actually changed with the grid change. 176 | } 177 | 178 | for( ObjectState objectState : frame.states ) { 179 | 180 | SharedObject so; 181 | 182 | if( objectState.realId != null ) { 183 | so = space.getObject(objectState.networkId, objectState.realId); 184 | } else { 185 | so = space.getObject(objectState.networkId); 186 | if( so == null ) { 187 | // This should be the sign of a problem, I think. 188 | // Either we are getting updates for an object we've never 189 | // seen a realId for (how could it happen if we never ACKed 190 | // a baseline with the realID.) or we are receiving updates 191 | // for an object we have deleted... apparently too soon. 192 | // I'm going to print an error to see if it shows up... could 193 | // be an exception, too, but I want to see if it's relatively 194 | // normal or not first. 195 | log.warn("********* Network ID lookup returned null. State:" + objectState 196 | + " messageId:" + state.messageId ); 197 | // I'm seeing these in an application with lots of objects. The objectState 198 | // seems to be empty, though. 199 | continue; 200 | } 201 | } 202 | 203 | // Resolve the zoneId 204 | 205 | // Apply the changes 206 | if( so.applyNetworkState(frame.time, objectState, zoneIndex) ) { 207 | // The object changed for real... we can notify any listeners 208 | // to update state, etc. SharedObjectSpace will do that for us. 209 | 210 | // If the object was removed for real then we need to remove 211 | // it from the space, also. 212 | if( so.isFullyMarkedRemoved() ) { 213 | // The object is marked removed in both current and baseline 214 | // so no reason to track it anymore. 215 | space.removeObject(so); 216 | } 217 | } 218 | 219 | } 220 | 221 | space.endFrame(); 222 | } 223 | 224 | } 225 | 226 | protected void processAcks( IntRange[] acked ) { 227 | // The server has acknowledged a certain number of our ACKs and 228 | // those represent the new baseline. So we need to apply them to our 229 | // local state. We are guaranteed to have _ALL_ of these because we 230 | // were the one who told the server that we had them... if we don't 231 | // have them it's only because the server has sent us redundant ACKs. 232 | // This should always be the case. Even if we missed messages it 233 | // doesn't matter because the server keeps sending the same values 234 | // until we have processed them for real. In other words, 235 | // the server only stops sending a particular ID if we have 236 | // received that message, ACKed it, the server got the ACK, the server 237 | // included it in ANOTHER message and we ACKed that one too. 238 | for( IntRange range : acked ) { 239 | int min = range.getMinValue(); 240 | int max = range.getMaxValue(); 241 | for( int ackedId = min; ackedId <= max; ackedId++ ) { 242 | SentState sentState = ackReceivedState(ackedId); 243 | 244 | // It is normal that we might not have a sent state anymore 245 | // because we might have previously ack'ed our ack. The server 246 | // keeps sending it until we claim we have it so we may get 247 | // it multiple times while the server waits for our first 248 | // ACK. 249 | if( sentState == null ) { 250 | // Totally normal. See above. We may see the same acked 251 | // ID hundreds of times if there is a message lag for our 252 | // responses to the server. 253 | continue; 254 | } 255 | 256 | List old = sentState.frames; 257 | if( old != null ) { 258 | if( log.isDebugEnabled() ) { 259 | log.debug("Updating baseline for message:" + ackedId ); 260 | } 261 | space.updateBaseline(old); 262 | } 263 | } 264 | } 265 | } 266 | 267 | /** 268 | * Called when we receive our own ACK back so that we can 269 | * update the baseline with our "known good" state. 270 | */ 271 | protected SentState ackReceivedState( int messageId ) { 272 | 273 | if( receivedStates.isEmpty() ) 274 | return null; 275 | 276 | // Note: the only reason a messageId shows up here is 277 | // because we've told the server we already have it. So 278 | // it's 100% guaranteed to be here unless we've already 279 | // processed it. No exceptions. 280 | // We are safe to purge the old ones because the server 281 | // always sends them in order. 282 | 283 | // Scan forward through the list, removing the stale 284 | // SentStates. 285 | for( Iterator it = receivedStates.values().iterator(); it.hasNext(); ) { 286 | SentState state = it.next(); 287 | if( state.messageId < messageId ) { //state.isBefore(messageId) ) { 288 | // Remove it and skip it... 289 | it.remove(); 290 | if( log.isTraceEnabled() ) { 291 | log.trace("Skipping stale state:" + state + " for messageId:" + messageId); 292 | } else { 293 | log.info("Skipping stale state for messageId:" + messageId); 294 | } 295 | continue; 296 | } 297 | 298 | if( state.messageId == messageId ) { 299 | // We found it 300 | it.remove(); 301 | return state; 302 | } 303 | 304 | // Else it is a state that we've already processed at some 305 | // earlier time as any next message will be after us 306 | return null; 307 | } 308 | 309 | return null; 310 | } 311 | 312 | } 313 | 314 | 315 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/zone/StateBlock.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.zone; 38 | 39 | import java.util.ArrayList; 40 | import java.util.List; 41 | 42 | import com.simsilica.mathd.Quatd; 43 | import com.simsilica.mathd.Vec3d; 44 | 45 | 46 | /** 47 | * 48 | * 49 | * @author Paul Speed 50 | */ 51 | public class StateBlock { 52 | private final long time; 53 | private final ZoneKey zone; 54 | private List updates; 55 | private List removes; 56 | private List warps; 57 | 58 | public StateBlock( long time, ZoneKey zone ) { 59 | this.time = time; 60 | this.zone = zone; 61 | } 62 | 63 | public ZoneKey getZone() { 64 | return zone; 65 | } 66 | 67 | public boolean isEmpty() { 68 | return updates == null && removes == null && warps == null; 69 | } 70 | 71 | public void addUpdate( Long parent, Long entity, Vec3d pos, Quatd rot ) { 72 | if( updates == null ) { 73 | updates = new ArrayList<>(); 74 | } 75 | updates.add(new StateEntry(parent, entity, pos, rot)); 76 | } 77 | 78 | public void removeEntity( Long entity ) { 79 | if( removes == null ) { 80 | removes = new ArrayList<>(); 81 | } 82 | removes.add(entity); 83 | } 84 | 85 | public void addWarp( Long parent, Long entity ) { 86 | if( warps == null ) { 87 | warps = new ArrayList<>(); 88 | } 89 | // Technically we probably only care about entities with 90 | // no parent... but what if a 'self' was attached as a child to 91 | // some ridable object? So we'll track them all. 92 | warps.add(entity); 93 | } 94 | 95 | public long getTime() { 96 | return time; 97 | } 98 | 99 | public List getUpdates() { 100 | return updates; 101 | } 102 | 103 | public List getRemovals() { 104 | return removes; 105 | } 106 | 107 | public List getWarps() { 108 | return warps; 109 | } 110 | 111 | @Override 112 | public String toString() { 113 | StringBuilder sb = new StringBuilder("StateBlock[time=" + time + ", zone=" + zone); 114 | if( updates != null ) { 115 | sb.append( ", updates=" + updates ); 116 | } 117 | if( removes != null ) { 118 | sb.append( ", removes=" + removes ); 119 | } 120 | sb.append( "]" ); 121 | return sb.toString(); 122 | } 123 | 124 | public static class StateEntry { 125 | private final Long parent; 126 | private final Long entity; 127 | private final Vec3d pos; 128 | private final Quatd rot; 129 | 130 | public StateEntry( Long parent, Long entity, Vec3d pos, Quatd rot ) { 131 | this.parent = parent; 132 | this.entity = entity; 133 | this.pos = pos; 134 | this.rot = rot; 135 | } 136 | 137 | public Long getParent() { 138 | return parent; 139 | } 140 | 141 | public Long getEntity() { 142 | return entity; 143 | } 144 | 145 | public Vec3d getPosition() { 146 | return pos; 147 | } 148 | 149 | public Quatd getRotation() { 150 | return rot; 151 | } 152 | 153 | @Override 154 | public String toString() { 155 | return "StateEntry[" + (parent != null ? (parent + ", ") : "") + entity + ", " + pos + ", " + rot + "]"; 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/zone/StateCollector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.zone; 38 | 39 | import java.util.ArrayList; 40 | import java.util.HashMap; 41 | import java.util.HashSet; 42 | import java.util.List; 43 | import java.util.Map; 44 | import java.util.Set; 45 | import java.util.concurrent.ConcurrentLinkedQueue; 46 | import java.util.concurrent.CopyOnWriteArraySet; 47 | import java.util.concurrent.atomic.AtomicBoolean; 48 | import org.slf4j.Logger; 49 | import org.slf4j.LoggerFactory; 50 | 51 | 52 | /** 53 | * Uses a background thread to periodically collect the accumulated 54 | * state for the zones and send the blocks of state to zone state 55 | * listeners. 56 | * 57 | * @author Paul Speed 58 | */ 59 | public class StateCollector { 60 | 61 | static Logger log = LoggerFactory.getLogger(StateCollector.class); 62 | 63 | private static final long NANOS_PER_SEC = 1000000000L; 64 | public static final long DEFAULT_PERIOD = NANOS_PER_SEC / 20; 65 | 66 | private ZoneManager zones; 67 | private long collectionPeriod; 68 | private long idleSleepTime = 1; // for our standard 20 FPS that's more than fine 69 | private final Runner runner = new Runner(); 70 | 71 | private final Set listeners = new CopyOnWriteArraySet<>(); 72 | private final ConcurrentLinkedQueue removed = new ConcurrentLinkedQueue<>(); 73 | 74 | /** 75 | * This is the actual zone interest management. It's only used by the 76 | * background thread which is why it is unsynchronized. All interactio 77 | * is done through the listeners set and removed queue. 78 | */ 79 | private final Map> zoneListeners = new HashMap<>(); 80 | 81 | public StateCollector( ZoneManager zones ) { 82 | this(zones, DEFAULT_PERIOD); 83 | } 84 | 85 | public StateCollector( ZoneManager zones, long collectionPeriod ) { 86 | this.zones = zones; 87 | this.collectionPeriod = collectionPeriod == 0 ? DEFAULT_PERIOD : collectionPeriod; 88 | } 89 | 90 | public void start() { 91 | log.info("Starting state collector."); 92 | runner.start(); 93 | } 94 | 95 | public void shutdown() { 96 | log.info("Shutting down state collector."); 97 | runner.close(); 98 | } 99 | 100 | /** 101 | * Adds a listener that self-indicates which specific zones 102 | * it is interested in from one frame to the next. This is necessary 103 | * so that it syncs with the background state collection in a way 104 | * that does not cause partial state, etc... it's also nicer to 105 | * to the background threads and synchronization if zone interest 106 | * is synched with updates. 107 | */ 108 | public void addListener( StateListener l ) { 109 | listeners.add(l); 110 | } 111 | 112 | public void removeListener( StateListener l ) { 113 | listeners.remove(l); 114 | removed.add(l); 115 | } 116 | 117 | /** 118 | * Sets the sleep() time for the state collector's idle periods. 119 | * This defaults to 1 which keeps the CPU happier while also providing 120 | * timely checks against the interval time. However, for higher rates of 121 | * state collection (lower collectionPeriods such as 16 ms or 60 FPS) on windows, 122 | * sleep(1) may take longer than 1/60th of a second or close enough to still 123 | * cause collection frame drops. In that case, it can be configured to 0 which 124 | * should provide timelier updates. 125 | * Set to -1 to avoid sleeping at all in which case the thread will consume 100% 126 | * of a single core in order to busy wait between collection intervals. 127 | */ 128 | public void setIdleSleepTime( long millis ) { 129 | this.idleSleepTime = millis; 130 | } 131 | 132 | public long getIdleSleepTime() { 133 | return idleSleepTime; 134 | } 135 | 136 | protected List getListeners( ZoneKey key, boolean create ) { 137 | List result = zoneListeners.get(key); 138 | if( result == null && create ) { 139 | result = new ArrayList<>(); 140 | zoneListeners.put(key, result); 141 | } 142 | return result; 143 | } 144 | 145 | protected void watch( ZoneKey key, StateListener l ) { 146 | if( log.isTraceEnabled() ) { 147 | log.trace("watch(" + key + ", " + l + ")"); 148 | } 149 | getListeners(key, true).add(l); 150 | } 151 | 152 | protected void unwatch( ZoneKey key, StateListener l ) { 153 | if( log.isTraceEnabled() ) { 154 | log.trace("unwatch(" + key + ", " + l + ")" ); 155 | } 156 | List list = getListeners(key, false); 157 | if( list == null ) { 158 | return; 159 | } 160 | list.remove(l); 161 | } 162 | 163 | protected void unwatchAll( StateListener l ) { 164 | if( log.isTraceEnabled() ) { 165 | log.trace("unwatchAll(" + l + ")" ); 166 | } 167 | for( List list : zoneListeners.values() ) { 168 | list.remove(l); 169 | } 170 | } 171 | 172 | /** 173 | * Called from the background thread when it first starts up. 174 | */ 175 | protected void initialize() { 176 | // Let the zone manager know that it can start collecting history 177 | zones.setCollectHistory(true); 178 | } 179 | 180 | protected void publish( StateBlock b ) { 181 | List list = getListeners(b.getZone(), false); 182 | if( list == null ) { 183 | return; 184 | } 185 | for( StateListener l : list ) { 186 | l.stateChanged(b); 187 | } 188 | } 189 | 190 | /** 191 | * Adjusts the per-listener zone interest based on latest 192 | * listener state, then publishes the state frame to all 193 | * interested listeners. 194 | */ 195 | protected void publishFrame( StateFrame frame ) { 196 | log.trace("publishFrame()"); 197 | 198 | // Somehow handle warped objects in case it would move 199 | // the state listener 200 | if( frame.getWarps() != null ) { 201 | log.info("Need to handle warps this frame:" + frame.getWarps()); 202 | // Any objects warped this frame will have no pending state 203 | // in any of their previous zones but will have no state in any 204 | // new intersecting zones. However, it's a bit tricky because 205 | // we don't know the position of the objects nor which state listeners 206 | // actually care. The state listener would need to know the position 207 | // in order to adjust its 'center'. 208 | // A hacky approach is to go through all of the state blocks and 209 | // check for a state entry. This is an nxnxn loop which is unfortunate. 210 | // Without having StateListener expose its 'self', we can't really make 211 | // it any more efficient. We can avoid some extra loops by at least 212 | // using the hash sets O(1) in our favor. 213 | Set warps = frame.getWarps(); 214 | Set missed = new HashSet<>(frame.getWarps()); 215 | for( StateBlock b : frame ) { 216 | if( b.getUpdates() != null ) { 217 | for( StateBlock.StateEntry e : b.getUpdates() ) { 218 | //log.info("Checking frame:" + e); 219 | if( warps.contains(e.getEntity()) ) { 220 | log.info("Found frame:" + e); 221 | missed.remove(e.getEntity()); 222 | for( StateListener l : listeners ) { 223 | l.objectWarped(e); 224 | } 225 | } 226 | } 227 | } 228 | } 229 | if( !missed.isEmpty() ) { 230 | log.info("Missed warp frames for:" + missed); 231 | } 232 | } 233 | 234 | for( StateListener l : listeners ) { 235 | if( l.hasChangedZones() ) { 236 | List exited = l.getExitedZones(); 237 | for( ZoneKey k : exited ) { 238 | unwatch( k, l ); 239 | } 240 | 241 | List entered = l.getEnteredZones(); 242 | for( ZoneKey k : entered ) { 243 | watch( k, l ); 244 | } 245 | } 246 | l.beginFrame(frame.getTime()); 247 | } 248 | 249 | for( StateBlock b : frame ) { 250 | publish( b ); 251 | } 252 | 253 | for( StateListener l : listeners ) { 254 | l.endFrame(frame.getTime()); 255 | } 256 | log.trace("end publishFrame()"); 257 | } 258 | 259 | /** 260 | * Called by the background thread to collect all 261 | * of the accumulated state since the last collection and 262 | * distribute it to the state listeners. This is called 263 | * once per "collectionPeriod". 264 | */ 265 | protected void collect() { 266 | log.trace("collect()"); 267 | 268 | // Purge any pending removals 269 | StateListener remove; 270 | while( (remove = removed.poll()) != null ) { 271 | unwatchAll(remove); 272 | } 273 | 274 | // Collect all state since the last time we asked 275 | // long start = System.nanoTime(); 276 | StateFrame[] frames = zones.purgeState(); 277 | // long end = System.nanoTime(); 278 | // System.out.println( "State purged in:" + ((end - start)/1000000.0) + " ms" ); 279 | 280 | for( StateListener l : listeners ) { 281 | l.beginFrameBlock(); 282 | } 283 | 284 | // start = end; 285 | for( StateFrame f : frames ) { 286 | if( f == null ) { 287 | continue; 288 | } 289 | publishFrame(f); 290 | } 291 | 292 | for( StateListener l : listeners ) { 293 | l.endFrameBlock(); 294 | } 295 | 296 | // end = System.nanoTime(); 297 | // System.out.println( "State published in:" + ((end - start)/1000000.0) + " ms" ); 298 | log.trace("end collect()"); 299 | } 300 | 301 | /** 302 | * Called by the background thread when it is shutting down. 303 | * Currently does nothing. 304 | */ 305 | protected void terminate() { 306 | // Let the zone manager know that it should stop collecting 307 | // history because we won't be purging it anymore. 308 | zones.setCollectHistory(false); 309 | } 310 | 311 | protected void collectionError( Exception e ) { 312 | log.error("Collection error", e); 313 | } 314 | 315 | private class Runner extends Thread { 316 | private final AtomicBoolean go = new AtomicBoolean(true); 317 | 318 | public Runner() { 319 | setName( "StateCollectionThread" ); 320 | //setPriority( Thread.MAX_PRIORITY ); 321 | } 322 | 323 | public void close() { 324 | go.set(false); 325 | try { 326 | join(); 327 | } catch( InterruptedException e ) { 328 | throw new RuntimeException( "Interrupted while waiting for physic thread to complete.", e ); 329 | } 330 | } 331 | 332 | @Override 333 | public void run() { 334 | initialize(); 335 | long lastTime = System.nanoTime(); 336 | long counter = 0; 337 | long nextCountTime = lastTime + 1000000000L; 338 | while( go.get() ) { 339 | long time = System.nanoTime(); 340 | long delta = time - lastTime; 341 | if( delta >= collectionPeriod ) { 342 | // Time to collect 343 | lastTime = time; 344 | try { 345 | collect(); 346 | counter++; 347 | //long end = System.nanoTime(); 348 | //delta = end - time; 349 | } catch( Exception e ) { 350 | collectionError(e); 351 | } 352 | 353 | if( lastTime > nextCountTime ) { 354 | if( counter < 20 ) { 355 | System.out.println("collect underflow FPS:" + counter); 356 | } 357 | //System.out.println("collect FPS:" + counter); 358 | counter = 0; 359 | nextCountTime = lastTime + 1000000000L; 360 | } 361 | // Don't sleep when we've processed in case we need 362 | // to process again immediately. 363 | continue; 364 | } 365 | 366 | // Wait just a little. This is an important enough thread 367 | // that we'll poll instead of smart-sleep. 368 | try { 369 | if( idleSleepTime > 0 ) { 370 | Thread.sleep(idleSleepTime); 371 | } 372 | } catch( InterruptedException e ) { 373 | throw new RuntimeException("Interrupted sleeping", e); 374 | } 375 | } 376 | terminate(); 377 | } 378 | } 379 | } 380 | 381 | 382 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/zone/StateFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.zone; 38 | 39 | import java.util.*; 40 | 41 | 42 | /** 43 | * 44 | * 45 | * @author Paul Speed 46 | */ 47 | public class StateFrame extends ArrayList { 48 | private final long time; 49 | 50 | private Set warps; 51 | 52 | public StateFrame( long time, int maxZones ) { 53 | super(maxZones); 54 | this.time = time; 55 | } 56 | 57 | public long getTime() { 58 | return time; 59 | } 60 | 61 | public void addWarps( Collection warps ) { 62 | if( warps == null ) { 63 | return; 64 | } 65 | if( this.warps == null ) { 66 | this.warps = new HashSet<>(); 67 | } 68 | this.warps.addAll(warps); 69 | } 70 | 71 | public Set getWarps() { 72 | return warps; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/zone/StateListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.zone; 38 | 39 | import java.util.List; 40 | 41 | 42 | /** 43 | * 44 | * 45 | * @author Paul Speed 46 | */ 47 | public interface StateListener { 48 | 49 | public void beginFrameBlock(); 50 | public void endFrameBlock(); 51 | 52 | /** 53 | * Called after beginFrameBlock() but before beginFrame(), this lets 54 | * the listener know that an object had warped beyond any previous 55 | * zone ranges. The listener can then determine if this would update 56 | * its own active zones and adjust accordingly. 57 | */ 58 | public void objectWarped( StateBlock.StateEntry entry ); 59 | 60 | public void beginFrame(long time); 61 | public void endFrame(long time); 62 | public boolean hasChangedZones(); 63 | public List getExitedZones(); 64 | public List getEnteredZones(); 65 | public void stateChanged( StateBlock b ); 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/zone/Zone.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.zone; 38 | 39 | import com.simsilica.mathd.Quatd; 40 | import com.simsilica.mathd.Vec3d; 41 | import java.util.HashSet; 42 | import java.util.Set; 43 | import org.slf4j.Logger; 44 | import org.slf4j.LoggerFactory; 45 | 46 | 47 | /** 48 | * 49 | * 50 | * @author Paul Speed 51 | */ 52 | public class Zone { 53 | static Logger log = LoggerFactory.getLogger(Zone.class); 54 | 55 | private final ZoneKey key; 56 | private final Set children = new HashSet<>(); 57 | 58 | private StateBlock current; 59 | private final StateBlock[] history; 60 | private int historyIndex = 0; 61 | 62 | public Zone( ZoneKey key, int historyBacklog ) { 63 | this.key = key; 64 | this.history = new StateBlock[historyBacklog]; 65 | } 66 | 67 | public void beginUpdate( long time ) { 68 | if( log.isTraceEnabled() ) { 69 | log.trace(key + ":beginUpdate(" + time + ")"); 70 | } 71 | current = new StateBlock(time, key); 72 | } 73 | 74 | /** 75 | * Adds the update information to the current StateBlock. If parent is null 76 | * then the object is a child of the world and has no parent. 77 | */ 78 | public void update( Long parent, Long id, Vec3d pos, Quatd rotation ) { 79 | if( log.isTraceEnabled() ) { 80 | log.trace(key + ":update(" + id + ", " + pos + ")"); 81 | } 82 | current.addUpdate(parent, id, pos, rotation); 83 | } 84 | 85 | /** 86 | * Adds the warp information to the current StateBlock. 87 | */ 88 | public void warp( Long parent, Long id ) { 89 | if( log.isTraceEnabled() ) { 90 | log.trace(key + ":warp(" + parent + ", " + id + ")"); 91 | } 92 | current.addWarp(parent, id); 93 | } 94 | 95 | public void addChild( Long id ) { 96 | if( log.isTraceEnabled() ) { 97 | log.trace(key + ":addChild(" + id + ")"); 98 | } 99 | if( !children.add(id) ) { 100 | log.warn( "Zone already had a body child for id:" + id ); 101 | } 102 | } 103 | 104 | public void removeChild( Long id ) { 105 | if( log.isTraceEnabled() ) { 106 | log.trace(key + ":removeChild(" + id + ")"); 107 | } 108 | if( !children.remove(id) ) { 109 | log.warn( "Zone did not have child to remove for id:" + id ); 110 | } 111 | current.removeEntity(id); 112 | } 113 | 114 | public final boolean isEmpty() { 115 | return children.isEmpty(); 116 | } 117 | 118 | /** 119 | * Returns true if there was a state block to push or false 120 | * if the state block was empty and history is also empty. 121 | * Note: this assumes the overall history write lock has 122 | * already been obtained and that it is safe to write. 123 | */ 124 | public boolean commitUpdate() { 125 | if( log.isTraceEnabled() ) { 126 | log.trace(key + ":commitUpdate() empty:" + current.isEmpty() + " children:" + children); 127 | } 128 | if( current.isEmpty() ) { 129 | // Return true if history is not empty... false otherwise. 130 | current = null; 131 | return historyIndex != 0; 132 | } 133 | history[historyIndex++] = current; 134 | current = null; 135 | return true; 136 | } 137 | 138 | /** 139 | * Purges the history and returns it. This assumes the overall 140 | * write lock has already been obtained and thus that it is safe 141 | * to clear the history. 142 | */ 143 | public StateBlock[] purgeHistory() { 144 | StateBlock[] result = new StateBlock[historyIndex]; 145 | System.arraycopy( history, 0, result, 0, historyIndex ); 146 | for( int i = 0; i < historyIndex; i++ ) { 147 | history[i] = null; 148 | } 149 | historyIndex = 0; 150 | return result; 151 | } 152 | 153 | @Override 154 | public String toString() { 155 | return "Zone[" + key + ", child.size=" + children.size() + "]"; 156 | } 157 | } 158 | 159 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/zone/ZoneGrid.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.zone; 38 | 39 | import com.simsilica.mathd.Vec3d; 40 | import com.simsilica.mathd.Vec3i; 41 | 42 | 43 | /** 44 | * Can translate to/from integer-based grid coordinates and world 45 | * real coordinates. Note: Zones are essentially 2 dimensional 46 | * in the 'ground' plane, by JME convention: x, z 47 | * 48 | * @author Paul Speed 49 | */ 50 | public class ZoneGrid { 51 | 52 | private Vec3i zoneSize; 53 | 54 | public ZoneGrid( int zoneSize ) { 55 | this(new Vec3i(zoneSize, zoneSize, zoneSize)); 56 | } 57 | 58 | public ZoneGrid( int xZoneSize, int yZoneSize, int zZoneSize ) { 59 | this(new Vec3i(xZoneSize, yZoneSize, zZoneSize)); 60 | } 61 | 62 | public ZoneGrid( Vec3i sizes ) { 63 | this.zoneSize = sizes; 64 | } 65 | 66 | public Vec3i getZoneSize() { 67 | return zoneSize; 68 | } 69 | 70 | private int worldToZone( int i, int size ) { 71 | if( size == 0 ) { 72 | return 0; // special case where the dimension is flattened 73 | } 74 | if( i < 0 ) { 75 | // Need to adjust so that, for example: 76 | // -32 to -1 is -1 instead of part -1 and part 0 77 | i = (i + 1) / size; 78 | return i - 1; 79 | } else { 80 | return i / size; 81 | } 82 | } 83 | 84 | private int worldToZone( double d, int size ) { 85 | return worldToZone((int)Math.floor(d), size); 86 | } 87 | 88 | public Vec3i worldToZone( double x, double y, double z ) { 89 | int i = worldToZone(x, zoneSize.x); 90 | int j = worldToZone(y, zoneSize.y); 91 | int k = worldToZone(z, zoneSize.z); 92 | return new Vec3i(i, j, k); 93 | } 94 | 95 | public Vec3i worldToZone( Vec3d world ) { 96 | int i = worldToZone(world.x, zoneSize.x); 97 | int j = worldToZone(world.y, zoneSize.y); 98 | int k = worldToZone(world.z, zoneSize.z); 99 | return new Vec3i(i, j, k); 100 | } 101 | 102 | private int zoneToWorld( int i, int size ) { 103 | return i * size; 104 | } 105 | 106 | public Vec3i zoneToWorld( int x, int y, int z ) { 107 | int i = zoneToWorld(x, zoneSize.x); 108 | int j = zoneToWorld(y, zoneSize.y); 109 | int k = zoneToWorld(z, zoneSize.z); 110 | return new Vec3i(i, j, k); 111 | } 112 | 113 | public ZoneKey worldToKey( Vec3d pos ) { 114 | return worldToKey(pos.x, pos.y, pos.z); 115 | } 116 | 117 | public ZoneKey worldToKey( double x, double y, double z ) { 118 | int i = worldToZone(x, zoneSize.x); 119 | int j = worldToZone(y, zoneSize.y); 120 | int k = worldToZone(z, zoneSize.z); 121 | return new ZoneKey(this, i, j, k); 122 | } 123 | 124 | public Vec3i zoneToWorld( ZoneKey key ) { 125 | return key.origin.clone(); 126 | } 127 | 128 | /** 129 | * Converts the x, y, z to a single long by masking 130 | * each value and bit shifting. This effectively means 131 | * the zone x,y,z values are limited to 2^21 (+/- 2^20) but that's probably 132 | * ok for all reasonable implementations. It's over a million zones in 133 | * every direction. 134 | */ 135 | public long toLongId( ZoneKey key ) { 136 | long x = key.x & 0x1fffffL; 137 | long y = key.y & 0x1fffffL; 138 | long z = key.z & 0x1fffffL; 139 | return (x << 42) | (y << 21) | z; 140 | } 141 | 142 | public ZoneKey fromLongId( long id ) { 143 | int z = (int)(id & 0x1fffffL); 144 | if( (z & 0x100000) != 0 ) { 145 | // Sign extend 146 | z = z | 0xfff00000; 147 | } 148 | int y = (int)((id >>> 21) & 0x1fffffL); 149 | if( (y & 0x100000) != 0 ) { 150 | // Sign extend 151 | y = y | 0xfff00000; 152 | } 153 | int x = (int)((id >>> 42) & 0x1fffffL); 154 | if( (x & 0x100000) != 0 ) { 155 | // Sign extend 156 | x = x | 0xfff00000; 157 | } 158 | 159 | return new ZoneKey(this, x, y, z); 160 | } 161 | 162 | public static void main( String... args ) { 163 | ZoneGrid grid = new ZoneGrid(32); 164 | 165 | int maxValue = 0xfffff; 166 | int minValue = -0xfffff; 167 | 168 | ZoneKey[] test = new ZoneKey[] { 169 | new ZoneKey(grid, 0, 0, 0), 170 | new ZoneKey(grid, 1, 1, 1), 171 | new ZoneKey(grid, 1, -1, 0), 172 | new ZoneKey(grid, 100, 100, 100), 173 | new ZoneKey(grid, -1, -1, -1), 174 | new ZoneKey(grid, -100, -100, -100), 175 | new ZoneKey(grid, maxValue, maxValue, maxValue), 176 | new ZoneKey(grid, minValue, minValue, minValue) 177 | }; 178 | 179 | for( ZoneKey k : test ) { 180 | System.out.println("Key:" + k); 181 | long id = grid.toLongId(k); 182 | System.out.println(" id:" + id + " " + Long.toHexString(id)); 183 | System.out.println(" reverse:" + grid.fromLongId(id)); 184 | } 185 | } 186 | 187 | 188 | @Override 189 | public String toString() { 190 | return "Grid[" + zoneSize + "]"; 191 | } 192 | } 193 | 194 | 195 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/ethereal/zone/ZoneKey.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2015, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.ethereal.zone; 38 | 39 | import com.simsilica.mathd.Vec3d; 40 | import com.simsilica.mathd.Vec3i; 41 | 42 | 43 | /** 44 | * A unique key for a particular zone in space. This is slightly 45 | * beyond a location because it also has a reference to its 'owning' 46 | * grid and thus can be used in a hashmap that manages multiple grid 47 | * sizes. (Be sure to reuse ZoneGrid instances for == comparison.) 48 | * 49 | * @author Paul Speed 50 | */ 51 | public class ZoneKey { 52 | 53 | public ZoneGrid grid; 54 | public int x; 55 | public int y; 56 | public int z; 57 | public Vec3i origin; 58 | 59 | public ZoneKey( ZoneGrid grid, int x, int y, int z ) { 60 | this.grid = grid; 61 | this.x = x; 62 | this.y = y; 63 | this.z = z; 64 | this.origin = grid.zoneToWorld(x, y, z); 65 | } 66 | 67 | public Vec3d toWorld( Vec3d relative ) { 68 | return toWorld(relative, new Vec3d()); 69 | } 70 | 71 | public Vec3d toWorld( Vec3d relative, Vec3d store ) { 72 | store.x = origin.x + relative.x; 73 | store.y = origin.y + relative.y; 74 | store.z = origin.z + relative.z; 75 | return store; 76 | } 77 | 78 | public Vec3d toLocal( Vec3d world ) { 79 | return toLocal(world, new Vec3d()); 80 | } 81 | 82 | public Vec3d toLocal( Vec3d world, Vec3d store ) { 83 | store.x = world.x - origin.x; 84 | store.y = world.y - origin.y; 85 | store.z = world.z - origin.z; 86 | return store; 87 | } 88 | 89 | public long toLongId() { 90 | return grid.toLongId(this); 91 | } 92 | 93 | @Override 94 | public boolean equals( Object o ) { 95 | if( o == this ) 96 | return true; 97 | if( o == null || o.getClass() != getClass() ) 98 | return false; 99 | ZoneKey other = (ZoneKey)o; 100 | return other.x == x && other.y == y && other.z == z && other.grid == grid; 101 | } 102 | 103 | @Override 104 | public int hashCode() { 105 | int hash = 37; 106 | hash += 37 * hash + x; 107 | hash += 37 * hash + y; 108 | hash += 37 * hash + z; 109 | return hash; 110 | } 111 | 112 | @Override 113 | public String toString() { 114 | return x + ":" + y + ":" + z; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/simsilica/util/BufferedHashSet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id$ 3 | * 4 | * Copyright (c) 2018, Simsilica, LLC 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions 9 | * are met: 10 | * 11 | * 1. Redistributions of source code must retain the above copyright 12 | * notice, this list of conditions and the following disclaimer. 13 | * 14 | * 2. Redistributions in binary form must reproduce the above copyright 15 | * notice, this list of conditions and the following disclaimer in 16 | * the documentation and/or other materials provided with the 17 | * distribution. 18 | * 19 | * 3. Neither the name of the copyright holder nor the names of its 20 | * contributors may be used to endorse or promote products derived 21 | * from this software without specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 26 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 27 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 28 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 29 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 30 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 31 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 32 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 34 | * OF THE POSSIBILITY OF SUCH DAMAGE. 35 | */ 36 | 37 | package com.simsilica.util; 38 | 39 | import java.util.*; 40 | 41 | //import com.google.common.collect.Iterators; 42 | 43 | /** 44 | * A HashSet implementation that buffers modifications until a commit 45 | * is performeed. The buffering can be done from one thread and the 46 | * reads from another. So reads are safe from any thread but writes 47 | * must be done from only one thread or properly synchronized externally. 48 | * This is useful for efficiently buffering the output of one process 49 | * for another. 50 | * 51 | * Note: this doubles the storage requirements over a typical HashSet 52 | * is it internally keeps two sets, one for the writer and one for all 53 | * readers. 54 | * 55 | * Also note: the semantics of the way reads and writes are decoupled 56 | * mean that it is possible for add(element) to return false (indicating 57 | * that the set already has the value) while contains(element) will also 58 | * return false (because the element hasn't been committed yet). The 59 | * writing thread should use getTransaction() if it wants to check 60 | * transactional data integrity for some reason. 61 | * 62 | * @author Paul Speed 63 | */ 64 | public class BufferedHashSet implements Set { 65 | 66 | private HashSet buffer = new HashSet<>(); 67 | private volatile HashSet delegate = new HashSet<>(); 68 | private Thread writer = null; 69 | 70 | public BufferedHashSet() { 71 | } 72 | 73 | private boolean checkThread() { 74 | if( writer == null ) { 75 | writer = Thread.currentThread(); 76 | return true; 77 | } 78 | return writer == Thread.currentThread(); 79 | } 80 | 81 | private String badThreadMessage() { 82 | return "Non-write thread:" + Thread.currentThread() + " accessing as writer:" + writer; 83 | } 84 | 85 | /** 86 | * Returns the transactional buffer. Note: access to this 87 | * is only thread safe from the writing thread. 88 | */ 89 | public Set getTransaction() { 90 | assert checkThread() : badThreadMessage(); 91 | return buffer; 92 | } 93 | 94 | /** 95 | * Called from the writing thread to apply the buffer changes 96 | * to the readable view. 97 | */ 98 | @SuppressWarnings("unchecked") 99 | public void commit() { 100 | assert checkThread() : badThreadMessage(); 101 | delegate = buffer; 102 | buffer = (HashSet)delegate.clone(); 103 | } 104 | 105 | /** 106 | * Returns a snapshot of the readable view of this HashSet at the 107 | * time this method is called. Subsequent commits will not affect 108 | * this view. 109 | */ 110 | public Set getSnapshot() { 111 | return Collections.unmodifiableSet(delegate); 112 | } 113 | 114 | @Override 115 | public int size() { 116 | return delegate.size(); 117 | } 118 | 119 | @Override 120 | public boolean isEmpty() { 121 | return delegate.isEmpty(); 122 | } 123 | 124 | @Override 125 | public boolean contains( Object o ) { 126 | return delegate.contains(o); 127 | } 128 | 129 | @Override 130 | public Iterator iterator() { 131 | //return Iterators.unmodifiableIterator(delegate.iterator()); 132 | return delegate.iterator(); 133 | } 134 | 135 | @Override 136 | public Object[] toArray() { 137 | return delegate.toArray(); 138 | } 139 | 140 | @Override 141 | public T[] toArray( T[] a ) { 142 | return delegate.toArray(a); 143 | } 144 | 145 | /** 146 | * Updates the write buffer to reflect the addition of the specified element. 147 | * This won't show up to the read methods until a commit() is performed. 148 | * Callers wishing to see data in the buffer must use the Set returned 149 | * from getTransaction(). 150 | */ 151 | @Override 152 | public boolean add( E e ) { 153 | assert checkThread() : badThreadMessage(); 154 | return buffer.add(e); 155 | } 156 | 157 | /** 158 | * Updates the write buffer to reflect the removal of the specified element. 159 | * This won't show up to the read methods until a commit() is performed. 160 | * Callers wishing to see data in the buffer must use the Set returned 161 | * from getTransaction(). 162 | */ 163 | @Override 164 | public boolean remove( Object o ) { 165 | assert checkThread() : badThreadMessage(); 166 | return buffer.remove(o); 167 | } 168 | 169 | @Override 170 | public boolean containsAll( Collection c ) { 171 | return delegate.containsAll(c); 172 | } 173 | 174 | /** 175 | * Updates the write buffer to reflect the removal of the specified elements. 176 | * This won't show up to the read methods until a commit() is performed. 177 | * Callers wishing to see data in the buffer must use the Set returned 178 | * from getTransaction(). 179 | */ 180 | @Override 181 | public boolean addAll( Collection c ) { 182 | return buffer.addAll(c); 183 | } 184 | 185 | /** 186 | * Updates the write buffer to reflect the retention of the specified elements. 187 | * This won't show up to the read methods until a commit() is performed. 188 | * Callers wishing to see data in the buffer must use the Set returned 189 | * from getTransaction(). 190 | */ 191 | @Override 192 | public boolean retainAll( Collection c ) { 193 | assert checkThread() : badThreadMessage(); 194 | return buffer.retainAll(c); 195 | } 196 | 197 | /** 198 | * Updates the write buffer to reflect the removal of the specified elements. 199 | * This won't show up to the read methods until a commit() is performed. 200 | * Callers wishing to see data in the buffer must use the Set returned 201 | * from getTransaction(). 202 | */ 203 | @Override 204 | public boolean removeAll( Collection c ) { 205 | assert checkThread() : badThreadMessage(); 206 | return buffer.removeAll(c); 207 | } 208 | 209 | /** 210 | * Clears the write buffer. 211 | * This won't show up to the read methods until a commit() is performed. 212 | * Callers wishing to see data in the buffer must use the Set returned 213 | * from getTransaction(). 214 | */ 215 | @Override 216 | public void clear() { 217 | assert checkThread() : badThreadMessage(); 218 | buffer.clear(); 219 | } 220 | 221 | @Override 222 | public boolean equals( Object o ) { 223 | if( o == this ) { 224 | return true; 225 | } 226 | if( o == null || o.getClass() != getClass() ) { 227 | return false; 228 | } 229 | BufferedHashSet other = (BufferedHashSet)o; 230 | return Objects.equals(delegate, other.delegate); 231 | } 232 | 233 | @Override 234 | public int hashCode() { 235 | return delegate.hashCode(); 236 | } 237 | 238 | @Override 239 | public String toString() { 240 | return delegate.toString(); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/main/resources/license.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Simsilica, LLC 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions 7 | * are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright 10 | * notice, this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in 14 | * the documentation and/or other materials provided with the 15 | * distribution. 16 | * 17 | * 3. Neither the name of the copyright holder nor the names of its 18 | * contributors may be used to endorse or promote products derived 19 | * from this software without specific prior written permission. 20 | * 21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 24 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 25 | * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 26 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 29 | * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 30 | * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 31 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 32 | * OF THE POSSIBILITY OF SUCH DAMAGE. 33 | */ 34 | 35 | --------------------------------------------------------------------------------